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/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index 04b287c91acef5b9f0b73edb71fbefbb1f08eee6..ebb2b7323b4e11749b7b905cc76863a841a74784 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -90,6 +90,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.blending.BlendingData;
 import net.minecraft.world.level.levelgen.structure.StructureStart;
@@ -1218,10 +1219,59 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
     @Nullable
     public CompoundTag readChunk(ChunkPos pos) throws IOException {
         CompoundTag nbttagcompound = this.read(pos);
+        // Paper start - Cache chunk status on disk
+        if (nbttagcompound == null) {
+            return null;
+        }
+
+        nbttagcompound = this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, this.generator.getTypeNameForDataFixer(), 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.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, this.generator.getTypeNameForDataFixer(), pos, level); // CraftBukkit
+        return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
     }
 
+    public ChunkStatus getChunkStatusOnDisk(ChunkPos chunkPos) throws IOException {
+        RegionFile regionFile = regionFileCache.getRegionFile(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.getRegionFile(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 anyPlayerCloseEnoughForSpawning(ChunkPos pos) {
         // Spigot start
         return this.anyPlayerCloseEnoughForSpawning(pos, false);
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 a7f173c385f7a96010652b92fe14bffa5f1ffc68..954e141e5617d6d52e2f3008c25fe9e2fe2f0f9a 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java
@@ -290,6 +290,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 37a60420b993525852492fd6665fb75afee9796f..9a4fe42da43037a65738b978c0c6c9969de59e76 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
@@ -606,6 +606,17 @@ public class ChunkSerializer {
         }));
     }
 
+    // Paper start
+    public static @Nullable ChunkStatus getStatus(@Nullable CompoundTag compound) {
+        if (compound == null) {
+            return null;
+        }
+
+        // Note: Copied from below
+        return ChunkStatus.getStatus(compound.getString("Status"));
+    }
+    // Paper end
+
     public static ChunkStatus.ChunkType getChunkTypeFromTag(@Nullable CompoundTag nbt) {
         return nbt != null ? ChunkStatus.byName(nbt.getString("Status")).getChunkType() : ChunkStatus.ChunkType.PROTOCHUNK;
     }
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 330fb8e6565a5c0490af0c5ca0e7355d81a82e58..861a25a15f1aab20e3245b6d5cdad5d23bdfd6d0 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
@@ -51,6 +51,30 @@ public class RegionFile implements AutoCloseable {
     public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper
     public final Path regionFile; // Paper
 
+    // Paper start - Cache chunk status
+    private final net.minecraft.world.level.chunk.ChunkStatus[] statuses = new net.minecraft.world.level.chunk.ChunkStatus[32 * 32];
+
+    private boolean closed;
+
+    // invoked on write/read
+    public void setStatus(int x, int z, net.minecraft.world.level.chunk.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 net.minecraft.world.level.chunk.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(Path file, Path directory, boolean dsync) throws IOException {
         this(file, directory, RegionFileVersion.VERSION_DEFLATE, dsync);
     }
@@ -398,6 +422,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;
     }
@@ -408,6 +433,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 a1bfcdd713c47d8613eb4af7625a64d51161690b..4bc33c31d497aa7d69226ab870fd78902bedfd5b 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
@@ -245,6 +245,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 f782147a7523b952b272ba67c7b85164236379ef..0edb08a391f806e56ed1bd4812eb9c9d2b966bd7 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;
@@ -293,8 +294,22 @@ public class CraftWorld extends CraftRegionAccessor 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);
         }
@@ -406,20 +421,48 @@ public class CraftWorld extends CraftRegionAccessor 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.getRegionFile(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
