From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Sat, 15 Jun 2019 08:54:33 -0700
Subject: [PATCH] Fix World#isChunkGenerated calls

Optimize World#loadChunk() too
This patch also adds a chunk status cache on region files (note that
its only purpose is to cache the status on DISK)

diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
index 7af33e8c470e499b7ec47467cce7df699c96873a..2720f4a51ffc8c9748932be55cebd5072cd4ffb2 100644
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -110,6 +110,19 @@ public class ChunkHolder {
         Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure> either = (Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>) statusFuture.getNow(null);
         return (either == null) ? null : (LevelChunk) either.left().orElse(null);
     }
+
+    public ChunkAccess getAvailableChunkNow() {
+        // TODO can we just getStatusFuture(EMPTY)?
+        for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) {
+            CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> future = this.getFutureIfPresentUnchecked(curr);
+            Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure> either = future.getNow(null);
+            if (either == null || !either.left().isPresent()) {
+                continue;
+            }
+            return either.left().get();
+        }
+        return null;
+    }
     // CraftBukkit end
 
     public CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> getFutureIfPresentUnchecked(ChunkStatus leastStatus) {
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index 6d024db8bfbd5139d4c94be3d3a48cfa1a240c62..6f5e1f7c23b19257c89b7c5a992ad76623bf4006 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -84,6 +84,7 @@ import net.minecraft.world.level.chunk.ProtoChunk;
 import net.minecraft.world.level.chunk.UpgradeData;
 import net.minecraft.world.level.chunk.storage.ChunkSerializer;
 import net.minecraft.world.level.chunk.storage.ChunkStorage;
+import net.minecraft.world.level.chunk.storage.RegionFile;
 import net.minecraft.world.level.entity.ChunkStatusUpdateListener;
 import net.minecraft.world.level.levelgen.structure.StructureStart;
 import net.minecraft.world.level.levelgen.structure.templatesystem.StructureManager;
@@ -1082,12 +1083,61 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
     // Paper end
 
     @Nullable
-    private CompoundTag readChunk(ChunkPos pos) throws IOException {
+    public CompoundTag readChunk(ChunkPos pos) throws IOException { // Paper - private -> public
         CompoundTag nbttagcompound = this.read(pos);
+        // Paper start - Cache chunk status on disk
+        if (nbttagcompound == null) {
+            return null;
+        }
+
+        nbttagcompound = this.getChunkData(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, pos, level); // CraftBukkit
+        if (nbttagcompound == null) {
+            return null;
+        }
+
+        this.updateChunkStatusOnDisk(pos, nbttagcompound);
+
+        return nbttagcompound;
+        // Paper end
+    }
+
+    // Paper start - chunk status cache "api"
+    public ChunkStatus getChunkStatusOnDiskIfCached(ChunkPos chunkPos) {
+        RegionFile regionFile = regionFileCache.getRegionFileIfLoaded(chunkPos);
 
-        return nbttagcompound == null ? null : this.getChunkData(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, pos, level); // CraftBukkit
+        return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
     }
 
+    public ChunkStatus getChunkStatusOnDisk(ChunkPos chunkPos) throws IOException {
+        RegionFile regionFile = regionFileCache.getFile(chunkPos, true);
+
+        if (regionFile == null || !regionFileCache.chunkExists(chunkPos)) {
+            return null;
+        }
+
+        ChunkStatus status = regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
+
+        if (status != null) {
+            return status;
+        }
+
+        this.readChunk(chunkPos);
+
+        return regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
+    }
+
+    public void updateChunkStatusOnDisk(ChunkPos chunkPos, @Nullable CompoundTag compound) throws IOException {
+        RegionFile regionFile = regionFileCache.getFile(chunkPos, false);
+
+        regionFile.setStatus(chunkPos.x, chunkPos.z, ChunkSerializer.getStatus(compound));
+    }
+
+    public ChunkAccess getUnloadingChunk(int chunkX, int chunkZ) {
+        ChunkHolder chunkHolder = this.pendingUnloads.get(ChunkPos.asLong(chunkX, chunkZ));
+        return chunkHolder == null ? null : chunkHolder.getAvailableChunkNow();
+    }
+    // Paper end
+
     boolean noPlayersCloseForSpawning(ChunkPos chunkPos) {
         // Spigot start
         return this.isOutsideOfRange(chunkPos, false);
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
index c0bde909755190603e9b4946fe924ba1cac53072..6dc28903dfefd6b454252f70763ce2b93773b75e 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -323,6 +323,7 @@ public class ServerChunkCache extends ChunkSource {
     }
     // Paper end
     // Paper start - async chunk io
+    @Nullable
     public ChunkAccess getChunkAtImmediately(int x, int z) {
         ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
         if (holder == null) {
diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java
index 6e0cf8ee76143301c939fc4af5eeb091abdcbc5c..1c7b18db0053bca6e7750225a79f7d95843edabe 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java
@@ -224,6 +224,17 @@ public class ChunkStatus {
         return this.chunkType;
     }
 
+    // Paper start
+    public static ChunkStatus getStatus(String name) {
+        try {
+            // We need this otherwise we return EMPTY for invalid names
+            ResourceLocation key = new ResourceLocation(name);
+            return Registry.CHUNK_STATUS.getOptional(key).orElse(null);
+        } catch (Exception ex) {
+            return null; // invalid name
+        }
+    }
+    // Paper end
     public static ChunkStatus byName(String id) {
         return (ChunkStatus) Registry.CHUNK_STATUS.get(ResourceLocation.tryParse(id));
     }
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
index 9e9ffb575c68c7044ab289e1f3f79ff821b30c24..ea3279113358b41281ee2e92f2371a0f1e36b472 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
@@ -563,6 +563,17 @@ public class ChunkSerializer {
         return nbttagcompound;
     }
 
+    // Paper start
+    public static ChunkStatus getStatus(CompoundTag compound) {
+        if (compound == null) {
+            return null;
+        }
+
+        // Note: Copied from below
+        return ChunkStatus.getStatus(compound.getCompound("Level").getString("Status"));
+    }
+    // Paper end
+
     public static ChunkStatus.ChunkType getChunkTypeFromTag(@Nullable CompoundTag nbt) {
         if (nbt != null) {
             ChunkStatus chunkstatus = ChunkStatus.byName(nbt.getCompound("Level").getString("Status"));
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
index 46226dd2d16a9f4017661712fe2bfc0c46f63cb2..c22391a0d4b7db49bd3994b0887939a7d8019391 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
@@ -27,6 +27,7 @@ import net.minecraft.Util;
 import net.minecraft.nbt.CompoundTag;
 import net.minecraft.nbt.NbtIo;
 import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.ChunkStatus;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -54,6 +55,30 @@ public class RegionFile implements AutoCloseable {
     public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper
     public final File regionFile; // Paper
 
+    // Paper start - Cache chunk status
+    private final ChunkStatus[] statuses = new ChunkStatus[32 * 32];
+
+    private boolean closed;
+
+    // invoked on write/read
+    public void setStatus(int x, int z, ChunkStatus status) {
+        if (this.closed) {
+            // We've used an invalid region file.
+            throw new IllegalStateException("RegionFile is closed");
+        }
+        this.statuses[getChunkLocation(x, z)] = status;
+    }
+
+    public ChunkStatus getStatusIfCached(int x, int z) {
+        if (this.closed) {
+            // We've used an invalid region file.
+            throw new IllegalStateException("RegionFile is closed");
+        }
+        final int location = getChunkLocation(x, z);
+        return this.statuses[location];
+    }
+    // Paper end
+
     public RegionFile(File file, File directory, boolean dsync) throws IOException {
         this(file.toPath(), directory.toPath(), RegionFileVersion.VERSION_DEFLATE, dsync);
     }
@@ -401,6 +426,7 @@ public class RegionFile implements AutoCloseable {
         return this.getOffset(pos) != 0;
     }
 
+    private static int getChunkLocation(int x, int z) { return (x & 31) + (z & 31) * 32; } // Paper - OBFHELPER - sort of, mirror of logic below
     private static int getOffsetIndex(ChunkPos pos) {
         return pos.getRegionLocalX() + pos.getRegionLocalZ() * 32;
     }
@@ -411,6 +437,7 @@ public class RegionFile implements AutoCloseable {
         synchronized (this) {
         try {
         // Paper end
+        this.closed = true; // Paper
         try {
             this.padToFullSector();
         } finally {
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
index 24092d3d3d234b6f1f2b90e22d90f297532358cc..43510774d489bfdd30f10d521e424fa1363b8919 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
@@ -219,6 +219,7 @@ public class RegionFileStorage implements AutoCloseable {
 
             try {
                 NbtIo.write(nbt, (DataOutput) dataoutputstream);
+                regionfile.setStatus(pos.x, pos.z, ChunkSerializer.getStatus(nbt)); // Paper - cache status on disk
                 regionfile.setOversized(pos.x, pos.z, false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone
             } catch (Throwable throwable) {
                 if (dataoutputstream != null) {
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index c6373fe0361bd5c77036d36ac8ebe66408335b0c..479693f9703f743c8e64bb6b949b2803109f4fa0 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -20,6 +20,7 @@ import java.util.Objects;
 import java.util.Random;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import net.minecraft.core.BlockPos;
@@ -419,8 +420,22 @@ public class CraftWorld implements World {
 
     @Override
     public boolean isChunkGenerated(int x, int z) {
+        // Paper start - Fix this method
+        if (!Bukkit.isPrimaryThread()) {
+            return CompletableFuture.supplyAsync(() -> {
+                return CraftWorld.this.isChunkGenerated(x, z);
+            }, world.getChunkSource().mainThreadProcessor).join();
+        }
+        ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z);
+        if (chunk == null) {
+            chunk = world.getChunkSource().chunkMap.getUnloadingChunk(x, z);
+        }
+        if (chunk != null) {
+            return chunk instanceof ImposterProtoChunk || chunk instanceof net.minecraft.world.level.chunk.LevelChunk;
+        }
         try {
-            return this.world.getChunkSource().getChunkAtIfCachedImmediately(x, z) != null || this.world.getChunkSource().chunkMap.read(new ChunkPos(x, z)) != null; // Paper (TODO check if the first part can be removed)
+            return world.getChunkSource().chunkMap.getChunkStatusOnDisk(new ChunkPos(x, z)) == ChunkStatus.FULL;
+            // Paper end
         } catch (IOException ex) {
             throw new RuntimeException(ex);
         }
@@ -531,20 +546,48 @@ public class CraftWorld implements World {
     @Override
     public boolean loadChunk(int x, int z, boolean generate) {
         org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot
-        ChunkAccess chunk = this.world.getChunkSource().getChunk(x, z, generate || isChunkGenerated(x, z) ? ChunkStatus.FULL : ChunkStatus.EMPTY, true); // Paper
+        // Paper start - Optimize this method
+        ChunkPos chunkPos = new ChunkPos(x, z);
 
-        // If generate = false, but the chunk already exists, we will get this back.
-        if (chunk instanceof ImposterProtoChunk) {
-            // We then cycle through again to get the full chunk immediately, rather than after the ticket addition
-            chunk = this.world.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true);
-        }
+        if (!generate) {
+            ChunkAccess immediate = world.getChunkSource().getChunkAtImmediately(x, z);
+            if (immediate == null) {
+                immediate = world.getChunkSource().chunkMap.getUnloadingChunk(x, z);
+            }
+            if (immediate != null) {
+                if (!(immediate instanceof ImposterProtoChunk) && !(immediate instanceof net.minecraft.world.level.chunk.LevelChunk)) {
+                    return false; // not full status
+                }
+                world.getChunkSource().addRegionTicket(TicketType.PLUGIN, chunkPos, 1, Unit.INSTANCE);
+                world.getChunk(x, z); // make sure we're at ticket level 32 or lower
+                return true;
+            }
 
-        if (chunk instanceof net.minecraft.world.level.chunk.LevelChunk) {
-            this.world.getChunkSource().addRegionTicket(TicketType.PLUGIN, new ChunkPos(x, z), 1, Unit.INSTANCE);
-            return true;
+            net.minecraft.world.level.chunk.storage.RegionFile file;
+            try {
+                file = world.getChunkSource().chunkMap.regionFileCache.getFile(chunkPos, false);
+            } catch (IOException ex) {
+                throw new RuntimeException(ex);
+            }
+
+            ChunkStatus status = file.getStatusIfCached(x, z);
+            if (!file.hasChunk(chunkPos) || (status != null && status != ChunkStatus.FULL)) {
+                return false;
+            }
+
+            ChunkAccess chunk = world.getChunkSource().getChunk(x, z, ChunkStatus.EMPTY, true);
+            if (!(chunk instanceof ImposterProtoChunk) && !(chunk instanceof net.minecraft.world.level.chunk.LevelChunk)) {
+                return false;
+            }
+
+            // fall through to load
+            // we do this so we do not re-read the chunk data on disk
         }
 
-        return false;
+        world.getChunkSource().addRegionTicket(TicketType.PLUGIN, chunkPos, 1, Unit.INSTANCE);
+        world.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true);
+        return true;
+        // Paper end
     }
 
     @Override