diff --git a/Bukkit/build.gradle b/Bukkit/build.gradle
index 1036e1089..e206fbe5b 100644
--- a/Bukkit/build.gradle
+++ b/Bukkit/build.gradle
@@ -28,7 +28,7 @@ dependencies {
exclude(module: "bukkit")
}
- compile("io.papermc:paperlib:1.0.2")
+ compile("io.papermc:paperlib:1.0.4")
implementation("net.kyori:text-adapter-bukkit:3.0.3")
compile("com.github.MilkBowl:VaultAPI:1.7") {
exclude(module: "bukkit")
@@ -94,7 +94,7 @@ task copyFiles {
shadowJar {
dependencies {
include(dependency(":PlotSquared-Core"))
- include(dependency("io.papermc:paperlib:1.0.2"))
+ include(dependency("io.papermc:paperlib:1.0.4"))
include(dependency("net.kyori:text-adapter-bukkit:3.0.3"))
include(dependency("org.bstats:bstats-bukkit:1.7"))
include(dependency("org.khelekore:prtree:1.7.0-SNAPSHOT"))
diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/queue/ChunkCoordinator.java b/Bukkit/src/main/java/com/plotsquared/bukkit/queue/ChunkCoordinator.java
new file mode 100644
index 000000000..cc5c1f72f
--- /dev/null
+++ b/Bukkit/src/main/java/com/plotsquared/bukkit/queue/ChunkCoordinator.java
@@ -0,0 +1,326 @@
+/*
+ * _____ _ _ _____ _
+ * | __ \| | | | / ____| | |
+ * | |__) | | ___ | |_| (___ __ _ _ _ __ _ _ __ ___ __| |
+ * | ___/| |/ _ \| __|\___ \ / _` | | | |/ _` | '__/ _ \/ _` |
+ * | | | | (_) | |_ ____) | (_| | |_| | (_| | | | __/ (_| |
+ * |_| |_|\___/ \__|_____/ \__, |\__,_|\__,_|_| \___|\__,_|
+ * | |
+ * |_|
+ * PlotSquared plot management system for Minecraft
+ * Copyright (C) 2020 IntellectualSites
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.plotsquared.bukkit.queue;
+
+import com.google.common.base.Preconditions;
+import com.plotsquared.bukkit.BukkitMain;
+import com.sk89q.worldedit.math.BlockVector2;
+import io.papermc.lib.PaperLib;
+import org.bukkit.Chunk;
+import org.bukkit.World;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.scheduler.BukkitRunnable;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+/**
+ * Utility that allows for the loading and coordination of chunk actions
+ *
+ * The coordinator takes in collection of chunk coordinates, loads them
+ * and allows the caller to specify a sink for the loaded chunks. The
+ * coordinator will prevent the chunks from being unloaded until the sink
+ * has fully consumed the chunk
+ *
+ * Usage:
+ *
{@code
+ * final ChunkCoordinator chunkCoordinator = ChunkCoordinator.builder()
+ * .inWorld(Objects.requireNonNull(Bukkit.getWorld("world"))).withChunk(BlockVector2.at(0, 0))
+ * .withConsumer(chunk -> System.out.printf("Got chunk %d;%d", chunk.getX(), chunk.getZ()))
+ * .withFinalAction(() -> System.out.println("All chunks have been loaded"))
+ * .withThrowableConsumer(throwable -> System.err.println("Something went wrong... =("))
+ * .withMaxIterationTime(25L)
+ * .build();
+ * chunkCoordinator.subscribeToProgress((coordinator, progress) ->
+ * System.out.printf("Progress: %.1f", progress * 100.0f));
+ * chunkCoordinator.start();
+ * }
+ *
+ * @author Alexander Söderberg
+ * @see #builder() To create a new coordinator instance
+ */
+public final class ChunkCoordinator extends BukkitRunnable {
+
+ private final List progressSubscribers = new LinkedList<>();
+
+ private final Queue requestedChunks;
+ private final Queue availableChunks;
+ private final long maxIterationTime;
+ private final Plugin plugin;
+ private final Consumer chunkConsumer;
+ private final World world;
+ private final Runnable whenDone;
+ private final Consumer throwableConsumer;
+ private final int totalSize;
+
+ private AtomicInteger expectedSize;
+ private int batchSize;
+
+ private ChunkCoordinator(final long maxIterationTime, final int initialBatchSize,
+ @NotNull final Consumer chunkConsumer, @NotNull final World world,
+ @NotNull final Collection requestedChunks, @NotNull final Runnable whenDone,
+ @NotNull final Consumer throwableConsumer) {
+ this.requestedChunks = new LinkedBlockingQueue<>(requestedChunks);
+ this.availableChunks = new LinkedBlockingQueue<>();
+ this.totalSize = requestedChunks.size();
+ this.expectedSize = new AtomicInteger(this.totalSize);
+ this.world = world;
+ this.batchSize = initialBatchSize;
+ this.chunkConsumer = chunkConsumer;
+ this.maxIterationTime = maxIterationTime;
+ this.whenDone = whenDone;
+ this.throwableConsumer = throwableConsumer;
+ this.plugin = JavaPlugin.getPlugin(BukkitMain.class);
+ }
+
+ /**
+ * Create a new {@link ChunkCoordinator} instance
+ *
+ * @return Coordinator builder instance
+ */
+ @NotNull public static ChunkCoordinatorBuilder builder() {
+ return new ChunkCoordinatorBuilder();
+ }
+
+ /**
+ * Start the coordinator instance
+ */
+ public void start() {
+ // Request initial batch
+ this.requestBatch();
+ // Wait until next tick to give the chunks a chance to be loaded
+ this.runTaskTimer(this.plugin, 1L, 1L);
+ }
+
+ @Override public void run() {
+ Chunk chunk = this.availableChunks.poll();
+ if (chunk == null) {
+ return;
+ }
+ long iterationTime;
+ int processedChunks = 0;
+ do {
+ final long start = System.currentTimeMillis();
+ try {
+ this.chunkConsumer.accept(chunk);
+ } catch (final Throwable throwable) {
+ this.throwableConsumer.accept(throwable);
+ }
+ this.freeChunk(chunk);
+ processedChunks++;
+ final long end = System.currentTimeMillis();
+ // Update iteration time
+ iterationTime = end - start;
+ } while (2 * iterationTime /* last chunk + next chunk */ < this.maxIterationTime
+ && (chunk = availableChunks.poll()) != null);
+ if (processedChunks < this.batchSize) {
+ // Adjust batch size based on the amount of processed chunks per tick
+ this.batchSize = processedChunks;
+ }
+
+ final int expected = this.expectedSize.addAndGet(-processedChunks);
+
+ final float progress = ((float) totalSize - (float) expected) / (float) totalSize;
+ for (final ProgressSubscriber subscriber : this.progressSubscribers) {
+ subscriber.notifyProgress(this, progress);
+ }
+
+ if (expected <= 0) {
+ try {
+ this.whenDone.run();
+ } catch (final Throwable throwable) {
+ this.throwableConsumer.accept(throwable);
+ }
+ this.cancel();
+ } else {
+ if (this.availableChunks.size() < processedChunks) {
+ this.requestBatch();
+ }
+ }
+ }
+
+ private void requestBatch() {
+ BlockVector2 chunk;
+ for (int i = 0; i < this.batchSize && (chunk = this.requestedChunks.poll()) != null; i++) {
+ // This required PaperLib to be bumped to version 1.0.4 to mark the request as urgent
+ PaperLib.getChunkAtAsync(this.world, chunk.getX(), chunk.getZ(), true, true)
+ .whenComplete((chunkObject, throwable) -> {
+ if (throwable != null) {
+ throwable.printStackTrace();
+ // We want one less because this couldn't be processed
+ this.expectedSize.decrementAndGet();
+ } else {
+ this.processChunk(chunkObject);
+ }
+ });
+ }
+ }
+
+ private void processChunk(@NotNull final Chunk chunk) {
+ if (!chunk.isLoaded()) {
+ throw new IllegalArgumentException(
+ String.format("Chunk %d;%d is is not loaded", chunk.getX(), chunk.getZ()));
+ }
+ chunk.addPluginChunkTicket(this.plugin);
+ this.availableChunks.add(chunk);
+ }
+
+ private void freeChunk(@NotNull final Chunk chunk) {
+ if (!chunk.isLoaded()) {
+ throw new IllegalArgumentException(
+ String.format("Chunk %d;%d is is not loaded", chunk.getX(), chunk.getZ()));
+ }
+ chunk.removePluginChunkTicket(this.plugin);
+ }
+
+ /**
+ * Get the amount of remaining chunks (at the time of the method call)
+ *
+ * @return Snapshot view of remaining chunk count
+ */
+ public int getRemainingChunks() {
+ return this.expectedSize.get();
+ }
+
+ /**
+ * Get the amount of requested chunks
+ *
+ * @return Requested chunk count
+ */
+ public int getTotalChunks() {
+ return this.totalSize;
+ }
+
+ /**
+ * Subscribe to coordinator progress updates
+ *
+ * @param subscriber Subscriber
+ */
+ public void subscribeToProgress(@NotNull final ChunkCoordinator.ProgressSubscriber subscriber) {
+ this.progressSubscribers.add(subscriber);
+ }
+
+
+ @FunctionalInterface
+ public interface ProgressSubscriber {
+
+ /**
+ * Notify about a progress update in the coordinator
+ *
+ * @param coordinator Coordinator instance that triggered the notification
+ * @param progress Progress in the range [0, 1]
+ */
+ void notifyProgress(@NotNull final ChunkCoordinator coordinator, final float progress);
+
+ }
+
+
+ public static final class ChunkCoordinatorBuilder {
+
+ private final List requestedChunks = new LinkedList<>();
+ private Consumer throwableConsumer = Throwable::printStackTrace;
+ private World world;
+ private Consumer chunkConsumer;
+ private Runnable whenDone = () -> {
+ };
+ private long maxIterationTime = 60; // A little over 1 tick;
+ private int initialBatchSize = 4;
+
+ private ChunkCoordinatorBuilder() {
+ }
+
+ @NotNull public ChunkCoordinatorBuilder inWorld(@NotNull final World world) {
+ this.world = Preconditions.checkNotNull(world, "World may not be null");
+ return this;
+ }
+
+ @NotNull
+ public ChunkCoordinatorBuilder withChunk(@NotNull final BlockVector2 chunkLocation) {
+ this.requestedChunks
+ .add(Preconditions.checkNotNull(chunkLocation, "Chunk location may not be null"));
+ return this;
+ }
+
+ @NotNull public ChunkCoordinatorBuilder withChunks(
+ @NotNull final Collection chunkLocations) {
+ chunkLocations.forEach(this::withChunk);
+ return this;
+ }
+
+ @NotNull
+ public ChunkCoordinatorBuilder withConsumer(@NotNull final Consumer chunkConsumer) {
+ this.chunkConsumer =
+ Preconditions.checkNotNull(chunkConsumer, "Chunk consumer may not be null");
+ return this;
+ }
+
+ @NotNull public ChunkCoordinatorBuilder withFinalAction(@NotNull final Runnable whenDone) {
+ this.whenDone = Preconditions.checkNotNull(whenDone, "Final action may not be null");
+ return this;
+ }
+
+ @NotNull public ChunkCoordinatorBuilder withMaxIterationTime(final long maxIterationTime) {
+ Preconditions
+ .checkArgument(maxIterationTime > 0, "Max iteration time must be positive");
+ this.maxIterationTime = maxIterationTime;
+ return this;
+ }
+
+ @NotNull public ChunkCoordinatorBuilder withInitialBatchSize(final int initialBatchSize) {
+ Preconditions
+ .checkArgument(initialBatchSize > 0, "Initial batch size must be positive");
+ this.initialBatchSize = initialBatchSize;
+ return this;
+ }
+
+ @NotNull public ChunkCoordinatorBuilder withThrowableConsumer(
+ @NotNull final Consumer throwableConsumer) {
+ this.throwableConsumer =
+ Preconditions.checkNotNull(throwableConsumer, "Throwable consumer may not be null");
+ return this;
+ }
+
+ @NotNull public ChunkCoordinator build() {
+ Preconditions.checkNotNull(this.world, "No world was supplied");
+ Preconditions.checkNotNull(this.chunkConsumer, "No chunk consumer was supplied");
+ Preconditions.checkNotNull(this.whenDone, "No final action was supplied");
+ Preconditions
+ .checkNotNull(this.throwableConsumer, "No throwable consumer was supplied");
+ return new ChunkCoordinator(this.maxIterationTime, this.initialBatchSize,
+ this.chunkConsumer, this.world, this.requestedChunks, this.whenDone,
+ this.throwableConsumer);
+ }
+
+ }
+
+}