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); + } + + } + +}