From ec9742690faec0a92c97e707cb8e7c333e0b0732 Mon Sep 17 00:00:00 2001 From: YouHaveTrouble Date: Tue, 3 Dec 2024 18:02:54 +0100 Subject: [PATCH] refactor in a way to provide possibility of querying progress of running operation --- .../blockedit/api/BlockEditAPI.java | 8 +- .../blockedit/api/OperationWork.java | 121 ++++++++++++++++++ .../blockedit/api/WorkSplitter.java | 51 +------- .../blockedit/util/Selection.java | 5 + 4 files changed, 137 insertions(+), 48 deletions(-) create mode 100644 src/main/java/me/youhavetrouble/blockedit/api/OperationWork.java diff --git a/src/main/java/me/youhavetrouble/blockedit/api/BlockEditAPI.java b/src/main/java/me/youhavetrouble/blockedit/api/BlockEditAPI.java index f7c060c..1ec01d6 100644 --- a/src/main/java/me/youhavetrouble/blockedit/api/BlockEditAPI.java +++ b/src/main/java/me/youhavetrouble/blockedit/api/BlockEditAPI.java @@ -7,7 +7,7 @@ import me.youhavetrouble.blockedit.util.ChunkWork; import me.youhavetrouble.blockedit.util.Selection; import org.jetbrains.annotations.NotNull; -import java.util.HashSet; +import java.util.Set; public class BlockEditAPI { @@ -17,13 +17,13 @@ public class BlockEditAPI { * @param chunksPerTick Amount of chunks per tick to modify * @param operation Operation to execute */ - public static void runOperation( + public static OperationWork runOperation( @NotNull Selection selection, int chunksPerTick, @NotNull BlockEditOperation operation ) { - HashSet work = WorkSplitter.getOperatedOnChunks(selection); - WorkSplitter.runOperation(work, selection, chunksPerTick, operation); + Set work = WorkSplitter.getOperatedOnChunks(selection); + return WorkSplitter.runOperation(work, selection, chunksPerTick, operation); } /** diff --git a/src/main/java/me/youhavetrouble/blockedit/api/OperationWork.java b/src/main/java/me/youhavetrouble/blockedit/api/OperationWork.java new file mode 100644 index 0000000..aecc465 --- /dev/null +++ b/src/main/java/me/youhavetrouble/blockedit/api/OperationWork.java @@ -0,0 +1,121 @@ +package me.youhavetrouble.blockedit.api; + +import me.youhavetrouble.blockedit.BlockEdit; +import me.youhavetrouble.blockedit.util.ChunkWork; +import me.youhavetrouble.blockedit.util.Selection; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.block.Block; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +public class OperationWork { + + private boolean started, finished = false; + private final List chunkWorkList = new ArrayList<>(); + private final Selection selection; + private final int chunksPerTick; + private final BlockEditOperation operation; + private final AtomicInteger chunksLeft; + private final AtomicInteger chunksProcessed = new AtomicInteger(0); + + protected OperationWork( + Set chunkWorks, + Selection selection, + int chunksPerTick, + BlockEditOperation operation + ) { + this.chunkWorkList.addAll(chunkWorks); + this.chunksLeft = new AtomicInteger(this.chunkWorkList.size() - 1); + this.selection = new Selection(selection.clone().expand(0.1), selection.getWorldUuid()); + this.chunksPerTick = chunksPerTick; + this.operation = operation; + } + + /** + * Starts the operation. This will start processing chunks in the background. + * This method can only be called once. + * + * @throws IllegalStateException if called when the operation has already been started + */ + protected void start() { + if (started) throw new IllegalStateException("Operation already started"); + started = true; + Bukkit.getGlobalRegionScheduler().runAtFixedRate(BlockEdit.getPlugin(), (task -> { + if (chunksLeft.get() < 0) { + this.finished = true; + task.cancel(); + return; + } + for (int i = 0; i < chunksPerTick; i++) { + int chunkWorkIndex = chunksLeft.getAndDecrement(); + ChunkWork chunkWork = chunkWorkList.get(chunkWorkIndex); + World world = selection.getWorld(); + if (world == null) return; + Bukkit.getRegionScheduler().execute(BlockEdit.getPlugin(), selection.getWorld(), chunkWork.getX(), chunkWork.getZ(), () -> { + processChunkWork(chunkWork, selection, operation); + }); + } + }), 1, 1); + } + + /** + * Returns the total amount of chunks that will be processed. + * + * @return Total amount of chunks + */ + public int getTotalChunks() { + return chunkWorkList.size(); + } + + /** + * Returns the amount of chunks processed. This might not match total amount of chunks after operation finishes. + * + * @return Amount of chunks processed + * @see #finished to check if the operation has finished + */ + public int getChunksProcessed() { + return chunksProcessed.get(); + } + + /** + * Returns the amount of chunks left to process. + * + * @return Amount of chunks left to process + */ + public int getChunksLeft() { + return chunksLeft.get(); + } + + /** + * Returns whether the operation has finished. This will return true after all work tasks have been delegated. + * + * @return Whether the operation has finished + * @see #getChunksLeft() to get the amount of chunks left to process + * @see #getChunksProcessed() to get the amount of chunks processed + */ + public boolean isFinished() { + return finished; + } + + private void processChunkWork(ChunkWork chunkWork, Selection selection, BlockEditOperation operation) { + World world = selection.getWorld(); + if (world == null) return; + chunkWork.getChunkAsync(world).thenAccept(chunk -> { + // skip y levels that are not in the selection + for (int y = (int) selection.getMinY(); y <= selection.getMaxY(); y++) { + for (int x = 0; x <= 15; x++) { + for (int z = 0; z <= 15; z++) { + Block block = chunk.getBlock(x, y, z); + if (!selection.contains(block.getLocation().toVector())) continue; + operation.transformBlock(block); + } + } + } + }).thenRunAsync(this.chunksProcessed::incrementAndGet); + } + +} diff --git a/src/main/java/me/youhavetrouble/blockedit/api/WorkSplitter.java b/src/main/java/me/youhavetrouble/blockedit/api/WorkSplitter.java index 26e6aa1..7355e37 100644 --- a/src/main/java/me/youhavetrouble/blockedit/api/WorkSplitter.java +++ b/src/main/java/me/youhavetrouble/blockedit/api/WorkSplitter.java @@ -1,21 +1,16 @@ package me.youhavetrouble.blockedit.api; -import me.youhavetrouble.blockedit.BlockEdit; import me.youhavetrouble.blockedit.util.ChunkWork; import me.youhavetrouble.blockedit.util.Selection; -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.block.Block; import org.bukkit.util.BoundingBox; +import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; import java.util.HashSet; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.Set; public class WorkSplitter { - protected static HashSet getOperatedOnChunks(BoundingBox boundingBox) { + protected static Set getOperatedOnChunks(BoundingBox boundingBox) { HashSet chunks = new HashSet<>(); int minChunkX = (int) Math.floor(boundingBox.getMinX()) >> 4; int maxChunkX = (int) Math.floor(boundingBox.getMaxX()) >> 4; @@ -32,42 +27,10 @@ public class WorkSplitter { return chunks; } - protected static void runOperation(HashSet chunkWorks, Selection selection, int chunksPerTick, BlockEditOperation operation) { - if (selection == null) return; - Selection sel = new Selection(selection.clone().expand(0.1), selection.getWorldUuid()); - List chunkWorkList = new ArrayList<>(chunkWorks); - AtomicInteger element = new AtomicInteger(chunkWorkList.size()-1); - Bukkit.getGlobalRegionScheduler().runAtFixedRate(BlockEdit.getPlugin(), (task -> { - if (element.get() < 0) { - task.cancel(); - return; - } - for (int i = 0; i< chunksPerTick; i++) { - int chunkWorkIndex = element.getAndDecrement(); - ChunkWork chunkWork = chunkWorkList.get(chunkWorkIndex); - Bukkit.getRegionScheduler().execute(BlockEdit.getPlugin(), selection.getWorld(), chunkWork.getX(), chunkWork.getZ(), () -> { - processChunkWork(chunkWork, sel, operation); - }); - } - }), 1, 1); - - } - - private static void processChunkWork(ChunkWork chunkWork, Selection selection, BlockEditOperation operation) { - World world = selection.getWorld(); - if (world == null) return; - chunkWork.getChunkAsync(world).thenAccept(chunk -> { - // skip y levels that are not in the selection - for (int y = (int) selection.getMinY(); y <= selection.getMaxY(); y++) { - for (int x = 0; x <= 15; x++) { - for (int z = 0; z <= 15; z++) { - Block block = chunk.getBlock(x, y, z); - if (!selection.contains(block.getLocation().toVector())) continue; - operation.transformBlock(block); - } - } - } - }); + protected static OperationWork runOperation(@NotNull Set chunkWorks, @NotNull Selection selection, int chunksPerTick, @NotNull BlockEditOperation operation) { + OperationWork operationWork = new OperationWork(chunkWorks, selection, chunksPerTick, operation); + operationWork.start(); + return operationWork; } } diff --git a/src/main/java/me/youhavetrouble/blockedit/util/Selection.java b/src/main/java/me/youhavetrouble/blockedit/util/Selection.java index 2bb6b0d..3e55fd8 100644 --- a/src/main/java/me/youhavetrouble/blockedit/util/Selection.java +++ b/src/main/java/me/youhavetrouble/blockedit/util/Selection.java @@ -6,6 +6,7 @@ import org.bukkit.World; import org.bukkit.util.BoundingBox; import org.bukkit.util.Vector; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Set; import java.util.UUID; @@ -27,18 +28,22 @@ public class Selection extends BoundingBox { this.worldUuid = worldUuid; } + @Nullable public Location getSelectionPoint1() { return selectionPoint1; } + @Nullable public Location getSelectionPoint2() { return selectionPoint2; } + @Nullable public World getWorld() { return Bukkit.getWorld(worldUuid); } + @NotNull public UUID getWorldUuid() { return worldUuid; }