334 lines
13 KiB
Java
334 lines
13 KiB
Java
package world.bentobox.bentobox.blueprints;
|
|
|
|
import java.math.BigDecimal;
|
|
import java.math.RoundingMode;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Map.Entry;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
import java.util.concurrent.CompletableFuture;
|
|
|
|
import org.bukkit.Bukkit;
|
|
import org.bukkit.Location;
|
|
import org.bukkit.World;
|
|
import org.bukkit.scheduler.BukkitTask;
|
|
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.BlueprintEntity;
|
|
import world.bentobox.bentobox.database.objects.Island;
|
|
import world.bentobox.bentobox.nms.PasteHandler;
|
|
import world.bentobox.bentobox.util.Util;
|
|
|
|
/**
|
|
* This class pastes the clipboard it is given
|
|
* @author tastybento
|
|
*
|
|
*/
|
|
public class BlueprintPaster {
|
|
|
|
/**
|
|
* This tracks the stages of pasting from loading the chunk, pasting blocks, attachments, entities and then finishing.
|
|
*/
|
|
enum PasteState {
|
|
CHUNK_LOAD,
|
|
CHUNK_LOADING,
|
|
BLOCKS,
|
|
ATTACHMENTS,
|
|
ENTITIES,
|
|
DONE,
|
|
CANCEL
|
|
}
|
|
|
|
/**
|
|
* Longest chunk loading time experienced when pasting an island.
|
|
* It is used to fine-tune the estimated pasting time.
|
|
* @since 1.11.1
|
|
*/
|
|
private static long chunkLoadTime = 0;
|
|
|
|
private final BentoBox plugin;
|
|
private final PasteHandler paster = Util.getPasteHandler();
|
|
private final PasteHandler fallback = new world.bentobox.bentobox.nms.fallback.PasteHandlerImpl();
|
|
private final World world;
|
|
// The minimum block position (x,y,z)
|
|
private Location pos1;
|
|
// The maximum block position (x,y,z)
|
|
private Location pos2;
|
|
private PasteState pasteState;
|
|
private BukkitTask pastingTask;
|
|
private BlueprintClipboard clipboard;
|
|
private CompletableFuture<Void> currentTask = CompletableFuture.completedFuture(null);
|
|
|
|
/**
|
|
* The Blueprint to paste.
|
|
*/
|
|
@NonNull
|
|
private final Blueprint blueprint;
|
|
|
|
/**
|
|
* The Location to paste to.
|
|
*/
|
|
@NonNull
|
|
private final Location location;
|
|
|
|
/**
|
|
* Island related to this paste, may be null.
|
|
*/
|
|
@Nullable
|
|
private final Island island;
|
|
|
|
/**
|
|
* Paste a clipboard to a location. Run {@link #paste()} to paste
|
|
* @param plugin - BentoBox
|
|
* @param clipboard - clipboard to paste
|
|
* @param location - location to which to paste
|
|
*/
|
|
public BlueprintPaster(@NonNull BentoBox plugin, @NonNull BlueprintClipboard clipboard, @NonNull Location location) {
|
|
this.plugin = plugin;
|
|
this.clipboard = clipboard;
|
|
// Calculate location for pasting
|
|
this.blueprint = Objects.requireNonNull(clipboard.getBlueprint(), "Clipboard cannot have a null Blueprint");
|
|
this.location = location;
|
|
this.world = location.getWorld();
|
|
this.island = null;
|
|
}
|
|
|
|
/**
|
|
* Pastes a blueprint to an island
|
|
* @param plugin - BentoBox
|
|
* @param bp - blueprint to paste
|
|
* @param world - world to paste to
|
|
* @param island - island related to this paste
|
|
*/
|
|
public BlueprintPaster(@NonNull BentoBox plugin, @NonNull Blueprint bp, World world, @NonNull Island island) {
|
|
this.plugin = plugin;
|
|
this.blueprint = bp;
|
|
this.island = island;
|
|
this.world = world;
|
|
// Offset due to bedrock
|
|
Vector off = bp.getBedrock() != null ? bp.getBedrock() : new Vector(0,0,0);
|
|
// Calculate location for pasting
|
|
this.location = island.getProtectionCenter().toVector().subtract(off).toLocation(world);
|
|
}
|
|
|
|
private record Bits(Map<Vector, BlueprintBlock> blocks,
|
|
Map<Vector, BlueprintBlock> attached,
|
|
Map<Vector, List<BlueprintEntity>> entities,
|
|
Iterator<Entry<Vector, BlueprintBlock>> it,
|
|
Iterator<Entry<Vector, BlueprintBlock>> it2,
|
|
Iterator<Entry<Vector, List<BlueprintEntity>>> it3,
|
|
int pasteSpeed) {}
|
|
|
|
/**
|
|
* The main pasting method
|
|
*/
|
|
public CompletableFuture<Boolean> paste() {
|
|
return this.paste(true);
|
|
}
|
|
|
|
/**
|
|
* Paste the clipboard
|
|
* @param useNMS if true, NMS pasting will be used, otherwise Bukkit API
|
|
* @return Future boolean where true is success
|
|
*/
|
|
public CompletableFuture<Boolean> paste(boolean useNMS) {
|
|
CompletableFuture<Boolean> result = new CompletableFuture<>();
|
|
// Iterators for the various maps to paste
|
|
final Map<Vector, BlueprintBlock> blocks = blueprint.getBlocks() == null ? Collections.emptyMap() : blueprint.getBlocks();
|
|
final Map<Vector, BlueprintBlock> attached = blueprint.getAttached() == null ? Collections.emptyMap() : blueprint.getAttached();
|
|
final Map<Vector, List<BlueprintEntity>> entities = blueprint.getEntities() == null ? Collections.emptyMap() : blueprint.getEntities();
|
|
|
|
// Initial state & speed
|
|
pasteState = PasteState.CHUNK_LOAD;
|
|
|
|
// If this is an island OVERWORLD paste, get the island owner.
|
|
final Optional<User> owner = Optional.ofNullable(island).map(i -> User.getInstance(i.getOwner()));
|
|
|
|
// Tell the owner we're pasting blocks and how much time it might take
|
|
owner.ifPresent(user -> tellOwner(user, blocks.size(), attached.size(), entities.size(), plugin.getSettings().getPasteSpeed()));
|
|
Bits bits = new Bits(blocks, attached, entities,
|
|
blocks.entrySet().iterator(), attached.entrySet().iterator(), entities.entrySet().iterator(),
|
|
plugin.getSettings().getPasteSpeed());
|
|
pastingTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> pasterTask(result, owner, bits, useNMS), 0L, 1L);
|
|
|
|
return result;
|
|
}
|
|
|
|
private void pasterTask(CompletableFuture<Boolean> result, Optional<User> owner, Bits bits, boolean useNMS) {
|
|
if (!currentTask.isDone()) return;
|
|
|
|
final int pasteSpeed = plugin.getSettings().getPasteSpeed();
|
|
|
|
int count = 0;
|
|
if (pasteState.equals(PasteState.CHUNK_LOAD)) {
|
|
loadChunk();
|
|
}
|
|
else if (pasteState.equals(PasteState.BLOCKS) || pasteState.equals(PasteState.ATTACHMENTS)) {
|
|
pasteBlocks(bits, count, owner, pasteSpeed, useNMS);
|
|
}
|
|
else if (pasteState.equals(PasteState.ENTITIES)) {
|
|
pasteEntities(bits, count, owner, pasteSpeed, useNMS);
|
|
}
|
|
else if (pasteState.equals(PasteState.DONE)) {
|
|
// All done. Cancel task
|
|
cancelTask(result);
|
|
} else if (pasteState.equals(PasteState.CANCEL)) {
|
|
// This state makes sure the follow-on task only ever runs once
|
|
pastingTask.cancel();
|
|
result.complete(true);
|
|
}
|
|
}
|
|
|
|
private void cancelTask(CompletableFuture<Boolean> result) {
|
|
// Set pos1 and 2 if this was a clipboard paste
|
|
if (island == null && clipboard != null) {
|
|
clipboard.setPos1(pos1);
|
|
clipboard.setPos2(pos2);
|
|
}
|
|
pasteState = PasteState.CANCEL;
|
|
result.complete(true);
|
|
}
|
|
|
|
private void pasteEntities(Bits bits, int count, Optional<User> owner, int pasteSpeed, boolean useNMS) {
|
|
if (bits.it3().hasNext()) {
|
|
Map<Location, List<BlueprintEntity>> entityMap = new HashMap<>();
|
|
// Paste entities
|
|
while (count < pasteSpeed) {
|
|
if (!bits.it3().hasNext()) {
|
|
break;
|
|
}
|
|
Entry<Vector, List<BlueprintEntity>> entry = bits.it3().next();
|
|
int x = location.getBlockX() + entry.getKey().getBlockX();
|
|
int y = location.getBlockY() + entry.getKey().getBlockY();
|
|
int z = location.getBlockZ() + entry.getKey().getBlockZ();
|
|
Location center = new Location(world, x, y, z).add(new Vector(0.5, 0.5, 0.5));
|
|
List<BlueprintEntity> entities = entry.getValue();
|
|
entityMap.put(center, entities);
|
|
count++;
|
|
}
|
|
if (!entityMap.isEmpty()) {
|
|
currentTask = useNMS ? paster.pasteEntities(island, world, entityMap)
|
|
: fallback.pasteEntities(island, world, entityMap);
|
|
}
|
|
} else {
|
|
pasteState = PasteState.DONE;
|
|
|
|
String dimensionType = switch (location.getWorld().getEnvironment()) {
|
|
case NETHER -> owner.map(user -> user.getTranslation("general.worlds.nether")).orElse("");
|
|
case THE_END -> owner.map(user -> user.getTranslation("general.worlds.the-end")).orElse("");
|
|
default -> owner.map(user -> user.getTranslation("general.worlds.overworld")).orElse("");
|
|
};
|
|
|
|
owner.ifPresent(user -> user.sendMessage("commands.island.create.pasting.dimension-done", "[world]", dimensionType));
|
|
}
|
|
|
|
}
|
|
|
|
private void pasteBlocks(Bits bits, int count, Optional<User> owner, int pasteSpeed, boolean useNMS) {
|
|
Iterator<Entry<Vector, BlueprintBlock>> it = pasteState.equals(PasteState.BLOCKS) ? bits.it : bits.it2;
|
|
if (it.hasNext()) {
|
|
pasteBlocksNow(it, count, pasteSpeed, useNMS);
|
|
} else {
|
|
if (pasteState.equals(PasteState.BLOCKS)) {
|
|
// Blocks done
|
|
// Next paste attachments
|
|
pasteState = PasteState.ATTACHMENTS;
|
|
} else {
|
|
// Attachments done. Next paste entities
|
|
pasteState = PasteState.ENTITIES;
|
|
if (bits.entities.size() != 0) {
|
|
owner.ifPresent(user -> user.sendMessage("commands.island.create.pasting.entities", TextVariables.NUMBER, String.valueOf(bits.entities.size())));
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private void pasteBlocksNow(Iterator<Entry<Vector, BlueprintBlock>> it, int count, int pasteSpeed, boolean useNMS) {
|
|
Map<Location, BlueprintBlock> blockMap = new HashMap<>();
|
|
// Paste blocks
|
|
while (count < pasteSpeed) {
|
|
if (!it.hasNext()) {
|
|
break;
|
|
}
|
|
Entry<Vector, BlueprintBlock> entry = it.next();
|
|
Location pasteTo = location.clone().add(entry.getKey());
|
|
// pos1 and pos2 update
|
|
updatePos(pasteTo);
|
|
|
|
BlueprintBlock block = entry.getValue();
|
|
blockMap.put(pasteTo, block);
|
|
count++;
|
|
}
|
|
if (!blockMap.isEmpty()) {
|
|
currentTask = useNMS ? paster.pasteBlocks(island, world, blockMap)
|
|
: fallback.pasteBlocks(island, world, blockMap);
|
|
}
|
|
|
|
}
|
|
|
|
private void loadChunk() {
|
|
long timer = System.currentTimeMillis();
|
|
pasteState = PasteState.CHUNK_LOADING;
|
|
// Load chunk
|
|
currentTask = Util.getChunkAtAsync(location).thenRun(() -> {
|
|
pasteState = PasteState.BLOCKS;
|
|
long duration = System.currentTimeMillis() - timer;
|
|
if (duration > chunkLoadTime) {
|
|
chunkLoadTime = duration;
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
private void tellOwner(User user, int blocksSize, int attachedSize, int entitiesSize, int pasteSpeed) {
|
|
// Estimated time:
|
|
double total = (double) blocksSize + attachedSize + entitiesSize;
|
|
BigDecimal time = BigDecimal.valueOf(total / (pasteSpeed * 20.0D) + (chunkLoadTime / 1000.0D)).setScale(1, RoundingMode.UP);
|
|
user.sendMessage("commands.island.create.pasting.estimated-time", TextVariables.NUMBER, String.valueOf(time.doubleValue()));
|
|
// We're pasting blocks!
|
|
user.sendMessage("commands.island.create.pasting.blocks", TextVariables.NUMBER, String.valueOf(blocksSize + attachedSize));
|
|
}
|
|
|
|
/**
|
|
* Tracks the minimum and maximum block positions
|
|
* @param l - location of block pasted
|
|
*/
|
|
private void updatePos(Location l) {
|
|
if (pos1 == null) {
|
|
pos1 = l.clone();
|
|
}
|
|
if (pos2 == null) {
|
|
pos2 = l.clone();
|
|
}
|
|
if (l.getBlockX() < pos1.getBlockX()) {
|
|
pos1.setX(l.getBlockX());
|
|
}
|
|
if (l.getBlockX() > pos2.getBlockX()) {
|
|
pos2.setX(l.getBlockX());
|
|
}
|
|
if (l.getBlockY() < pos1.getBlockY()) {
|
|
pos1.setY(l.getBlockY());
|
|
}
|
|
if (l.getBlockY() > pos2.getBlockY()) {
|
|
pos2.setY(l.getBlockY());
|
|
}
|
|
if (l.getBlockZ() < pos1.getBlockZ()) {
|
|
pos1.setZ(l.getBlockZ());
|
|
}
|
|
if (l.getBlockZ() > pos2.getBlockZ()) {
|
|
pos2.setZ(l.getBlockZ());
|
|
}
|
|
}
|
|
}
|