001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.bukkit.queue;
020
021import com.google.inject.Inject;
022import com.google.inject.assistedinject.Assisted;
023import com.plotsquared.bukkit.BukkitPlatform;
024import com.plotsquared.core.PlotSquared;
025import com.plotsquared.core.queue.ChunkCoordinator;
026import com.plotsquared.core.queue.subscriber.ProgressSubscriber;
027import com.plotsquared.core.util.task.PlotSquaredTask;
028import com.plotsquared.core.util.task.TaskManager;
029import com.plotsquared.core.util.task.TaskTime;
030import com.sk89q.worldedit.math.BlockVector2;
031import com.sk89q.worldedit.world.World;
032import io.papermc.lib.PaperLib;
033import org.bukkit.Bukkit;
034import org.bukkit.Chunk;
035import org.bukkit.plugin.Plugin;
036import org.bukkit.plugin.java.JavaPlugin;
037import org.checkerframework.checker.nullness.qual.NonNull;
038
039import java.util.Collection;
040import java.util.LinkedList;
041import java.util.List;
042import java.util.Queue;
043import java.util.concurrent.LinkedBlockingQueue;
044import java.util.concurrent.atomic.AtomicInteger;
045import java.util.function.Consumer;
046
047/**
048 * Utility that allows for the loading and coordination of chunk actions
049 * <p>
050 * The coordinator takes in collection of chunk coordinates, loads them
051 * and allows the caller to specify a sink for the loaded chunks. The
052 * coordinator will prevent the chunks from being unloaded until the sink
053 * has fully consumed the chunk
054 * </p>
055 **/
056public final class BukkitChunkCoordinator extends ChunkCoordinator {
057
058    private final List<ProgressSubscriber> progressSubscribers = new LinkedList<>();
059
060    private final Queue<BlockVector2> requestedChunks;
061    private final Queue<Chunk> availableChunks;
062    private final long maxIterationTime;
063    private final Plugin plugin;
064    private final Consumer<BlockVector2> chunkConsumer;
065    private final org.bukkit.World bukkitWorld;
066    private final Runnable whenDone;
067    private final Consumer<Throwable> throwableConsumer;
068    private final boolean unloadAfter;
069    private final int totalSize;
070    private final AtomicInteger expectedSize;
071    private final AtomicInteger loadingChunks = new AtomicInteger();
072    private final boolean forceSync;
073
074    private int batchSize;
075    private PlotSquaredTask task;
076    private volatile boolean shouldCancel;
077    private boolean finished;
078
079    @Inject
080    private BukkitChunkCoordinator(
081            @Assisted final long maxIterationTime,
082            @Assisted final int initialBatchSize,
083            @Assisted final @NonNull Consumer<BlockVector2> chunkConsumer,
084            @Assisted final @NonNull World world,
085            @Assisted final @NonNull Collection<BlockVector2> requestedChunks,
086            @Assisted final @NonNull Runnable whenDone,
087            @Assisted final @NonNull Consumer<Throwable> throwableConsumer,
088            @Assisted("unloadAfter") final boolean unloadAfter,
089            @Assisted final @NonNull Collection<ProgressSubscriber> progressSubscribers,
090            @Assisted("forceSync") final boolean forceSync
091    ) {
092        this.requestedChunks = new LinkedBlockingQueue<>(requestedChunks);
093        this.availableChunks = new LinkedBlockingQueue<>();
094        this.totalSize = requestedChunks.size();
095        this.expectedSize = new AtomicInteger(this.totalSize);
096        this.batchSize = initialBatchSize;
097        this.chunkConsumer = chunkConsumer;
098        this.maxIterationTime = maxIterationTime;
099        this.whenDone = whenDone;
100        this.throwableConsumer = throwableConsumer;
101        this.unloadAfter = unloadAfter;
102        this.plugin = JavaPlugin.getPlugin(BukkitPlatform.class);
103        this.bukkitWorld = Bukkit.getWorld(world.getName());
104        this.progressSubscribers.addAll(progressSubscribers);
105        this.forceSync = forceSync;
106    }
107
108    @Override
109    public void start() {
110        if (!forceSync) {
111            // Request initial batch
112            this.requestBatch();
113            // Wait until next tick to give the chunks a chance to be loaded
114            TaskManager.runTaskLater(() -> task = TaskManager.runTaskRepeat(this, TaskTime.ticks(1)), TaskTime.ticks(1));
115        } else {
116            try {
117                while (!shouldCancel && !requestedChunks.isEmpty()) {
118                    chunkConsumer.accept(requestedChunks.poll());
119                }
120            } catch (Throwable t) {
121                throwableConsumer.accept(t);
122            } finally {
123                finish();
124            }
125        }
126    }
127
128    @Override
129    public void cancel() {
130        shouldCancel = true;
131    }
132
133    private void finish() {
134        try {
135            this.whenDone.run();
136        } catch (final Throwable throwable) {
137            this.throwableConsumer.accept(throwable);
138        } finally {
139            for (final ProgressSubscriber subscriber : this.progressSubscribers) {
140                subscriber.notifyEnd();
141            }
142            if (task != null) {
143                task.cancel();
144            }
145            finished = true;
146        }
147    }
148
149    @Override
150    public void run() {
151        if (shouldCancel) {
152            if (unloadAfter) {
153                Chunk chunk;
154                while ((chunk = availableChunks.poll()) != null) {
155                    freeChunk(chunk);
156                }
157            }
158            finish();
159            return;
160        }
161
162        Chunk chunk = this.availableChunks.poll();
163        if (chunk == null) {
164            if (this.availableChunks.isEmpty()) {
165                if (this.requestedChunks.isEmpty() && loadingChunks.get() == 0) {
166                    finish();
167                } else {
168                    requestBatch();
169                }
170            }
171            return;
172        }
173        long[] iterationTime = new long[2];
174        int processedChunks = 0;
175        do {
176            final long start = System.currentTimeMillis();
177            try {
178                this.chunkConsumer.accept(BlockVector2.at(chunk.getX(), chunk.getZ()));
179            } catch (final Throwable throwable) {
180                this.throwableConsumer.accept(throwable);
181            }
182            if (unloadAfter) {
183                this.freeChunk(chunk);
184            }
185            processedChunks++;
186            final long end = System.currentTimeMillis();
187            // Update iteration time
188            iterationTime[0] = iterationTime[1];
189            iterationTime[1] = end - start;
190        } while (iterationTime[0] + iterationTime[1] < this.maxIterationTime * 2 && (chunk = availableChunks.poll()) != null);
191        if (processedChunks < this.batchSize) {
192            // Adjust batch size based on the amount of processed chunks per tick
193            this.batchSize = processedChunks;
194        }
195
196        final int expected = this.expectedSize.addAndGet(-processedChunks);
197
198        if (expected <= 0) {
199            finish();
200        } else {
201            if (this.availableChunks.size() < processedChunks) {
202                final double progress = ((double) totalSize - (double) expected) / (double) totalSize;
203                for (final ProgressSubscriber subscriber : this.progressSubscribers) {
204                    subscriber.notifyProgress(this, progress);
205                }
206                this.requestBatch();
207            }
208        }
209    }
210
211    /**
212     * Requests a batch of chunks to be loaded
213     */
214    private void requestBatch() {
215        BlockVector2 chunk;
216        for (int i = 0; i < this.batchSize && (chunk = this.requestedChunks.poll()) != null; i++) {
217            // This required PaperLib to be bumped to version 1.0.4 to mark the request as urgent
218            loadingChunks.incrementAndGet();
219            PaperLib
220                    .getChunkAtAsync(this.bukkitWorld, chunk.getX(), chunk.getZ(), true, true)
221                    .whenComplete((chunkObject, throwable) -> {
222                        loadingChunks.decrementAndGet();
223                        if (throwable != null) {
224                            throwable.printStackTrace();
225                            // We want one less because this couldn't be processed
226                            this.expectedSize.decrementAndGet();
227                        } else if (PlotSquared.get().isMainThread(Thread.currentThread())) {
228                            this.processChunk(chunkObject);
229                        } else {
230                            TaskManager.runTask(() -> this.processChunk(chunkObject));
231                        }
232                    });
233        }
234    }
235
236    /**
237     * Once a chunk has been loaded, process it (add a plugin ticket and add to
238     * available chunks list). It is important that this gets executed on the
239     * server's main thread.
240     */
241    private void processChunk(final @NonNull Chunk chunk) {
242        /* Chunk#isLoaded does not necessarily return true shortly after PaperLib#getChunkAtAsync completes, but the chunk is
243        still loaded.
244        if (!chunk.isLoaded()) {
245            throw new IllegalArgumentException(String.format("Chunk %d;%d is is not loaded", chunk.getX(), chunk.getZ());
246        }*/
247        if (finished) {
248            return;
249        }
250        chunk.addPluginChunkTicket(this.plugin);
251        this.availableChunks.add(chunk);
252    }
253
254    /**
255     * Once a chunk has been used, free it up for unload by removing the plugin ticket
256     */
257    private void freeChunk(final @NonNull Chunk chunk) {
258        if (!chunk.isLoaded()) {
259            throw new IllegalArgumentException(String.format("Chunk %d;%d is is not loaded", chunk.getX(), chunk.getZ()));
260        }
261        chunk.removePluginChunkTicket(this.plugin);
262    }
263
264    @Override
265    public int getRemainingChunks() {
266        return this.expectedSize.get();
267    }
268
269    @Override
270    public int getTotalChunks() {
271        return this.totalSize;
272    }
273
274    /**
275     * Subscribe to coordinator progress updates
276     *
277     * @param subscriber Subscriber
278     */
279    public void subscribeToProgress(final @NonNull ProgressSubscriber subscriber) {
280        this.progressSubscribers.add(subscriber);
281    }
282
283}