bentobox/src/main/java/world/bentobox/bentobox/blueprints/BlueprintClipboard.java

412 lines
14 KiB
Java

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.stream.Collectors;
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.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;
/**
* 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<Vector, List<BlueprintEntity>> bpEntities = new LinkedHashMap<>();
private final Map<Vector, BlueprintBlock> bpAttachable = new LinkedHashMap<>();
private final Map<Vector, BlueprintBlock> bpBlocks = new LinkedHashMap<>();
private final BentoBox plugin = BentoBox.getInstance();
/**
* Create a clipboard for blueprint
* @param blueprint - the blueprint to load into the clipboard
*/
public BlueprintClipboard(@NonNull Blueprint blueprint) {
this.blueprint = blueprint;
}
public BlueprintClipboard() { }
/**
* 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) {
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<Vector> vectorsToCopy = getVectors(toCopy);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> copyAsync(world, user, vectorsToCopy, speed, copyAir));
return true;
}
private void copyAsync(World world, User user, List<Vector> vectorsToCopy, int speed, boolean copyAir) {
copying = false;
copyTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
if (copying) {
return;
}
copying = true;
vectorsToCopy.stream().skip(index).limit(speed).forEach(v -> {
List<LivingEntity> 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))
.collect(Collectors.toList());
if (copyBlock(v.toLocation(world), origin, copyAir, 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();
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
*/
private List<Vector> getVectors(BoundingBox b) {
List<Vector> r = new ArrayList<>();
for (int y = (int)b.getMinY(); y <= b.getMaxY(); y++) {
for (int x = (int)b.getMinX(); x <= b.getMaxX(); x++) {
for (int z = (int)b.getMinZ(); z <= b.getMaxZ(); z++) {
r.add(new Vector(x,y,z));
}
}
}
return r;
}
private boolean copyBlock(Location l, @Nullable Vector origin2, boolean copyAir, Collection<LivingEntity> entities) {
Block block = l.getBlock();
if (!copyAir && block.getType().equals(Material.AIR) && entities.isEmpty()) {
return false;
}
// Create position
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<BlueprintEntity> 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);
if (b != null) {
this.bpBlocks.put(pos, b);
}
return true;
}
private BlueprintBlock bluePrintBlock(Vector pos, Block block) {
// Block state
BlockState blockState = block.getState();
BlueprintBlock b = new BlueprintBlock(block.getBlockData().getAsString());
// Biome
b.setBiome(block.getBiome());
// Signs
if (blockState instanceof Sign sign) {
b.setSignLines(Arrays.asList(sign.getLines()));
}
// 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) {
b.setBannerPatterns(((Banner) blockState).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<BlueprintEntity> setEntities(Collection<LivingEntity> entities) {
List<BlueprintEntity> bpEnts = new ArrayList<>();
for (LivingEntity entity: entities) {
BlueprintEntity bpe = new BlueprintEntity();
bpe.setType(entity.getType());
bpe.setCustomName(entity.getCustomName());
if (entity instanceof Villager) {
setVillager(entity, bpe);
}
if (entity instanceof Colorable c) {
if (c.getColor() != null) {
bpe.setColor(c.getColor());
}
}
if (entity instanceof Tameable) {
bpe.setTamed(((Tameable)entity).isTamed());
}
if (entity instanceof ChestedHorse) {
bpe.setChest(((ChestedHorse)entity).isCarryingChest());
}
// Only set if child. Most animals are adults
if (entity instanceof Ageable && !((Ageable)entity).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());
}
bpEnts.add(bpe);
}
return bpEnts;
}
/**
* Set the villager stats
* @param entity - villager
* @param bpe - Blueprint Entity
*/
private void setVillager(LivingEntity entity, BlueprintEntity bpe) {
Villager v = (Villager)entity;
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) {
if (pos1.getBlockY() < 0) {
pos1.setY(0);
}
if (pos1.getBlockY() > 255) {
pos1.setY(255);
}
}
this.pos1 = pos1;
}
/**
* @param pos2 the pos2 to set
*/
public void setPos2(@Nullable Location pos2) {
origin = null;
if (pos2 != null) {
if (pos2.getBlockY() < 0) {
pos2.setY(0);
}
if (pos2.getBlockY() > 255) {
pos2.setY(255);
}
}
this.pos2 = pos2;
}
/**
* @return the blueprint
*/
public Blueprint getBlueprint() {
return blueprint;
}
/**
* @param blueprint the blueprint to set
*/
public BlueprintClipboard setBlueprint(Blueprint blueprint) {
this.blueprint = blueprint;
return this;
}
}