package world.bentobox.bentobox.blueprints; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.Banner; import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.block.CreatureSpawner; import org.bukkit.block.Sign; import org.bukkit.block.sign.Side; import org.bukkit.entity.AbstractHorse; import org.bukkit.entity.Ageable; import org.bukkit.entity.ChestedHorse; import org.bukkit.entity.Horse; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.entity.Tameable; import org.bukkit.entity.Villager; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import org.bukkit.material.Attachable; import org.bukkit.material.Colorable; import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.BoundingBox; import org.bukkit.util.Vector; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBlock; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintCreatureSpawner; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintEntity; import world.bentobox.bentobox.hooks.MythicMobsHook; /** * The clipboard provides the holding spot for an active blueprint that is being * manipulated by a user. It supports copying from the world and setting of coordinates * such as the bounding box around the cuboid copy area. * Pasting is done by the {@link BlueprintPaster} class. * @author tastybento * @since 1.5.0 */ public class BlueprintClipboard { private @Nullable Blueprint blueprint; private @Nullable Location pos1; private @Nullable Location pos2; private @Nullable Vector origin; private BukkitTask copyTask; private int count; private boolean copying; private int index; private int lastPercentage; private final Map> bpEntities = new LinkedHashMap<>(); private final Map bpAttachable = new LinkedHashMap<>(); private final Map bpBlocks = new LinkedHashMap<>(); private final BentoBox plugin = BentoBox.getInstance(); private Optional mmh; /** * Create a clipboard for blueprint * @param blueprint - the blueprint to load into the clipboard */ public BlueprintClipboard(@NonNull Blueprint blueprint) { this.blueprint = blueprint; // MythicMobs mmh = plugin.getHooks().getHook("MythicMobs").filter(MythicMobsHook.class::isInstance) .map(MythicMobsHook.class::cast); } public BlueprintClipboard() { // MythicMobs mmh = plugin.getHooks().getHook("MythicMobs").filter(MythicMobsHook.class::isInstance) .map(MythicMobsHook.class::cast); } /** * Copy the blocks between pos1 and pos2 into the clipboard for a user. * This will erase any previously registered data from the clipboard. * Copying is done async. * @param user - user * @return true if successful, false if pos1 or pos2 are undefined. */ public boolean copy(User user, boolean copyAir, boolean copyBiome) { if (copying) { user.sendMessage("commands.admin.blueprint.mid-copy"); return false; } if (pos1 == null || pos2 == null) { user.sendMessage("commands.admin.blueprint.need-pos1-pos2"); return false; } if (origin == null) { setOrigin(user.getLocation().toVector()); } user.sendMessage("commands.admin.blueprint.copying"); // World World world = pos1.getWorld(); if (world == null) { return false; } // Clear the clipboard blueprint = new Blueprint(); bpEntities.clear(); bpAttachable.clear(); bpBlocks.clear(); count = 0; index = 0; lastPercentage = 0; BoundingBox toCopy = BoundingBox.of(pos1, pos2); blueprint.setxSize((int)toCopy.getWidthX()); blueprint.setySize((int)toCopy.getHeight()); blueprint.setzSize((int)toCopy.getWidthZ()); int speed = plugin.getSettings().getPasteSpeed(); List vectorsToCopy = getVectors(toCopy); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> copyAsync(world, user, vectorsToCopy, speed, copyAir, copyBiome)); return true; } private void copyAsync(World world, User user, List vectorsToCopy, int speed, boolean copyAir, boolean copyBiome) { copying = false; copyTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { if (copying) { return; } copying = true; vectorsToCopy.stream().skip(index).limit(speed).forEach(v -> { List ents = world.getLivingEntities().stream() .filter(Objects::nonNull) .filter(e -> !(e instanceof Player)) .filter(e -> new Vector(Math.rint(e.getLocation().getX()), Math.rint(e.getLocation().getY()), Math.rint(e.getLocation().getZ())).equals(v)) .toList(); if (copyBlock(v.toLocation(world), copyAir, copyBiome, ents)) { count++; } }); index += speed; int percent = (int)(index * 100 / (double)vectorsToCopy.size()); if (percent != lastPercentage && percent % 10 == 0) { user.sendMessage("commands.admin.blueprint.copied-percent", TextVariables.NUMBER, String.valueOf(percent)); lastPercentage = percent; } if (index > vectorsToCopy.size()) { copyTask.cancel(); assert blueprint != null; blueprint.setAttached(bpAttachable); blueprint.setBlocks(bpBlocks); blueprint.setEntities(bpEntities); user.sendMessage("general.success"); user.sendMessage("commands.admin.blueprint.copied-blocks", TextVariables.NUMBER, String.valueOf(count)); } copying = false; }, 0L, 1L); } /** * Get all the x,y,z coords that must be copied * @param b - bounding box * @return - list of vectors */ protected List getVectors(BoundingBox b) { List r = new ArrayList<>(); for (int y = (int) Math.floor(b.getMinY()); y <= b.getMaxY(); y++) { for (int x = (int) Math.floor(b.getMinX()); x <= b.getMaxX(); x++) { for (int z = (int) Math.floor(b.getMinZ()); z <= b.getMaxZ(); z++) { r.add(new Vector(x,y,z)); } } } return r; } private boolean copyBlock(Location l, boolean copyAir, boolean copyBiome, Collection entities) { Block block = l.getBlock(); if (!copyAir && block.getType().equals(Material.AIR) && entities.isEmpty()) { return false; } // Create position Vector origin2 = origin == null ? new Vector(0,0,0) : origin; int x = l.getBlockX() - origin2.getBlockX(); int y = l.getBlockY() - origin2.getBlockY(); int z = l.getBlockZ() - origin2.getBlockZ(); Vector pos = new Vector(x, y, z); // Set entities List bpEnts = setEntities(entities); // Store if (!bpEnts.isEmpty()) { bpEntities.put(pos, bpEnts); } // Return if this is just air block if (!copyAir && block.getType().equals(Material.AIR) && !entities.isEmpty()) { return true; } BlueprintBlock b = bluePrintBlock(pos, block, copyBiome); if (b != null) { this.bpBlocks.put(pos, b); } return true; } private BlueprintBlock bluePrintBlock(Vector pos, Block block, boolean copyBiome) { // Block state BlockState blockState = block.getState(); BlueprintBlock b = new BlueprintBlock(block.getBlockData().getAsString()); if (copyBiome) { // Biome b.setBiome(block.getBiome()); } // Signs if (blockState instanceof Sign sign) { for (Side side : Side.values()) { b.setSignLines(side, Arrays.asList(sign.getSide(side).getLines())); b.setGlowingText(side, sign.getSide(side).isGlowingText()); } } // Set block data if (blockState.getData() instanceof Attachable) { // Placeholder for attachment bpBlocks.put(pos, new BlueprintBlock("minecraft:air")); bpAttachable.put(pos, b); return null; } if (block.getType().equals(Material.BEDROCK)) { // Find highest bedrock if(blueprint.getBedrock() == null) { blueprint.setBedrock(pos); } else { if (pos.getBlockY() > blueprint.getBedrock().getBlockY()) { blueprint.setBedrock(pos); } } } // Chests if (blockState instanceof InventoryHolder ih) { b.setInventory(new HashMap<>()); for (int i = 0; i < ih.getInventory().getSize(); i++) { ItemStack item = ih.getInventory().getItem(i); if (item != null) { b.getInventory().put(i, item); } } } if (blockState instanceof CreatureSpawner spawner) { b.setCreatureSpawner(getSpawner(spawner)); } // Banners if (blockState instanceof Banner banner) { b.setBannerPatterns(banner.getPatterns()); } return b; } private BlueprintCreatureSpawner getSpawner(CreatureSpawner spawner) { BlueprintCreatureSpawner cs = new BlueprintCreatureSpawner(); cs.setSpawnedType(spawner.getSpawnedType()); cs.setDelay(spawner.getDelay()); cs.setMaxNearbyEntities(spawner.getMaxNearbyEntities()); cs.setMaxSpawnDelay(spawner.getMaxSpawnDelay()); cs.setMinSpawnDelay(spawner.getMinSpawnDelay()); cs.setRequiredPlayerRange(spawner.getRequiredPlayerRange()); cs.setSpawnRange(spawner.getSpawnRange()); return cs; } private List setEntities(Collection entities) { List bpEnts = new ArrayList<>(); for (LivingEntity entity: entities) { BlueprintEntity bpe = new BlueprintEntity(); bpe.setType(entity.getType()); bpe.setCustomName(entity.getCustomName()); if (entity instanceof Villager villager) { setVillager(villager, bpe); } if (entity instanceof Colorable c && c.getColor() != null) { bpe.setColor(c.getColor()); } if (entity instanceof Tameable tameable) { bpe.setTamed(tameable.isTamed()); } if (entity instanceof ChestedHorse chestedHorse) { bpe.setChest(chestedHorse.isCarryingChest()); } // Only set if child. Most animals are adults if (entity instanceof Ageable ageable && !ageable.isAdult()) { bpe.setAdult(false); } if (entity instanceof AbstractHorse horse) { bpe.setDomestication(horse.getDomestication()); bpe.setInventory(new HashMap<>()); for (int i = 0; i < horse.getInventory().getSize(); i++) { ItemStack item = horse.getInventory().getItem(i); if (item != null) { bpe.getInventory().put(i, item); } } } if (entity instanceof Horse horse) { bpe.setStyle(horse.getStyle()); } mmh.filter(mm -> mm.isMythicMob(entity)).map(mm -> mm.getMythicMob(entity)) .ifPresent(mmr -> bpe.setMythicMobsRecord(mmr)); bpEnts.add(bpe); } return bpEnts; } /** * Set the villager stats * @param v - villager * @param bpe - Blueprint Entity */ private void setVillager(Villager v, BlueprintEntity bpe) { bpe.setExperience(v.getVillagerExperience()); bpe.setLevel(v.getVillagerLevel()); bpe.setProfession(v.getProfession()); bpe.setVillagerType(v.getVillagerType()); } /** * @return the origin */ @Nullable public Vector getOrigin() { return origin; } /** * @return the pos1 */ @Nullable public Location getPos1() { return pos1; } /** * @return the pos2 */ @Nullable public Location getPos2() { return pos2; } public boolean isFull() { return blueprint != null; } /** * @param origin the origin to set */ public void setOrigin(@Nullable Vector origin) { this.origin = origin; } /** * @param pos1 the pos1 to set */ public void setPos1(@Nullable Location pos1) { origin = null; if (pos1 != null) { final int minHeight = pos1.getWorld() == null ? 0 : pos1.getWorld().getMinHeight(); final int maxHeight = pos1.getWorld() == null ? 255 : pos1.getWorld().getMaxHeight(); if (pos1.getBlockY() < minHeight) { pos1.setY(minHeight); } if (pos1.getBlockY() > maxHeight) { pos1.setY(maxHeight); } } this.pos1 = pos1; } /** * @param pos2 the pos2 to set */ public void setPos2(@Nullable Location pos2) { origin = null; if (pos2 != null) { final int minHeight = pos2.getWorld() == null ? 0 : pos2.getWorld().getMinHeight(); final int maxHeight = pos2.getWorld() == null ? 255 : pos2.getWorld().getMaxHeight(); if (pos2.getBlockY() < minHeight) { pos2.setY(minHeight); } if (pos2.getBlockY() > maxHeight) { pos2.setY(maxHeight); } } this.pos2 = pos2; } /** * @return the blueprint */ public @Nullable Blueprint getBlueprint() { return blueprint; } /** * @param blueprint the blueprint to set */ public BlueprintClipboard setBlueprint(Blueprint blueprint) { this.blueprint = blueprint; return this; } }