refactor in a way to provide possibility of querying progress of running operation

This commit is contained in:
2024-12-03 18:02:54 +01:00
parent 9e7a4d1ecf
commit ec9742690f
4 changed files with 137 additions and 48 deletions
@@ -7,7 +7,7 @@ import me.youhavetrouble.blockedit.util.ChunkWork;
import me.youhavetrouble.blockedit.util.Selection; import me.youhavetrouble.blockedit.util.Selection;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.HashSet; import java.util.Set;
public class BlockEditAPI { public class BlockEditAPI {
@@ -17,13 +17,13 @@ public class BlockEditAPI {
* @param chunksPerTick Amount of chunks per tick to modify * @param chunksPerTick Amount of chunks per tick to modify
* @param operation Operation to execute * @param operation Operation to execute
*/ */
public static void runOperation( public static OperationWork runOperation(
@NotNull Selection selection, @NotNull Selection selection,
int chunksPerTick, int chunksPerTick,
@NotNull BlockEditOperation operation @NotNull BlockEditOperation operation
) { ) {
HashSet<ChunkWork> work = WorkSplitter.getOperatedOnChunks(selection); Set<ChunkWork> work = WorkSplitter.getOperatedOnChunks(selection);
WorkSplitter.runOperation(work, selection, chunksPerTick, operation); return WorkSplitter.runOperation(work, selection, chunksPerTick, operation);
} }
/** /**
@@ -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<ChunkWork> 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<ChunkWork> 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);
}
}
@@ -1,21 +1,16 @@
package me.youhavetrouble.blockedit.api; package me.youhavetrouble.blockedit.api;
import me.youhavetrouble.blockedit.BlockEdit;
import me.youhavetrouble.blockedit.util.ChunkWork; import me.youhavetrouble.blockedit.util.ChunkWork;
import me.youhavetrouble.blockedit.util.Selection; 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.bukkit.util.BoundingBox;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
public class WorkSplitter { public class WorkSplitter {
protected static HashSet<ChunkWork> getOperatedOnChunks(BoundingBox boundingBox) { protected static Set<ChunkWork> getOperatedOnChunks(BoundingBox boundingBox) {
HashSet<ChunkWork> chunks = new HashSet<>(); HashSet<ChunkWork> chunks = new HashSet<>();
int minChunkX = (int) Math.floor(boundingBox.getMinX()) >> 4; int minChunkX = (int) Math.floor(boundingBox.getMinX()) >> 4;
int maxChunkX = (int) Math.floor(boundingBox.getMaxX()) >> 4; int maxChunkX = (int) Math.floor(boundingBox.getMaxX()) >> 4;
@@ -32,42 +27,10 @@ public class WorkSplitter {
return chunks; return chunks;
} }
protected static void runOperation(HashSet<ChunkWork> chunkWorks, Selection selection, int chunksPerTick, BlockEditOperation operation) { protected static OperationWork runOperation(@NotNull Set<ChunkWork> chunkWorks, @NotNull Selection selection, int chunksPerTick, @NotNull BlockEditOperation operation) {
if (selection == null) return; OperationWork operationWork = new OperationWork(chunkWorks, selection, chunksPerTick, operation);
Selection sel = new Selection(selection.clone().expand(0.1), selection.getWorldUuid()); operationWork.start();
List<ChunkWork> chunkWorkList = new ArrayList<>(chunkWorks); return operationWork;
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);
}
}
}
});
} }
} }
@@ -6,6 +6,7 @@ import org.bukkit.World;
import org.bukkit.util.BoundingBox; import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@@ -27,18 +28,22 @@ public class Selection extends BoundingBox {
this.worldUuid = worldUuid; this.worldUuid = worldUuid;
} }
@Nullable
public Location getSelectionPoint1() { public Location getSelectionPoint1() {
return selectionPoint1; return selectionPoint1;
} }
@Nullable
public Location getSelectionPoint2() { public Location getSelectionPoint2() {
return selectionPoint2; return selectionPoint2;
} }
@Nullable
public World getWorld() { public World getWorld() {
return Bukkit.getWorld(worldUuid); return Bukkit.getWorld(worldUuid);
} }
@NotNull
public UUID getWorldUuid() { public UUID getWorldUuid() {
return worldUuid; return worldUuid;
} }