diff --git a/Spigot-Server-Patches/0384-Async-Chunk-Loading-and-Generation.patch b/Spigot-Server-Patches/0384-Async-Chunk-Loading-and-Generation.patch new file mode 100644 index 0000000000..2d0c089328 --- /dev/null +++ b/Spigot-Server-Patches/0384-Async-Chunk-Loading-and-Generation.patch @@ -0,0 +1,1933 @@ +From 815ddeb3be11a9c190bf98c31bbed0ae8b861f6d Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Sat, 1 Sep 2018 12:20:09 -0400 +Subject: [PATCH] Async Chunk Loading and Generation + +This brings back parity to 1.12 and older versions in that any +chunk requested as part of the PlayerChunkMap can be loaded +asynchronously, since the chunk isn't needed "immediately". + +The previous system used by CraftBukkit has been completely abandoned, as +mojang has put more concurrency checks into the process. + +The new process is no longer lock free, but tries to maintain locks as +short as possible. + +But with 1.13, we now have Chunk Conversions too. A main issue about +keeping just loading parity to 1.12 is that standard loads now +are treated as generation level events, to run the converter on +another thread. + +However mojangs code was pretty bad here and doesn't actually provide +any concurrency... + +Mojangs code is still not thread safe, and can only operate on +one world per thread safely, but this is still a major improvement +to get world generation off of the main thread for exploration. + +This change brings Chunk Requests triggered by the Chunk Map to be +lazily loaded asynchronously. + +Standard chunk loads can load in parallel across a shared executor. + +However, chunk conversions and generations must only run one per world +at a time, so we have a single thread executor for those operations +per world, that all of those requests get scheduled to. + +getChunkAt method is now thread safe, but has not been tested in +use by other threads for generations, but should be safe to do. + +However, we are not encouraging plugins to go getting chunks async, +as while looking the chunk up may be safe, absolutely nothing about +reading or writing to the chunk will be safe, so plugins still +should not be touching chunks asynchronously! + +diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java +index fca18fbb81..ce52733840 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java ++++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java +@@ -377,4 +377,15 @@ public class PaperConfig { + } + } + } ++ ++ // Temporary opt out, will be removed later ++ public static boolean asyncChunks = false; ++ private static void disableAsyncChunks() { ++ asyncChunks = config.getBoolean("settings.async-chunks", true); ++ if (!asyncChunks) { ++ log("Async Chunks: Disabled - Chunks will be managed synchronosuly, and will cause tremendous lag."); ++ } else { ++ log("Async Chunks: Enabled - Chunks will be loaded and generated much faster, without lag."); ++ } ++ } + } +diff --git a/src/main/java/com/destroystokyo/paper/util/PriorityQueuedExecutor.java b/src/main/java/com/destroystokyo/paper/util/PriorityQueuedExecutor.java +new file mode 100644 +index 0000000000..8dfed1a8cf +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/PriorityQueuedExecutor.java +@@ -0,0 +1,277 @@ ++package com.destroystokyo.paper.util; ++ ++import com.google.common.util.concurrent.ThreadFactoryBuilder; ++import net.minecraft.server.NamedIncrementingThreadFactory; ++ ++import javax.annotation.Nonnull; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.concurrent.AbstractExecutorService; ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.ConcurrentLinkedQueue; ++import java.util.concurrent.RejectedExecutionException; ++import java.util.concurrent.ThreadFactory; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.function.Supplier; ++ ++/** ++ * Implements an Executor Service that allows specifying Task Priority ++ * and bumping of task priority. ++ * ++ * @author aikar ++ */ ++@SuppressWarnings({"WeakerAccess", "UnusedReturnValue", "unused"}) ++public class PriorityQueuedExecutor extends AbstractExecutorService { ++ private final ConcurrentLinkedQueue high = new ConcurrentLinkedQueue<>(); ++ private final ConcurrentLinkedQueue normal = new ConcurrentLinkedQueue<>(); ++ private final RejectionHandler handler; ++ private volatile boolean shuttingDown = false; ++ private volatile boolean shuttingDownNow = false; ++ private final List threads = new ArrayList<>(); ++ ++ public PriorityQueuedExecutor(String name) { ++ this(name, Runtime.getRuntime().availableProcessors(), null); ++ } ++ ++ public PriorityQueuedExecutor(String name, int threads) { ++ this(name, threads, null); ++ } ++ ++ public PriorityQueuedExecutor(String name, int threads, RejectionHandler handler) { ++ ThreadFactory factory = new ThreadFactoryBuilder() ++ .setThreadFactory(new NamedIncrementingThreadFactory(name)) ++ .setDaemon(true) ++ .build(); ++ for (int i = 0; i < threads; i++) { ++ final Thread thread = factory.newThread(this::processQueues); ++ thread.start(); ++ this.threads.add(thread); ++ } ++ if (handler == null) { ++ handler = ABORT_POLICY; ++ } ++ this.handler = handler; ++ } ++ ++ public void shutdown() { ++ shuttingDown = true; ++ synchronized (this) { ++ this.notifyAll(); ++ } ++ } ++ ++ @Nonnull ++ @Override ++ public List shutdownNow() { ++ shuttingDown = true; ++ shuttingDownNow = true; ++ List tasks = new ArrayList<>(high.size() + normal.size()); ++ Runnable run; ++ while ((run = getTask()) != null) { ++ tasks.add(run); ++ } ++ ++ return tasks; ++ } ++ ++ @Override ++ public boolean isShutdown() { ++ return shuttingDown; ++ } ++ ++ @Override ++ public boolean isTerminated() { ++ if (!shuttingDown) { ++ return false; ++ } ++ return high.isEmpty() && normal.isEmpty(); ++ } ++ ++ @Override ++ public boolean awaitTermination(long timeout, @Nonnull TimeUnit unit) { ++ synchronized (this) { ++ this.notifyAll(); ++ } ++ final long wait = unit.toNanos(timeout); ++ final long max = System.nanoTime() + wait; ++ for (;!threads.isEmpty() && System.nanoTime() < max;) { ++ threads.removeIf(thread -> !thread.isAlive()); ++ } ++ return isTerminated(); ++ } ++ ++ ++ public PendingTask createPendingTask(Runnable task) { ++ return createPendingTask(task, Priority.NORMAL); ++ } ++ public PendingTask createPendingTask(Runnable task, Priority priority) { ++ return createPendingTask(() -> { ++ task.run(); ++ return null; ++ }, priority); ++ } ++ ++ public PendingTask createPendingTask(Supplier task) { ++ return createPendingTask(task, Priority.NORMAL); ++ } ++ ++ public PendingTask createPendingTask(Supplier task, Priority priority) { ++ return new PendingTask<>(task, priority); ++ } ++ ++ public PendingTask submitTask(Runnable run) { ++ return submitTask(createPendingTask(run)); ++ } ++ ++ public PendingTask submitTask(Runnable run, Priority priority) { ++ return submitTask(createPendingTask(run, priority)); ++ } ++ ++ public PendingTask submitTask(Supplier run) { ++ return submitTask(createPendingTask(run)); ++ } ++ ++ public PendingTask submitTask(Supplier run, Priority priority) { ++ return submitTask(createPendingTask(run, priority)); ++ } ++ ++ public PendingTask submitTask(PendingTask task) { ++ if (shuttingDown) { ++ handler.onRejection(task, this); ++ return task; ++ } ++ task.submit(this); ++ return task; ++ } ++ ++ @Override ++ public void execute(@Nonnull Runnable command) { ++ submitTask(command); ++ } ++ ++ private Runnable getTask() { ++ Runnable run = high.poll(); ++ if (run != null) { ++ return run; ++ } ++ return normal.poll(); ++ } ++ ++ private void processQueues() { ++ Runnable run = null; ++ while (true) { ++ if (run != null) { ++ run.run(); ++ } ++ if (shuttingDownNow) { ++ return; ++ } ++ if ((run = getTask()) != null) { ++ continue; ++ } ++ synchronized (PriorityQueuedExecutor.this) { ++ if ((run = getTask()) != null) { ++ continue; ++ } ++ ++ if (shuttingDown || shuttingDownNow) { ++ return; ++ } ++ try { ++ PriorityQueuedExecutor.this.wait(); ++ } catch (InterruptedException ignored) { ++ } ++ } ++ } ++ } ++ ++ public enum Priority { ++ HIGH, NORMAL ++ } ++ ++ public class PendingTask implements Runnable { ++ ++ private final AtomicBoolean hasRan = new AtomicBoolean(); ++ private final AtomicInteger submitted = new AtomicInteger(-1); ++ private final AtomicInteger priority; ++ private final Supplier run; ++ private final CompletableFuture future = new CompletableFuture<>(); ++ private volatile PriorityQueuedExecutor executor; ++ ++ public PendingTask(Supplier run) { ++ this(run, Priority.NORMAL); ++ } ++ ++ public PendingTask(Supplier run, Priority priority) { ++ this.priority = new AtomicInteger(priority.ordinal()); ++ this.run = run; ++ } ++ ++ @Override ++ public void run() { ++ if (!hasRan.compareAndSet(false, true)) { ++ return; ++ } ++ ++ try { ++ future.complete(run.get()); ++ } catch (Throwable e) { ++ future.completeExceptionally(e); ++ } ++ } ++ ++ public void bumpPriority() { ++ if (!priority.compareAndSet(Priority.NORMAL.ordinal(), Priority.HIGH.ordinal())) { ++ return; ++ } ++ ++ if (this.executor == null) { ++ return; ++ } ++ // If we have already been submitted, resubmit with new priority ++ submit(this.executor); ++ } ++ ++ public CompletableFuture onDone() { ++ return future; ++ } ++ ++ public void submit(PriorityQueuedExecutor executor) { ++ for (;;) { ++ final int submitted = this.submitted.get(); ++ final int priority = this.priority.get(); ++ if (submitted == priority) { ++ return; ++ } ++ if (this.submitted.compareAndSet(submitted, priority)) { ++ if (priority == 1) { ++ high.add(this); ++ } else { ++ normal.add(this); ++ } ++ ++ break; ++ } ++ } ++ ++ //noinspection SynchronizationOnLocalVariableOrMethodParameter ++ synchronized (executor) { ++ // Wake up a thread to take this work ++ executor.notify(); ++ } ++ } ++ } ++ public interface RejectionHandler { ++ void onRejection(Runnable run, PriorityQueuedExecutor executor); ++ } ++ ++ public static final RejectionHandler ABORT_POLICY = (run, executor) -> { ++ throw new RejectedExecutionException("Executor has been shutdown"); ++ }; ++ public static final RejectionHandler CALLER_RUNS_POLICY = (run, executor) -> { ++ run.run(); ++ }; ++ ++} +diff --git a/src/main/java/net/minecraft/server/Chunk.java b/src/main/java/net/minecraft/server/Chunk.java +index 165a901010..51d1503fb3 100644 +--- a/src/main/java/net/minecraft/server/Chunk.java ++++ b/src/main/java/net/minecraft/server/Chunk.java +@@ -917,6 +917,7 @@ public class Chunk implements IChunkAccess { + } + + public void addEntities() { ++ ChunkMap.onPostLoad(this); // Paper + this.i = true; + this.world.a(this.tileEntities.values()); + List[] aentityslice = this.entitySlices; // Spigot +diff --git a/src/main/java/net/minecraft/server/ChunkMap.java b/src/main/java/net/minecraft/server/ChunkMap.java +index b941676829..1b85520775 100644 +--- a/src/main/java/net/minecraft/server/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/ChunkMap.java +@@ -48,7 +48,13 @@ public class ChunkMap extends Long2ObjectOpenHashMap { + } + } + } ++ + chunk.world.timings.syncChunkLoadPostTimer.stopTiming(); // Paper ++ // Paper start ++ return chunk1; ++ } ++ static void onPostLoad(Chunk chunk) { ++ // Paper end + + if (chunk.newChunk) { + chunk.world.timings.syncChunkLoadPopulateTimer.startTiming(); // Paper +@@ -76,7 +82,7 @@ public class ChunkMap extends Long2ObjectOpenHashMap { + } + // CraftBukkit end + +- return chunk1; ++ //return chunk1; // Paper + } + + public Chunk a(Long olong, Chunk chunk) { +diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java +index 99613b2ef3..56576d2d06 100644 +--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java ++++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java +@@ -41,9 +41,9 @@ public class ChunkProviderServer implements IChunkProvider { + public final Long2ObjectMap chunks = Long2ObjectMaps.synchronize(new ChunkMap(8192)); + private Chunk lastChunk; + private final ChunkTaskScheduler chunkScheduler; +- private final SchedulerBatch batchScheduler; ++ final SchedulerBatch batchScheduler; // Paper + public final WorldServer world; +- private final IAsyncTaskHandler asyncTaskHandler; ++ final IAsyncTaskHandler asyncTaskHandler; // Paper + + public ChunkProviderServer(WorldServer worldserver, IChunkLoader ichunkloader, ChunkGenerator chunkgenerator, IAsyncTaskHandler iasynctaskhandler) { + this.world = worldserver; +@@ -80,10 +80,61 @@ public class ChunkProviderServer implements IChunkProvider { + this.unloadQueue.remove(ChunkCoordIntPair.a(i, j)); + } + ++ // Paper start - defaults if Async Chunks is not enabled ++ boolean chunkGoingToExists(int x, int z) { ++ final long k = ChunkCoordIntPair.asLong(x, z); ++ return chunkScheduler.progressCache.containsKey(k); ++ } ++ public void bumpPriority(ChunkCoordIntPair coords) { ++ // do nothing, override in async ++ } ++ ++ public List getSpiralOutChunks(BlockPosition blockposition, int radius) { ++ List list = com.google.common.collect.Lists.newArrayList(); ++ ++ for (int r = 1; r <= radius; r++) { ++ int x = -r; ++ int z = r; ++ list.add(new ChunkCoordIntPair(blockposition.getX(), blockposition.getZ())); ++ // Iterates the edge of half of the box; then negates for other half. ++ while (x <= r && z > -r) { ++ list.add(new ChunkCoordIntPair(blockposition.getX() + x, blockposition.getZ() + z)); ++ list.add(new ChunkCoordIntPair(blockposition.getX() - x, blockposition.getZ() - z)); ++ ++ if (x < r) { ++ x++; ++ } else { ++ z--; ++ } ++ } ++ } ++ return list; ++ } ++ ++ public Chunk getChunkAt(int x, int z, boolean load, boolean gen, Consumer consumer) { ++ return getChunkAt(x, z, load, gen, false, consumer); ++ } ++ public Chunk getChunkAt(int x, int z, boolean load, boolean gen, boolean priority, Consumer consumer) { ++ Chunk chunk = getChunkAt(x, z, load, gen); ++ if (consumer != null) { ++ consumer.accept(chunk); ++ } ++ return chunk; ++ } ++ // Paper end ++ + @Nullable + public Chunk getChunkAt(int i, int j, boolean flag, boolean flag1) { + IChunkLoader ichunkloader = this.chunkLoader; + Chunk chunk; ++ // Paper start - do already loaded checks before synchronize ++ long k = ChunkCoordIntPair.a(i, j); ++ chunk = (Chunk) this.chunks.get(k); ++ if (chunk != null) { ++ //this.lastChunk = chunk; // Paper remove vanilla lastChunk ++ return chunk; ++ } ++ // Paper end + + synchronized (this.chunkLoader) { + // Paper start - remove vanilla lastChunk, we do it more accurately +@@ -91,13 +142,15 @@ public class ChunkProviderServer implements IChunkProvider { + return this.lastChunk; + }*/ // Paper end + +- long k = ChunkCoordIntPair.a(i, j); ++ // Paper start - move up ++ //long k = ChunkCoordIntPair.a(i, j); + +- chunk = (Chunk) this.chunks.get(k); ++ /*chunk = (Chunk) this.chunks.get(k); + if (chunk != null) { + //this.lastChunk = chunk; // Paper remove vanilla lastChunk + return chunk; +- } ++ }*/ ++ // Paper end + + if (flag) { + try (co.aikar.timings.Timing timing = world.timings.syncChunkLoadTimer.startTiming()) { // Paper +@@ -153,7 +206,8 @@ public class ChunkProviderServer implements IChunkProvider { + return (IChunkAccess) (chunk != null ? chunk : (IChunkAccess) this.chunkScheduler.b(new ChunkCoordIntPair(i, j), flag)); + } + +- public CompletableFuture a(Iterable iterable, Consumer consumer) { ++ public CompletableFuture loadAllChunks(Iterable iterable, Consumer consumer) { return a(iterable, consumer).thenCompose(protoChunk -> null); } // Paper - overriden in async chunk provider ++ private CompletableFuture a(Iterable iterable, Consumer consumer) { // Paper - mark private, use above method + this.batchScheduler.b(); + Iterator iterator = iterable.iterator(); + +@@ -171,6 +225,7 @@ public class ChunkProviderServer implements IChunkProvider { + return this.batchScheduler.c(); + } + ++ ReportedException generateChunkError(int i, int j, Throwable throwable) { return a(i, j, throwable); } // Paper - OBFHELPER + private ReportedException a(int i, int j, Throwable throwable) { + CrashReport crashreport = CrashReport.a(throwable, "Exception generating new chunk"); + CrashReportSystemDetails crashreportsystemdetails = crashreport.a("Chunk to be generated"); +@@ -290,11 +345,13 @@ public class ChunkProviderServer implements IChunkProvider { + } + + public void close() { +- try { ++ // Paper start - we do not need to wait for chunk generations to finish on close ++ /*try { + this.batchScheduler.a(); + } catch (InterruptedException interruptedexception) { + ChunkProviderServer.a.error("Couldn\'t stop taskManager", interruptedexception); +- } ++ }*/ ++ // Paper end + + } + +@@ -433,4 +490,5 @@ public class ChunkProviderServer implements IChunkProvider { + public boolean isLoaded(int i, int j) { + return this.chunks.containsKey(ChunkCoordIntPair.a(i, j)); + } ++ + } +diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java +index c2007b4d0c..9b8db0c612 100644 +--- a/src/main/java/net/minecraft/server/ChunkRegionLoader.java ++++ b/src/main/java/net/minecraft/server/ChunkRegionLoader.java +@@ -128,7 +128,7 @@ public class ChunkRegionLoader implements IChunkLoader, IAsyncChunkSaver { + // CraftBukkit start + private boolean check(ChunkProviderServer cps, int x, int z) throws IOException { + if (cps != null) { +- com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread"); ++ //com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread"); // Paper - this is safe + if (cps.isLoaded(x, z)) { + return true; + } +@@ -378,11 +378,12 @@ public class ChunkRegionLoader implements IChunkLoader, IAsyncChunkSaver { + } + }; + } else { ++ /* // Paper start - we will never invoke this in an unsafe way + NBTTagCompound nbttagcompound2 = this.a(world, chunkcoordintpair.x, chunkcoordintpair.z); + + if (nbttagcompound2 != null && this.a(nbttagcompound2) == ChunkStatus.Type.LEVELCHUNK) { + return; +- } ++ }*/ // Paper end + + completion = new Supplier() { + public NBTTagCompound get() { +diff --git a/src/main/java/net/minecraft/server/ChunkTaskScheduler.java b/src/main/java/net/minecraft/server/ChunkTaskScheduler.java +index 34019bd1b3..4ca977645f 100644 +--- a/src/main/java/net/minecraft/server/ChunkTaskScheduler.java ++++ b/src/main/java/net/minecraft/server/ChunkTaskScheduler.java +@@ -20,7 +20,7 @@ public class ChunkTaskScheduler extends Scheduler d; + private final IChunkLoader e; + private final IAsyncTaskHandler f; +- private final Long2ObjectMap progressCache = new ExpiringMap(8192, 5000) { // CraftBukkit - decompile error ++ final Long2ObjectMap progressCache = new ExpiringMap(8192, 5000) { // CraftBukkit - decompile error // Paper - synchronize + protected boolean a(Scheduler.a scheduler_a) { + ProtoChunk protochunk = (ProtoChunk) scheduler_a.a(); + +@@ -50,7 +50,7 @@ public class ChunkTaskScheduler extends Scheduler { + ProtoChunk protochunk; + +diff --git a/src/main/java/net/minecraft/server/DataPaletteBlock.java b/src/main/java/net/minecraft/server/DataPaletteBlock.java +index 71a3636be6..b0170db9ca 100644 +--- a/src/main/java/net/minecraft/server/DataPaletteBlock.java ++++ b/src/main/java/net/minecraft/server/DataPaletteBlock.java +@@ -3,7 +3,7 @@ package net.minecraft.server; + import com.destroystokyo.paper.antixray.ChunkPacketInfo; // Paper - Anti-Xray + import java.util.Arrays; + import java.util.Objects; +-import java.util.concurrent.locks.ReentrantLock; ++import java.util.concurrent.locks.ReentrantReadWriteLock; + import java.util.function.Function; + import java.util.stream.Collectors; + +@@ -20,25 +20,16 @@ public class DataPaletteBlock implements DataPaletteExpandable { + protected DataBits a; protected DataBits getDataBits() { return this.a; } // Paper - OBFHELPER + private DataPalette h; private DataPalette getDataPalette() { return this.h; } // Paper - OBFHELPER + private int i; private int getBitsPerObject() { return this.i; } // Paper - OBFHELPER +- private final ReentrantLock j = new ReentrantLock(); ++ // Paper start - use read write locks ++ private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + private void b() { +- if (this.j.isLocked() && !this.j.isHeldByCurrentThread()) { +- String s = (String)Thread.getAllStackTraces().keySet().stream().filter(Objects::nonNull).map((thread) -> { +- return thread.getName() + ": \n\tat " + (String)Arrays.stream(thread.getStackTrace()).map(Object::toString).collect(Collectors.joining("\n\tat ")); +- }).collect(Collectors.joining("\n")); +- CrashReport crashreport = new CrashReport("Writing into PalettedContainer from multiple threads", new IllegalStateException()); +- CrashReportSystemDetails crashreportsystemdetails = crashreport.a("Thread dumps"); +- crashreportsystemdetails.a("Thread dumps", s); +- throw new ReportedException(crashreport); +- } else { +- this.j.lock(); +- } ++ lock.writeLock().lock(); + } +- + private void c() { +- this.j.unlock(); ++ lock.writeLock().unlock(); + } ++ // Paper end + + public DataPaletteBlock(DataPalette datapalette, RegistryBlockID registryblockid, Function function, Function function1, T object) { + // Paper start - Anti-Xray - Support default constructor +@@ -147,8 +138,13 @@ public class DataPaletteBlock implements DataPaletteExpandable { + } + + protected T a(int ix) { +- T object = this.h.a(this.a.a(ix)); // Paper - decompile fix +- return (T)(object == null ? this.g : object); ++ try { // Paper ++ lock.readLock().lock(); ++ T object = this.h.a(this.a.a(ix)); // Paper - decompile fix ++ return (T)(object == null ? this.g : object); ++ } finally { ++ lock.readLock().unlock(); ++ } // Paper + } + + // Paper start - Anti-Xray - Support default methods +diff --git a/src/main/java/net/minecraft/server/DefinedStructureManager.java b/src/main/java/net/minecraft/server/DefinedStructureManager.java +index 271dc41d45..bd15534c23 100644 +--- a/src/main/java/net/minecraft/server/DefinedStructureManager.java ++++ b/src/main/java/net/minecraft/server/DefinedStructureManager.java +@@ -19,7 +19,7 @@ import org.apache.logging.log4j.Logger; + + public class DefinedStructureManager implements IResourcePackListener { + private static final Logger a = LogManager.getLogger(); +- private final Map b = Maps.newHashMap(); ++ private final Map b = Maps.newConcurrentMap(); // Paper + private final DataFixer c; + private final MinecraftServer d; + private final java.nio.file.Path e; +diff --git a/src/main/java/net/minecraft/server/Entity.java b/src/main/java/net/minecraft/server/Entity.java +index 8c2ce70060..244302d45b 100644 +--- a/src/main/java/net/minecraft/server/Entity.java ++++ b/src/main/java/net/minecraft/server/Entity.java +@@ -209,7 +209,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke + this.random = SHARED_RANDOM; // Paper + this.fireTicks = -this.getMaxFireTicks(); + this.justCreated = true; +- this.uniqueID = MathHelper.a(this.random); ++ this.uniqueID = MathHelper.a(java.util.concurrent.ThreadLocalRandom.current()); // Paper + this.au = this.uniqueID.toString(); + this.aJ = Sets.newHashSet(); + this.aL = new double[] { 0.0D, 0.0D, 0.0D}; +diff --git a/src/main/java/net/minecraft/server/IChunkLoader.java b/src/main/java/net/minecraft/server/IChunkLoader.java +index 4698ee99f8..dfb45cc4ea 100644 +--- a/src/main/java/net/minecraft/server/IChunkLoader.java ++++ b/src/main/java/net/minecraft/server/IChunkLoader.java +@@ -6,6 +6,8 @@ import javax.annotation.Nullable; + + public interface IChunkLoader { + ++ void loadEntities(NBTTagCompound nbttagcompound, Chunk chunk); // Paper - Async Chunks ++ Object[] loadChunk(GeneratorAccess generatoraccess, int i, int j, Consumer consumer) throws IOException; // Paper - Async Chunks + @Nullable + Chunk a(GeneratorAccess generatoraccess, int i, int j, Consumer consumer) throws IOException; + +diff --git a/src/main/java/net/minecraft/server/MathHelper.java b/src/main/java/net/minecraft/server/MathHelper.java +index 49fba0979e..c6661851d1 100644 +--- a/src/main/java/net/minecraft/server/MathHelper.java ++++ b/src/main/java/net/minecraft/server/MathHelper.java +@@ -142,6 +142,7 @@ public class MathHelper { + return Math.floorMod(i, j); + } + ++ public static float normalizeYaw(float fx) { return g(fx); } // Paper + public static float g(float fx) { + fx = fx % 360.0F; + if (fx >= 180.0F) { +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 98d182fdb8..487d98eb1b 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -503,6 +503,7 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati + + // CraftBukkit start - fire WorldLoadEvent and handle whether or not to keep the spawn in memory + Stopwatch stopwatch = Stopwatch.createStarted(); ++ boolean waitForChunks = Boolean.getBoolean("paper.waitforchunks"); // Paper + for (WorldServer worldserver : this.getWorlds()) { + MinecraftServer.LOGGER.info("Preparing start region for level " + worldserver.dimension + " (Seed: " + worldserver.getSeed() + ")"); + if (!worldserver.getWorld().getKeepSpawnInMemory()) { +@@ -510,29 +511,25 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati + } + + BlockPosition blockposition = worldserver.getSpawn(); +- ArrayList arraylist = Lists.newArrayList(); ++ List arraylist = worldserver.getChunkProviderServer().getSpiralOutChunks(blockposition, worldserver.paperConfig.keepLoadedRange >> 4); // Paper + Set set = Sets.newConcurrentHashSet(); + +- // Paper start +- short radius = worldserver.paperConfig.keepLoadedRange; +- for (int i = -radius; i <= radius && this.isRunning(); i += 16) { +- for (int j = -radius; j <= radius && this.isRunning(); j += 16) { +- // Paper end +- arraylist.add(new ChunkCoordIntPair(blockposition.getX() + i >> 4, blockposition.getZ() + j >> 4)); +- } +- } // Paper ++ // Paper - remove arraylist creation, call spiral above + if (this.isRunning()) { // Paper + int expected = arraylist.size(); // Paper + + +- CompletableFuture completablefuture = worldserver.getChunkProviderServer().a((Iterable) arraylist, (chunk) -> { ++ CompletableFuture completablefuture = worldserver.getChunkProviderServer().loadAllChunks(arraylist, (chunk) -> { // Paper + set.add(chunk.getPos()); +- if (set.size() < expected && set.size() % 25 == 0) this.a(new ChatMessage("menu.preparingSpawn", new Object[0]), set.size() * 100 / expected); // Paper ++ if (waitForChunks && (set.size() == expected || (set.size() < expected && set.size() % (set.size() / 10) == 0))) { ++ this.a(new ChatMessage("menu.preparingSpawn", new Object[0]), set.size() * 100 / expected); // Paper ++ } + }); + +- while (!completablefuture.isDone()) { ++ while (waitForChunks && !completablefuture.isDone() && isRunning()) { // Paper + try { +- completablefuture.get(1L, TimeUnit.SECONDS); ++ PaperAsyncChunkProvider.processChunkLoads(worldserver); // Paper ++ completablefuture.get(50L, TimeUnit.MILLISECONDS); // Paper + } catch (InterruptedException interruptedexception) { + throw new RuntimeException(interruptedexception); + } catch (ExecutionException executionexception) { +@@ -542,11 +539,11 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati + + throw new RuntimeException(executionexception.getCause()); + } catch (TimeoutException timeoutexception) { +- this.a(new ChatMessage("menu.preparingSpawn", new Object[0]), set.size() * 100 / expected); // Paper ++ //this.a(new ChatMessage("menu.preparingSpawn", new Object[0]), set.size() * 100 / expected); // Paper + } + } + +- this.a(new ChatMessage("menu.preparingSpawn", new Object[0]), set.size() * 100 / expected); // Paper ++ if (waitForChunks) this.a(new ChatMessage("menu.preparingSpawn", new Object[0]), set.size() * 100 / expected); // Paper + } + } + +@@ -650,6 +647,7 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati + if (hasStopped) return; + hasStopped = true; + } ++ PaperAsyncChunkProvider.stop(this); // Paper + // CraftBukkit end + MinecraftServer.LOGGER.info("Stopping server"); + MinecraftTimings.stopServer(); // Paper +@@ -1017,6 +1015,7 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati + while ((futuretask = (FutureTask) this.f.poll()) != null) { + SystemUtils.a(futuretask, MinecraftServer.LOGGER); + } ++ PaperAsyncChunkProvider.processChunkLoads(this); // Paper + MinecraftTimings.minecraftSchedulerTimer.stopTiming(); // Paper + + this.methodProfiler.c("commandFunctions"); +@@ -1053,6 +1052,7 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati + // CraftBukkit - dropTickTime + for (Iterator iterator = this.getWorlds().iterator(); iterator.hasNext();) { + WorldServer worldserver = (WorldServer) iterator.next(); ++ PaperAsyncChunkProvider.processChunkLoads(worldserver); // Paper + TileEntityHopper.skipHopperEvents = worldserver.paperConfig.disableHopperMoveEvents || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper + i = SystemUtils.c(); + if (true || worldserver.worldProvider.getDimensionManager() == DimensionManager.OVERWORLD || this.getAllowNether()) { // CraftBukkit +@@ -1109,6 +1109,7 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati + this.methodProfiler.e(); + this.methodProfiler.e(); + worldserver.explosionDensityCache.clear(); // Paper - Optimize explosions ++ PaperAsyncChunkProvider.processChunkLoads(worldserver); // Paper + } + } + +diff --git a/src/main/java/net/minecraft/server/PaperAsyncChunkProvider.java b/src/main/java/net/minecraft/server/PaperAsyncChunkProvider.java +new file mode 100644 +index 0000000000..604f1db287 +--- /dev/null ++++ b/src/main/java/net/minecraft/server/PaperAsyncChunkProvider.java +@@ -0,0 +1,478 @@ ++/* ++ * This file is licensed under the MIT License (MIT). ++ * ++ * Copyright (c) 2018 Daniel Ennis ++ * ++ * Permission is hereby granted, free of charge, to any person obtaining a copy ++ * of this software and associated documentation files (the "Software"), to deal ++ * in the Software without restriction, including without limitation the rights ++ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ++ * copies of the Software, and to permit persons to whom the Software is ++ * furnished to do so, subject to the following conditions: ++ * ++ * The above copyright notice and this permission notice shall be included in ++ * all copies or substantial portions of the Software. ++ * ++ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ++ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ++ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ++ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ++ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ++ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ++ * THE SOFTWARE. ++ */ ++package net.minecraft.server; ++ ++import com.destroystokyo.paper.PaperConfig; ++import com.destroystokyo.paper.util.PriorityQueuedExecutor; ++import com.destroystokyo.paper.util.PriorityQueuedExecutor.Priority; ++import it.unimi.dsi.fastutil.longs.Long2ObjectMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import org.bukkit.Bukkit; ++import org.bukkit.craftbukkit.generator.CustomChunkGenerator; ++import org.bukkit.craftbukkit.generator.InternalChunkGenerator; ++ ++import javax.annotation.Nullable; ++import java.io.IOException; ++import java.util.ArrayList; ++import java.util.Iterator; ++import java.util.List; ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.ConcurrentLinkedQueue; ++import java.util.function.Consumer; ++ ++@SuppressWarnings("unused") ++public class PaperAsyncChunkProvider extends ChunkProviderServer { ++ ++ private static final PriorityQueuedExecutor.RejectionHandler IGNORE_HANDLER = (run, executor) -> {}; ++ private static final int CHUNK_THREADS = (int) Math.min(Integer.getInteger("paper.maxchunkthreads", 8), Runtime.getRuntime().availableProcessors() * 1.5); ++ private static final PriorityQueuedExecutor EXECUTOR = new PriorityQueuedExecutor("PaperChunkLoader", PaperConfig.asyncChunks ? CHUNK_THREADS : 0, IGNORE_HANDLER); ++ ++ private final PriorityQueuedExecutor generationExecutor; ++ //private static final PriorityQueuedExecutor generationExecutor = new PriorityQueuedExecutor("PaperChunkGen", 1); ++ private final Long2ObjectMap pendingChunks = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>()); ++ private final ConcurrentLinkedQueue mainThreadQueue = new ConcurrentLinkedQueue<>(); ++ private final IAsyncTaskHandler asyncHandler; ++ ++ private final WorldServer world; ++ private final IChunkLoader chunkLoader; ++ private final MinecraftServer server; ++ private final boolean shouldGenSync; ++ ++ public PaperAsyncChunkProvider(WorldServer world, IChunkLoader chunkLoader, InternalChunkGenerator generator, MinecraftServer server) { ++ super(world, chunkLoader, generator, server); ++ ++ this.server = world.getMinecraftServer(); ++ this.world = world; ++ this.asyncHandler = server; ++ this.chunkLoader = chunkLoader; ++ String worldName = this.world.getWorld().getName(); ++ this.shouldGenSync = generator instanceof CustomChunkGenerator && !(((CustomChunkGenerator) generator).asyncSupported); ++ this.generationExecutor = new PriorityQueuedExecutor("PaperChunkGen-" + worldName, shouldGenSync ? 0 : 1, IGNORE_HANDLER); ++ } ++ ++ static void processChunkLoads(MinecraftServer server) { ++ for (WorldServer world : server.getWorlds()) { ++ processChunkLoads(world); ++ } ++ } ++ ++ static void processChunkLoads(World world) { ++ IChunkProvider chunkProvider = world.getChunkProvider(); ++ if (chunkProvider instanceof PaperAsyncChunkProvider) { ++ ((PaperAsyncChunkProvider) chunkProvider).processChunkLoads(); ++ } ++ } ++ ++ static void stop(MinecraftServer server) { ++ EXECUTOR.shutdownNow(); ++ for (WorldServer world : server.getWorlds()) { ++ IChunkProvider chunkProvider = world.getChunkProvider(); ++ if (chunkProvider instanceof PaperAsyncChunkProvider) { ++ ((PaperAsyncChunkProvider) chunkProvider).generationExecutor.shutdownNow(); ++ } ++ } ++ } ++ ++ private boolean processChunkLoads() { ++ Runnable run; ++ boolean hadLoad = false; ++ while ((run = mainThreadQueue.poll()) != null) { ++ run.run(); ++ hadLoad = true; ++ } ++ return hadLoad; ++ } ++ ++ @Override ++ public void bumpPriority(ChunkCoordIntPair coords) { ++ PendingChunk pending = pendingChunks.get(coords.asLong()); ++ if (pending != null) { ++ pending.bumpPriority(); ++ } ++ } ++ ++ @Nullable ++ @Override ++ public Chunk getChunkAt(int x, int z, boolean load, boolean gen) { ++ return getChunkAt(x, z, load, gen, null); ++ } ++ ++ @Nullable ++ @Override ++ public Chunk getChunkAt(int x, int z, boolean load, boolean gen, boolean priority, Consumer consumer) { ++ long key = ChunkCoordIntPair.asLong(x, z); ++ Chunk chunk = this.chunks.get(key); ++ if (chunk != null || !load) { // return null if we aren't loading ++ return chunk; ++ } ++ return loadOrGenerateChunk(x, z, gen, priority, consumer); // Async overrides this method ++ } ++ ++ private Chunk loadOrGenerateChunk(int x, int z, boolean gen, boolean priority, Consumer consumer) { ++ final long key = ChunkCoordIntPair.asLong(x, z); ++ ++ // Obtain a PendingChunk ++ final PendingChunk pending; ++ final boolean isBlockingMain = consumer == null && server.isMainThread(); ++ synchronized (pendingChunks) { ++ PendingChunk pendingChunk = pendingChunks.get(key); ++ // DO NOT CALL ANY METHODS ON PENDING CHUNK IN THIS BLOCK - WILL DEADLOCK ++ if (pendingChunk == null) { ++ pending = new PendingChunk(x, z, key, gen, priority || isBlockingMain); ++ pendingChunks.put(key, pending); ++ } else if (pendingChunk.hasFinished && gen && !pendingChunk.canGenerate && pendingChunk.chunk == null) { ++ // need to overwrite the old ++ pending = new PendingChunk(x, z, key, true, priority || isBlockingMain); ++ pendingChunks.put(key, pending); ++ } else { ++ pending = pendingChunk; ++ if (priority || isBlockingMain) { ++ pending.bumpPriority(); ++ } ++ } ++ } ++ // Listen for when result is ready ++ final CompletableFuture future = new CompletableFuture<>(); ++ pending.addListener(future, gen); ++ ++ if (isBlockingMain && pending.hasFinished) { ++ processChunkLoads(); ++ return pending.postChunk(); ++ } ++ ++ if (isBlockingMain) { ++ try (co.aikar.timings.Timing timing = world.timings.syncChunkLoadTimer.startTiming()) { ++ while (!future.isDone()) { ++ // We aren't done, obtain lock on queue ++ synchronized (mainThreadQueue) { ++ // We may of received our request now, check it ++ if (processChunkLoads()) { ++ // If we processed SOMETHING, don't wait ++ continue; ++ } ++ try { ++ // We got nothing from the queue, wait until something has been added ++ mainThreadQueue.wait(1); ++ } catch (InterruptedException ignored) { ++ } ++ } ++ // Queue has been notified or timed out, process it ++ processChunkLoads(); ++ } ++ // We should be done AND posted into chunk map now, return it ++ return future.join(); ++ } ++ } else if (consumer == null) { ++ // This is on another thread ++ return future.join(); ++ } else { ++ future.thenAccept((c) -> this.asyncHandler.postToMainThread(() -> consumer.accept(c))); ++ } ++ return null; ++ } ++ ++ @Override ++ public CompletableFuture loadAllChunks(Iterable iterable, Consumer consumer) { ++ Iterator iterator = iterable.iterator(); ++ ++ List> all = new ArrayList<>(); ++ while (iterator.hasNext()) { ++ ChunkCoordIntPair chunkcoordintpair = iterator.next(); ++ CompletableFuture future = new CompletableFuture<>(); ++ all.add(future); ++ this.getChunkAt(chunkcoordintpair.x, chunkcoordintpair.z, true, true, chunk -> { ++ future.complete(chunk); ++ if (consumer != null) { ++ consumer.accept(chunk); ++ } ++ }); ++ } ++ return CompletableFuture.allOf(all.toArray(new CompletableFuture[0])); ++ } ++ ++ boolean chunkGoingToExists(int x, int z) { ++ synchronized (pendingChunks) { ++ long key = ChunkCoordIntPair.asLong(x, z); ++ PendingChunk pendingChunk = pendingChunks.get(key); ++ return pendingChunk != null && pendingChunk.canGenerate; ++ } ++ } ++ ++ private enum PendingStatus { ++ /** ++ * Request has just started ++ */ ++ STARTED, ++ /** ++ * Chunk is attempting to be loaded from disk ++ */ ++ LOADING, ++ /** ++ * Chunk must generate on main and is pending main ++ */ ++ GENERATION_PENDING, ++ /** ++ * Chunk is generating ++ */ ++ GENERATING, ++ /** ++ * Chunk is ready and is pending post to main ++ */ ++ PENDING_MAIN, ++ /** ++ * Could not load chunk, and did not need to generat ++ */ ++ FAIL, ++ /** ++ * Fully done with this request (may or may not of loaded) ++ */ ++ DONE ++ } ++ ++ private class PendingChunk implements Runnable { ++ private final int x; ++ private final int z; ++ private final long key; ++ private final PriorityQueuedExecutor.PendingTask task; ++ private final PriorityQueuedExecutor.PendingTask chunkGenTask; ++ private final CompletableFuture loadOnly = new CompletableFuture<>(); ++ private final CompletableFuture generate = new CompletableFuture<>(); ++ ++ private volatile PendingStatus status = PendingStatus.STARTED; ++ private volatile boolean generating; ++ private volatile boolean canGenerate; ++ private volatile boolean isHighPriority; ++ private volatile boolean hasPosted; ++ private volatile boolean hasFinished; ++ private volatile Chunk chunk; ++ private volatile NBTTagCompound pendingLevel; ++ ++ PendingChunk(int x, int z, long key, boolean canGenerate, boolean priority) { ++ this.x = x; ++ this.z = z; ++ this.key = key; ++ this.canGenerate = canGenerate; ++ Priority taskPriority = priority ? Priority.HIGH : Priority.NORMAL; ++ this.chunkGenTask = generationExecutor.createPendingTask(this::generateChunk, taskPriority); ++ this.task = EXECUTOR.submitTask(this, taskPriority); ++ } ++ ++ private synchronized void setStatus(PendingStatus status) { ++ this.status = status; ++ } ++ ++ private Chunk loadChunk(int x, int z) throws IOException { ++ setStatus(PendingStatus.LOADING); ++ Object[] data = chunkLoader.loadChunk(world, x, z, null); ++ if (data != null) { ++ // Level must be loaded on main ++ this.pendingLevel = ((NBTTagCompound) data[1]).getCompound("Level"); ++ return (Chunk) data[0]; ++ } else { ++ return null; ++ } ++ } ++ ++ private Chunk generateChunk() { ++ CompletableFuture pending = new CompletableFuture<>(); ++ batchScheduler.startBatch(); ++ batchScheduler.add(new ChunkCoordIntPair(x, z)); ++ try { ++ ProtoChunk protoChunk = batchScheduler.executeBatch().join(); ++ boolean saved = false; ++ if (!Bukkit.isPrimaryThread()) { ++ // If we are async, dispatch later ++ try { ++ chunkLoader.saveChunk(world, protoChunk, true); ++ saved = true; ++ } catch (IOException | ExceptionWorldConflict e) { ++ e.printStackTrace(); ++ } ++ } ++ Chunk chunk = new Chunk(world, protoChunk, x, z); ++ if (saved) { ++ chunk.setLastSaved(world.getTime()); ++ } ++ generateFinished(chunk); ++ ++ return chunk; ++ } catch (Exception e) { ++ MinecraftServer.LOGGER.error("Couldn't generate chunk (" +world.getWorld().getName() + ":" + x + "," + z + ")", e); ++ generateFinished(null); ++ return null; ++ } ++ } ++ ++ boolean loadFinished(Chunk chunk) { ++ if (chunk != null) { ++ postChunkToMain(chunk); ++ return false; ++ } ++ loadOnly.complete(null); ++ ++ synchronized (this) { ++ if (!canGenerate) { ++ setStatus(PendingStatus.FAIL); ++ this.chunk = null; ++ this.hasFinished = true; ++ pendingChunks.remove(key); ++ return false; ++ } else { ++ setStatus(PendingStatus.GENERATING); ++ generating = true; ++ return true; ++ } ++ } ++ } ++ ++ void generateFinished(Chunk chunk) { ++ synchronized (this) { ++ this.chunk = chunk; ++ this.hasFinished = true; ++ } ++ if (chunk != null) { ++ postChunkToMain(chunk); ++ } else { ++ synchronized (this) { ++ pendingChunks.remove(key); ++ completeFutures(null); ++ } ++ } ++ } ++ ++ synchronized private void completeFutures(Chunk chunk) { ++ loadOnly.complete(chunk); ++ generate.complete(chunk); ++ } ++ ++ private void postChunkToMain(Chunk chunk) { ++ synchronized (this) { ++ setStatus(PendingStatus.PENDING_MAIN); ++ this.chunk = chunk; ++ this.hasFinished = true; ++ } ++ if (Bukkit.isPrimaryThread()) { ++ postChunk(); ++ } else { ++ synchronized (mainThreadQueue) { ++ mainThreadQueue.add(this::postChunk); ++ mainThreadQueue.notify(); ++ } ++ } ++ } ++ ++ Chunk postChunk() { ++ if (!server.isMainThread()) { ++ throw new IllegalStateException("Must post from main"); ++ } ++ synchronized (this) { ++ if (hasPosted) { ++ return chunk; ++ } ++ hasPosted = true; ++ } ++ try { ++ if (chunk == null) { ++ chunk = chunks.get(key); ++ completeFutures(chunk); ++ return chunk; ++ } ++ if (pendingLevel != null) { ++ chunkLoader.loadEntities(pendingLevel, chunk); ++ pendingLevel = null; ++ } ++ if (!chunk.newChunk) { ++ chunk.setLastSaved(chunk.world.getTime()); ++ } ++ synchronized (chunks) { ++ final Chunk other = chunks.get(key); ++ if (other != null) { ++ this.chunk = other; ++ completeFutures(other); ++ return other; ++ } ++ if (chunk != null) { ++ chunks.put(key, chunk); ++ } ++ } ++ ++ chunk.addEntities(); ++ ++ completeFutures(chunk); ++ return chunk; ++ } finally { ++ pendingChunks.remove(key); ++ setStatus(PendingStatus.DONE); ++ } ++ } ++ ++ synchronized void addListener(CompletableFuture future, boolean gen) { ++ if (hasFinished) { ++ future.complete(chunk); ++ } else if (gen) { ++ canGenerate = true; ++ generate.thenAccept(future::complete); ++ } else { ++ if (generating) { ++ future.complete(null); ++ } else { ++ loadOnly.thenAccept(future::complete); ++ } ++ } ++ } ++ ++ @Override ++ public void run() { ++ try { ++ if (!loadFinished(loadChunk(x, z))) { ++ return; ++ } ++ } catch (Exception ex) { ++ MinecraftServer.LOGGER.error("Couldn't load chunk (" +world.getWorld().getName() + ":" + x + "," + z + ")", ex); ++ if (!(ex instanceof IOException)) { ++ return; ++ } ++ } ++ ++ if (shouldGenSync) { ++ synchronized (this) { ++ setStatus(PendingStatus.GENERATION_PENDING); ++ mainThreadQueue.add(() -> generateFinished(this.generateChunk())); ++ } ++ synchronized (mainThreadQueue) { ++ mainThreadQueue.notify(); ++ } ++ } else { ++ generationExecutor.submitTask(chunkGenTask); ++ } ++ } ++ ++ void bumpPriority() { ++ task.bumpPriority(); ++ chunkGenTask.bumpPriority(); ++ } ++ } ++ ++} +diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java +index e7d465fb8a..61de438fdf 100644 +--- a/src/main/java/net/minecraft/server/PlayerChunk.java ++++ b/src/main/java/net/minecraft/server/PlayerChunk.java +@@ -30,13 +30,42 @@ public class PlayerChunk { + // All may seem good at first, but there's deeper issues if you play for a bit + boolean chunkExists; // Paper + private boolean loadInProgress = false; +- private Runnable loadedRunnable = new Runnable() { +- public void run() { +- loadInProgress = false; +- PlayerChunk.this.chunk = PlayerChunk.this.playerChunkMap.getWorld().getChunkProviderServer().getChunkAt(location.x, location.z, true, true); +- markChunkUsed(); // Paper - delay chunk unloads +- } ++ // Paper start ++ private java.util.function.Consumer chunkLoadedConsumer = chunk -> { ++ loadInProgress = false; ++ PlayerChunk pChunk = PlayerChunk.this; ++ pChunk.chunk = chunk; ++ markChunkUsed(); // Paper - delay chunk unloads + }; ++ private boolean markedHigh = false; ++ void checkHighPriority(EntityPlayer player) { ++ if (markedHigh || chunk != null) { ++ return; ++ } ++ final double dist = getDistance(player.locX, player.locZ); ++ if (dist > 8) { ++ return; ++ } ++ if (dist >= 3 && getDistance(player, 5) > 3.5) { ++ return; ++ } ++ ++ markedHigh = true; ++ playerChunkMap.getWorld().getChunkProviderServer().bumpPriority(location); ++ } ++ private double getDistance(EntityPlayer player, int inFront) { ++ final float yaw = MathHelper.normalizeYaw(player.yaw); ++ final double x = player.locX + (inFront * Math.cos(Math.toRadians(yaw))); ++ final double z = player.locZ + (inFront * Math.sin(Math.toRadians(yaw))); ++ return getDistance(x, z); ++ } ++ ++ private double getDistance(double blockX, double blockZ) { ++ final double x = location.x - ((int)Math.floor(blockX) >> 4); ++ final double z = location.z - ((int)Math.floor(blockZ) >> 4); ++ return Math.sqrt((x * x) + (z * z)); ++ } ++ // Paper end + // Paper start - delay chunk unloads + public final void markChunkUsed() { + if (chunk != null && chunk.scheduledForUnload != null) { +@@ -52,8 +81,8 @@ public class PlayerChunk { + ChunkProviderServer chunkproviderserver = playerchunkmap.getWorld().getChunkProviderServer(); + + chunkproviderserver.a(i, j); +- this.chunk = chunkproviderserver.getChunkAt(i, j, true, false); +- this.chunkExists = this.chunk != null || ChunkIOExecutor.hasQueuedChunkLoad(playerChunkMap.getWorld(), i, j); // Paper ++ this.chunk = chunkproviderserver.getChunkAt(i, j, false, false); // Paper ++ this.chunkExists = this.chunk != null || chunkproviderserver.chunkGoingToExists(i, j); // Paper + markChunkUsed(); // Paper - delay chunk unloads + } + +@@ -68,6 +97,7 @@ public class PlayerChunk { + if (this.c.isEmpty()) { + this.i = this.playerChunkMap.getWorld().getTime(); + } ++ checkHighPriority(entityplayer); // Paper + + this.c.add(entityplayer); + if (this.done) { +@@ -95,8 +125,13 @@ public class PlayerChunk { + if (this.chunk != null) { + return true; + } else { +- this.chunk = this.playerChunkMap.getWorld().getChunkProviderServer().getChunkAt(this.location.x, this.location.z, true, flag); +- markChunkUsed(); // Paper - delay chunk unloads ++ // Paper start - async chunks ++ if (!loadInProgress) { ++ loadInProgress = true; ++ this.chunk = this.playerChunkMap.getWorld().getChunkProviderServer().getChunkAt(this.location.x, this.location.z, true, flag, markedHigh, chunkLoadedConsumer); // Paper) ++ markChunkUsed(); // Paper - delay chunk unloads ++ } ++ // Paper end + return this.chunk != null; + } + } +diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java +index d1a443ca8d..6c54294d3f 100644 +--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java ++++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java +@@ -349,7 +349,13 @@ public class PlayerChunkMap { + if (playerchunk != null) { + playerchunk.b(entityplayer); + } ++ } else { // Paper start ++ PlayerChunk playerchunk = this.getChunk(l1 - j1, i2 - k1); ++ if (playerchunk != null) { ++ playerchunk.checkHighPriority(entityplayer); // Paper ++ } + } ++ // Paper end + } + } + +@@ -360,7 +366,11 @@ public class PlayerChunkMap { + // CraftBukkit start - send nearest chunks first + Collections.sort(chunksToLoad, new ChunkCoordComparator(entityplayer)); + for (ChunkCoordIntPair pair : chunksToLoad) { +- this.c(pair.x, pair.z).a(entityplayer); ++ // Paper start ++ PlayerChunk c = this.c(pair.x, pair.z); ++ c.checkHighPriority(entityplayer); ++ c.a(entityplayer); ++ // Paper end + } + // CraftBukkit end + } +diff --git a/src/main/java/net/minecraft/server/RegionLimitedWorldAccess.java b/src/main/java/net/minecraft/server/RegionLimitedWorldAccess.java +index 3c35c0f481..187ca2813a 100644 +--- a/src/main/java/net/minecraft/server/RegionLimitedWorldAccess.java ++++ b/src/main/java/net/minecraft/server/RegionLimitedWorldAccess.java +@@ -35,7 +35,7 @@ public class RegionLimitedWorldAccess implements GeneratorAccess { + this.d = l; + this.e = i; + this.f = j; +- this.g = world; ++ this.g = world.regionLimited(this); // Paper + this.h = world.getSeed(); + this.m = world.getChunkProvider().getChunkGenerator().getSettings(); + this.i = world.getSeaLevel(); +diff --git a/src/main/java/net/minecraft/server/SchedulerBatch.java b/src/main/java/net/minecraft/server/SchedulerBatch.java +index d868149d1a..0d94b262ac 100644 +--- a/src/main/java/net/minecraft/server/SchedulerBatch.java ++++ b/src/main/java/net/minecraft/server/SchedulerBatch.java +@@ -9,6 +9,7 @@ public class SchedulerBatch, R> { + private final Scheduler b; + private boolean c; + private int d = 1000; ++ private final java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock(true); // Paper + + public SchedulerBatch(Scheduler scheduler) { + this.b = scheduler; +@@ -18,7 +19,9 @@ public class SchedulerBatch, R> { + this.b.b(); + } + ++ public void startBatch() { b(); } // Paper - OBFHELPER + public void b() { ++ lock.lock(); // Paper + if (this.c) { + throw new RuntimeException("Batch already started."); + } else { +@@ -27,6 +30,7 @@ public class SchedulerBatch, R> { + } + } + ++ public CompletableFuture add(K k0) { return a(k0); } // Paper - OBFHELPER + public CompletableFuture a(K object) { + if (!this.c) { + throw new RuntimeException("Batch not properly started. Please use startBatch to create a new batch."); +@@ -42,7 +46,13 @@ public class SchedulerBatch, R> { + } + } + ++ public CompletableFuture executeBatch() { return c(); } // Paper - OBFHELPER + public CompletableFuture c() { ++ // Paper start ++ if (!lock.isHeldByCurrentThread()) { ++ throw new IllegalStateException("Current thread does not hold the write lock"); ++ } ++ try {// Paper end + if (!this.c) { + throw new RuntimeException("Batch not properly started. Please use startBatch to create a new batch."); + } else { +@@ -53,5 +63,6 @@ public class SchedulerBatch, R> { + this.c = false; + return this.b.c(); + } ++ } finally { lock.unlock(); } // Paper + } + } +diff --git a/src/main/java/net/minecraft/server/StructurePiece.java b/src/main/java/net/minecraft/server/StructurePiece.java +index a5cf017da1..def8730b86 100644 +--- a/src/main/java/net/minecraft/server/StructurePiece.java ++++ b/src/main/java/net/minecraft/server/StructurePiece.java +@@ -14,7 +14,7 @@ public abstract class StructurePiece { + private EnumBlockMirror b; + private EnumBlockRotation c; + protected int o; +- private static final Set d = ImmutableSet.builder().add(Blocks.NETHER_BRICK_FENCE).add(Blocks.TORCH).add(Blocks.WALL_TORCH).add(Blocks.OAK_FENCE).add(Blocks.SPRUCE_FENCE).add(Blocks.DARK_OAK_FENCE).add(Blocks.ACACIA_FENCE).add(Blocks.BIRCH_FENCE).add(Blocks.JUNGLE_FENCE).add(Blocks.LADDER).add(Blocks.IRON_BARS).build(); ++ private static final Set d = ImmutableSet.builder().add(Blocks.NETHER_BRICK_FENCE).add(Blocks.TORCH).add(Blocks.WALL_TORCH).add(Blocks.OAK_FENCE).add(Blocks.SPRUCE_FENCE).add(Blocks.DARK_OAK_FENCE).add(Blocks.ACACIA_FENCE).add(Blocks.BIRCH_FENCE).add(Blocks.JUNGLE_FENCE).add(Blocks.LADDER).add(Blocks.IRON_BARS).build(); // Paper - decompile error + + public StructurePiece() { + } +@@ -63,11 +63,11 @@ public abstract class StructurePiece { + } + + public static StructurePiece a(List list, StructureBoundingBox structureboundingbox) { +- for(StructurePiece structurepiece : list) { ++ synchronized (list) { for(StructurePiece structurepiece : list) { // Paper - synchronize main structure list + if (structurepiece.d() != null && structurepiece.d().a(structureboundingbox)) { + return structurepiece; + } +- } ++ }} // Paper + + return null; + } +diff --git a/src/main/java/net/minecraft/server/StructureStart.java b/src/main/java/net/minecraft/server/StructureStart.java +index f87182b5c4..574930f5fe 100644 +--- a/src/main/java/net/minecraft/server/StructureStart.java ++++ b/src/main/java/net/minecraft/server/StructureStart.java +@@ -6,7 +6,7 @@ import java.util.List; + import java.util.Random; + + public abstract class StructureStart { +- protected final List a = Lists.newArrayList(); ++ protected final List a = java.util.Collections.synchronizedList(Lists.newArrayList()); // Paper + protected StructureBoundingBox b; + protected int c; + protected int d; +@@ -49,9 +49,9 @@ public abstract class StructureStart { + protected void a(IBlockAccess var1) { + this.b = StructureBoundingBox.a(); + +- for(StructurePiece structurepiece : this.a) { ++ synchronized (this.a) {for(StructurePiece structurepiece : this.a) { // Paper - synchronize + this.b.b(structurepiece.d()); +- } ++ }} // Paper + + } + +@@ -114,9 +114,9 @@ public abstract class StructureStart { + int l = k - this.b.e; + this.b.a(0, l, 0); + +- for(StructurePiece structurepiece : this.a) { ++ synchronized (this.a) {for(StructurePiece structurepiece : this.a) { // Paper - synchronize + structurepiece.a(0, l, 0); +- } ++ }} // Paper + + } + +@@ -132,9 +132,9 @@ public abstract class StructureStart { + int i1 = l - this.b.b; + this.b.a(0, i1, 0); + +- for(StructurePiece structurepiece : this.a) { ++ synchronized (this.a) {for(StructurePiece structurepiece : this.a) { // Paper - synchronize + structurepiece.a(0, i1, 0); +- } ++ }} // Paper + + } + +diff --git a/src/main/java/net/minecraft/server/World.java b/src/main/java/net/minecraft/server/World.java +index e52e4bb458..13f69f1b82 100644 +--- a/src/main/java/net/minecraft/server/World.java ++++ b/src/main/java/net/minecraft/server/World.java +@@ -46,7 +46,7 @@ import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; + import org.bukkit.generator.ChunkGenerator; + // CraftBukkit end + +-public abstract class World implements IEntityAccess, GeneratorAccess, IIBlockAccess, AutoCloseable { ++public abstract class World implements IEntityAccess, GeneratorAccess, IIBlockAccess, AutoCloseable, Cloneable { // Paper + + protected static final Logger e = LogManager.getLogger(); + private static final EnumDirection[] a = EnumDirection.values(); +@@ -109,6 +109,24 @@ public abstract class World implements IEntityAccess, GeneratorAccess, IIBlockAc + protected PersistentVillage villages; + public final MethodProfiler methodProfiler; + public final boolean isClientSide; ++ // Paper start - yes this is hacky as shit ++ RegionLimitedWorldAccess regionLimited; ++ World originalWorld; ++ public World regionLimited(RegionLimitedWorldAccess limitedWorldAccess) { ++ try { ++ World clone = (World) super.clone(); ++ clone.regionLimited = limitedWorldAccess; ++ clone.originalWorld = this; ++ return clone; ++ } catch (CloneNotSupportedException e1) { ++ } ++ return null; ++ } ++ ChunkCoordIntPair[] strongholdCoords; ++ final java.util.concurrent.atomic.AtomicBoolean ++ strongholdInit = new java.util.concurrent.atomic.AtomicBoolean ++ (false); ++ // Paper end + public boolean allowMonsters; + public boolean allowAnimals; + private boolean J; +@@ -741,17 +759,42 @@ public abstract class World implements IEntityAccess, GeneratorAccess, IIBlockAc + + } + +- public IBlockData getType(BlockPosition blockposition) { +- // CraftBukkit start - tree generation ++ // Paper - async variant ++ public java.util.concurrent.CompletableFuture getTypeAsync(BlockPosition blockposition) { ++ int x = blockposition.getX(); ++ int z = blockposition.getZ(); + if (captureTreeGeneration) { + Iterator it = capturedBlockStates.iterator(); + while (it.hasNext()) { + CraftBlockState previous = it.next(); +- if (previous.getX() == blockposition.getX() && previous.getY() == blockposition.getY() && previous.getZ() == blockposition.getZ()) { +- return previous.getHandle(); ++ if (previous.getX() == x && previous.getY() == blockposition.getY() && previous.getZ() == z) { ++ return java.util.concurrent.CompletableFuture.completedFuture(previous.getHandle()); + } + } + } ++ if (blockposition.isInvalidYLocation()) { ++ return java.util.concurrent.CompletableFuture.completedFuture(Blocks.VOID_AIR.getBlockData()); ++ } else { ++ java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); ++ ((ChunkProviderServer) chunkProvider).getChunkAt(x << 4, z << 4, true, true, (chunk) -> { ++ future.complete(chunk.getType(blockposition)); ++ }); ++ return future; ++ } ++ } ++ // Paper end ++ ++ public IBlockData getType(BlockPosition blockposition) { ++ // CraftBukkit start - tree generation ++ if (captureTreeGeneration) { // If any of this logic updates, update async variant above ++ Iterator it = capturedBlockStates.iterator(); ++ while (it.hasNext()) { // If any of this logic updates, update async variant above ++ CraftBlockState previous = it.next(); ++ if (previous.getX() == blockposition.getX() && previous.getY() == blockposition.getY() && previous.getZ() == blockposition.getZ()) { ++ return previous.getHandle(); // If any of this logic updates, update async variant above ++ } ++ } // If any of this logic updates, update async variant above ++ } + // CraftBukkit end + if (blockposition.isInvalidYLocation()) { // Paper + return Blocks.VOID_AIR.getBlockData(); +@@ -1022,6 +1065,11 @@ public abstract class World implements IEntityAccess, GeneratorAccess, IIBlockAc + } + + public boolean addEntity(Entity entity, SpawnReason spawnReason) { // Changed signature, added SpawnReason ++ // Paper start ++ if (regionLimited != null) { ++ return regionLimited.addEntity(entity, spawnReason); ++ } ++ // Paper end + org.spigotmc.AsyncCatcher.catchOp( "entity add"); // Spigot + if (entity == null) return false; + if (entity.valid) { MinecraftServer.LOGGER.error("Attempted Double World add on " + entity, new Throwable()); return true; } // Paper +diff --git a/src/main/java/net/minecraft/server/WorldGenStronghold.java b/src/main/java/net/minecraft/server/WorldGenStronghold.java +index fa99fe0146..4f49786aa3 100644 +--- a/src/main/java/net/minecraft/server/WorldGenStronghold.java ++++ b/src/main/java/net/minecraft/server/WorldGenStronghold.java +@@ -9,24 +9,29 @@ import java.util.Random; + import javax.annotation.Nullable; + + public class WorldGenStronghold extends StructureGenerator { +- private boolean b; +- private ChunkCoordIntPair[] c; +- private long d; ++ // Paper start - no shared state ++ //private boolean b; ++ //private ChunkCoordIntPair[] c; ++ //private long d; ++ // Paper end + + public WorldGenStronghold() { + } + + protected boolean a(ChunkGenerator chunkgenerator, Random var2, int i, int j) { +- if (this.d != chunkgenerator.getSeed()) { ++ // Paper start ++ /*if (this.d != chunkgenerator.getSeed()) { + this.c(); +- } ++ }*/ + +- if (!this.b) { ++ synchronized (chunkgenerator.getWorld().strongholdInit) { ++ if (chunkgenerator.getWorld().strongholdInit.compareAndSet(false, true)) { // Paper + this.a(chunkgenerator); +- this.b = true; +- } ++ //this.b = true; ++ }} // Paper ++ // Paper end + +- for(ChunkCoordIntPair chunkcoordintpair : this.c) { ++ for(ChunkCoordIntPair chunkcoordintpair : chunkgenerator.getWorld().strongholdCoords) { // Paper + if (i == chunkcoordintpair.x && j == chunkcoordintpair.z) { + return true; + } +@@ -36,8 +41,8 @@ public class WorldGenStronghold extends StructureGenerator chunkgenerator) { +- this.d = chunkgenerator.getSeed(); ++ //this.d = chunkgenerator.getSeed(); // Paper + ArrayList arraylist = Lists.newArrayList(); + + for(BiomeBase biomebase : IRegistry.BIOME) { +@@ -111,7 +119,7 @@ public class WorldGenStronghold extends StructureGenerator= k) { +- this.c[j1] = new ChunkCoordIntPair(k1, l1); ++ strongholdCoords[j1] = new ChunkCoordIntPair(k1, l1); // Paper + } + + d1 += (Math.PI * 2D) / (double)i; +@@ -153,7 +161,7 @@ public class WorldGenStronghold extends StructureGenerator> 4)) {{ ++ int j = coords.x; ++ int k = coords.z; ++ // Paper end ++ + long l = System.currentTimeMillis(); + + if (l < i) { +@@ -1031,7 +1035,7 @@ public final class CraftServer implements Server { + } + + BlockPosition chunkcoordinates = internal.getSpawn(); +- internal.getChunkProviderServer().getChunkAt(chunkcoordinates.getX() + j >> 4, chunkcoordinates.getZ() + k >> 4, true, true); ++ internal.getChunkProviderServer().getChunkAt(chunkcoordinates.getX() + j >> 4, chunkcoordinates.getZ() + k >> 4, true, true, c -> {}); // Paper + } + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 3bd32ef3e9..70b6a59d97 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -162,9 +162,7 @@ public class CraftWorld implements World { + public java.util.concurrent.CompletableFuture getChunkAtAsync(final int x, final int z, final boolean gen) { + final ChunkProviderServer cps = this.world.getChunkProviderServer(); + java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); +- net.minecraft.server.Chunk chunk = cps.getChunkAt(x, z, true, gen); +- // TODO: Add back async variant +- future.complete(chunk != null ? chunk.bukkitChunk : null); ++ cps.getChunkAt(x, z, true, gen, chunk -> future.complete(chunk != null ? chunk.bukkitChunk : null)); + return future; + } + // Paper end +@@ -1344,10 +1342,13 @@ public class CraftWorld implements World { + int chunkCoordZ = chunkcoordinates.getZ() >> 4; + // Cycle through the 25x25 Chunks around it to load/unload the chunks. + int radius = world.paperConfig.keepLoadedRange / 16; // Paper +- for (int x = -radius; x <= radius; x++) { // Paper +- for (int z = -radius; z <= radius; z++) { // Paper ++ // Paper start ++ for (ChunkCoordIntPair coords : world.getChunkProviderServer().getSpiralOutChunks(world.getSpawn(), radius)) {{ ++ int x = coords.x; ++ int z = coords.z; ++ // Paper end + if (keepLoaded) { +- loadChunk(chunkCoordX + x, chunkCoordZ + z); ++ getChunkAtAsync(chunkCoordX + x, chunkCoordZ + z, chunk -> {}); // Paper - Async Chunks + } else { + if (isChunkLoaded(chunkCoordX + x, chunkCoordZ + z)) { + unloadChunk(chunkCoordX + x, chunkCoordZ + z); +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index 12e2c0f6e1..cd6138855e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -77,6 +77,7 @@ public class CraftEventFactory { + public static final DamageSource POISON = CraftDamageSource.copyOf(DamageSource.MAGIC); + public static org.bukkit.block.Block blockDamage; // For use in EntityDamageByBlockEvent + public static Entity entityDamage; // For use in EntityDamageByEntityEvent ++ public static boolean isWorldGen(GeneratorAccess world) { return world instanceof net.minecraft.server.RegionLimitedWorldAccess; } // Paper + + // helper methods + private static boolean canBuild(CraftWorld world, Player player, int x, int z) { +@@ -300,6 +301,7 @@ public class CraftEventFactory { + CraftServer craftServer = (CraftServer) entity.getServer(); + + CreatureSpawnEvent event = new CreatureSpawnEvent(entity, spawnReason); ++ if (isWorldGen(entityliving.world)) return event; // Paper - do not call during world gen + craftServer.getPluginManager().callEvent(event); + return event; + } +@@ -947,6 +949,7 @@ public class CraftEventFactory { + } + + BlockIgniteEvent event = new BlockIgniteEvent(bukkitWorld.getBlockAt(block.getX(), block.getY(), block.getZ()), cause, igniter); ++ if (isWorldGen(world)) return event; // Paper - do not call during world gen + world.getServer().getPluginManager().callEvent(event); + return event; + } +@@ -971,6 +974,7 @@ public class CraftEventFactory { + } + + BlockIgniteEvent event = new BlockIgniteEvent(bukkitWorld.getBlockAt(pos.getX(), pos.getY(), pos.getZ()), cause, bukkitIgniter); ++ if (isWorldGen(world)) return event; // Paper - do not call during world gen + world.getServer().getPluginManager().callEvent(event); + return event; + } +@@ -1178,7 +1182,8 @@ public class CraftEventFactory { + public static BlockPhysicsEvent callBlockPhysicsEvent(GeneratorAccess world, BlockPosition blockposition) { + org.bukkit.block.Block block = CraftBlock.at(world, blockposition); + BlockPhysicsEvent event = new BlockPhysicsEvent(block, block.getBlockData()); +- world.getMinecraftWorld().getMinecraftServer().server.getPluginManager().callEvent(event); ++ if (isWorldGen(world)) return event; // Paper - do not call during world gen ++ Bukkit.getPluginManager().callEvent(event); // Paper + return event; + } + +@@ -1214,6 +1219,7 @@ public class CraftEventFactory { + } + + EntityPotionEffectEvent event = new EntityPotionEffectEvent((LivingEntity) entity.getBukkitEntity(), bukkitOldEffect, bukkitNewEffect, cause, action, willOverride); ++ if (isWorldGen(entity.world)) return event; // Paper - do not call during world gen + Bukkit.getPluginManager().callEvent(event); + + return event; +@@ -1232,6 +1238,7 @@ public class CraftEventFactory { + blockState.setData(block); + + BlockFormEvent event = (entity == null) ? new BlockFormEvent(blockState.getBlock(), blockState) : new EntityBlockFormEvent(entity.getBukkitEntity(), blockState.getBlock(), blockState); ++ if (isWorldGen(world)) return true; // Paper - do not call during world gen + world.getServer().getPluginManager().callEvent(event); + + if (!event.isCancelled()) { +diff --git a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java +index 9c2adb2351..62c197b80d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java ++++ b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java +@@ -21,6 +21,7 @@ public class CustomChunkGenerator extends InternalChunkGenerator