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}