From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Sat, 13 Sep 2014 23:14:43 -0400
Subject: [PATCH] Configurable Keep Spawn Loaded range per world

This lets you disable it for some worlds and lower it for others.

diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
index a9a3dbbe7608d1f0dc122fe8d49928e7e3fa1438..e9c03546c42657dd5f5d4c6f71bd7e0cc7e2cb15 100644
--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
@@ -434,4 +434,10 @@ public class PaperWorldConfig {
                 break;
         }
     }
+
+    public short keepLoadedRange;
+    private void keepLoadedRange() {
+        keepLoadedRange = (short) (getInt("keep-spawn-loaded-range", Math.min(spigotConfig.viewDistance, 10)) * 16);
+        log( "Keep Spawn Loaded Range: " + (keepLoadedRange/16));
+    }
 }
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 44991803a489a42842f79ce51bdd53a73ef35d71..12866eec621d9ae8801d50bdda13e757f303010e 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -605,6 +605,14 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
         this.forceTicks = true;
         // CraftBukkit end
 
+        // Paper start - configurable spawn reason
+        int radiusBlocks = worldserver.paperConfig.keepLoadedRange;
+        int radiusChunks = radiusBlocks / 16 + ((radiusBlocks & 15) != 0 ? 1 : 0);
+        int totalChunks = ((radiusChunks) * 2 + 1);
+        totalChunks *= totalChunks;
+        worldloadlistener.setChunkRadius(radiusBlocks / 16);
+        // Paper end
+
         MinecraftServer.LOGGER.info("Preparing start region for dimension {}", worldserver.getDimensionKey().a());
         BlockPosition blockposition = worldserver.getSpawn();
 
@@ -613,14 +621,12 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
 
         chunkproviderserver.getLightEngine().a(500);
         this.nextTick = SystemUtils.getMonotonicMillis();
-        chunkproviderserver.addTicket(TicketType.START, new ChunkCoordIntPair(blockposition), 11, Unit.INSTANCE);
-
-        while (chunkproviderserver.b() != 441) {
-            // CraftBukkit start
-            // this.nextTick = SystemUtils.getMonotonicMillis() + 10L;
-            this.executeModerately();
-            // CraftBukkit end
+        // Paper start - Configurable spawn radius
+        if (worldserver.keepSpawnInMemory) {
+            worldserver.addTicketsForSpawn(radiusBlocks, blockposition);
         }
+        // Paper end
+        LOGGER.info("Loaded " + chunkproviderserver.b() + " spawn chunks for world " + worldserver.getWorld().getName()); // Paper
 
         // CraftBukkit start
         // this.nextTick = SystemUtils.getMonotonicMillis() + 10L;
diff --git a/src/main/java/net/minecraft/server/WorldLoadListener.java b/src/main/java/net/minecraft/server/WorldLoadListener.java
index d6762d3853b55b639047f455351150a1cbc9399a..7b6f5b2da0a76661a0e047ee7002aa07cdd4a8b1 100644
--- a/src/main/java/net/minecraft/server/WorldLoadListener.java
+++ b/src/main/java/net/minecraft/server/WorldLoadListener.java
@@ -9,4 +9,6 @@ public interface WorldLoadListener {
     void a(ChunkCoordIntPair chunkcoordintpair, @Nullable ChunkStatus chunkstatus);
 
     void b();
+
+    void setChunkRadius(int radius); // Paper - allow changing chunk radius
 }
diff --git a/src/main/java/net/minecraft/server/WorldLoadListenerLogger.java b/src/main/java/net/minecraft/server/WorldLoadListenerLogger.java
index 3868572aed50c8bffd93727a139a3fbb8dc19688..ae77805f71c6c574d92f39c51b1e48f2138e9ab6 100644
--- a/src/main/java/net/minecraft/server/WorldLoadListenerLogger.java
+++ b/src/main/java/net/minecraft/server/WorldLoadListenerLogger.java
@@ -7,16 +7,24 @@ import org.apache.logging.log4j.Logger;
 public class WorldLoadListenerLogger implements WorldLoadListener {
 
     private static final Logger LOGGER = LogManager.getLogger();
-    private final int b;
+    private int b; // Paper - remove final
     private int c;
     private long d;
     private long e = Long.MAX_VALUE;
 
     public WorldLoadListenerLogger(int i) {
-        int j = i * 2 + 1;
+        // Paper start - Allow changing radius later for configurable spawn patch
+        this.setChunkRadius(i); // Move to method
+    }
+
+    @Override
+    public void setChunkRadius(int radius) {
+        // Paper - copied from above
+        int j = radius * 2 + 1;
 
         this.b = j * j;
     }
+    // Paper end
 
     @Override
     public void a(ChunkCoordIntPair chunkcoordintpair) {
diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java
index de2bdd55213613f439cd797bfe01986c8851eaa4..644167d400e0f4af142f0afe6a11cae4ac591ddf 100644
--- a/src/main/java/net/minecraft/server/WorldServer.java
+++ b/src/main/java/net/minecraft/server/WorldServer.java
@@ -1552,12 +1552,88 @@ public class WorldServer extends World implements GeneratorAccessSeed {
         return ((PersistentIdCounts) this.getMinecraftServer().E().getWorldPersistentData().a(PersistentIdCounts::new, "idcounts")).a();
     }
 
+    // Paper start - helper function for configurable spawn radius
+    public void addTicketsForSpawn(int radiusInBlocks, BlockPosition spawn) {
+        // In order to respect vanilla behavior, which is ensuring everything but the spawn border can tick, we add tickets
+        // with level 31 for the non-border spawn chunks
+        ChunkProviderServer chunkproviderserver = this.getChunkProvider();
+        int tickRadius = radiusInBlocks - 16;
+
+        // add ticking chunks
+        for (int x = -tickRadius; x <= tickRadius; x += 16) {
+            for (int z = -tickRadius; z <= tickRadius; z += 16) {
+                // radius of 2 will have the current chunk be level 31
+                chunkproviderserver.addTicket(TicketType.START, new ChunkCoordIntPair(spawn.add(x, 0, z)), 2, Unit.INSTANCE);
+            }
+        }
+
+        // add border chunks
+
+        // add border along x axis (including corner chunks)
+        for (int x = -radiusInBlocks; x <= radiusInBlocks; x += 16) {
+            // top
+            chunkproviderserver.addTicket(TicketType.START, new ChunkCoordIntPair(spawn.add(x, 0, radiusInBlocks)), 1, Unit.INSTANCE); // level 32
+            // bottom
+            chunkproviderserver.addTicket(TicketType.START, new ChunkCoordIntPair(spawn.add(x, 0, -radiusInBlocks)), 1, Unit.INSTANCE); // level 32
+        }
+
+        // add border along z axis (excluding corner chunks)
+        for (int z = -radiusInBlocks + 16; z < radiusInBlocks; z += 16) {
+            // right
+            chunkproviderserver.addTicket(TicketType.START, new ChunkCoordIntPair(spawn.add(radiusInBlocks, 0, z)), 1, Unit.INSTANCE); // level 32
+            // left
+            chunkproviderserver.addTicket(TicketType.START, new ChunkCoordIntPair(spawn.add(-radiusInBlocks, 0, z)), 1, Unit.INSTANCE); // level 32
+        }
+
+        MCUtil.getSpiralOutChunks(spawn, radiusInBlocks >> 4).forEach(pair -> {
+            getChunkProvider().getChunkAtMainThread(pair.x, pair.z);
+        });
+    }
+    public void removeTicketsForSpawn(int radiusInBlocks, BlockPosition spawn) {
+        // In order to respect vanilla behavior, which is ensuring everything but the spawn border can tick, we added tickets
+        // with level 31 for the non-border spawn chunks
+        ChunkProviderServer chunkproviderserver = this.getChunkProvider();
+        int tickRadius = radiusInBlocks - 16;
+
+        // remove ticking chunks
+        for (int x = -tickRadius; x <= tickRadius; x += 16) {
+            for (int z = -tickRadius; z <= tickRadius; z += 16) {
+                // radius of 2 will have the current chunk be level 31
+                chunkproviderserver.removeTicket(TicketType.START, new ChunkCoordIntPair(spawn.add(x, 0, z)), 2, Unit.INSTANCE);
+            }
+        }
+
+        // remove border chunks
+
+        // remove border along x axis (including corner chunks)
+        for (int x = -radiusInBlocks; x <= radiusInBlocks; x += 16) {
+            // top
+            chunkproviderserver.removeTicket(TicketType.START, new ChunkCoordIntPair(spawn.add(x, 0, radiusInBlocks)), 1, Unit.INSTANCE); // level 32
+            // bottom
+            chunkproviderserver.removeTicket(TicketType.START, new ChunkCoordIntPair(spawn.add(x, 0, -radiusInBlocks)), 1, Unit.INSTANCE); // level 32
+        }
+
+        // remove border along z axis (excluding corner chunks)
+        for (int z = -radiusInBlocks + 16; z < radiusInBlocks; z += 16) {
+            // right
+            chunkproviderserver.removeTicket(TicketType.START, new ChunkCoordIntPair(spawn.add(radiusInBlocks, 0, z)), 1, Unit.INSTANCE); // level 32
+            // left
+            chunkproviderserver.removeTicket(TicketType.START, new ChunkCoordIntPair(spawn.add(-radiusInBlocks, 0, z)), 1, Unit.INSTANCE); // level 32
+        }
+    }
+    // Paper end
+
     public void a(BlockPosition blockposition, float f) {
-        ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(new BlockPosition(this.worldData.a(), 0, this.worldData.c()));
+        // Paper - configurable spawn radius
+        BlockPosition prevSpawn = this.getSpawn();
+        //ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(new BlockPosition(this.worldData.a(), 0, this.worldData.c()));
 
         this.worldData.setSpawn(blockposition, f);
-        this.getChunkProvider().removeTicket(TicketType.START, chunkcoordintpair, 11, Unit.INSTANCE);
-        this.getChunkProvider().addTicket(TicketType.START, new ChunkCoordIntPair(blockposition), 11, Unit.INSTANCE);
+        if (this.keepSpawnInMemory) {
+            // if this keepSpawnInMemory is false a plugin has already removed our tickets, do not re-add
+            this.removeTicketsForSpawn(this.paperConfig.keepLoadedRange, prevSpawn);
+            this.addTicketsForSpawn(this.paperConfig.keepLoadedRange, blockposition);
+        }
         this.getMinecraftServer().getPlayerList().sendAll(new PacketPlayOutSpawnPosition(blockposition, f));
     }
 
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index 2dd749a375aaa2e89ffdafe986ef331234086929..7287f595c89e0548252a2c39464d4a06d7f71bb7 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -1954,15 +1954,21 @@ public class CraftWorld implements World {
 
     @Override
     public void setKeepSpawnInMemory(boolean keepLoaded) {
+        // Paper start - Configurable spawn radius
+        if (keepLoaded == world.keepSpawnInMemory) {
+            // do nothing, nothing has changed
+            return;
+        }
         world.keepSpawnInMemory = keepLoaded;
         // Grab the worlds spawn chunk
-        BlockPosition chunkcoordinates = this.world.getSpawn();
+        BlockPosition prevSpawn = this.world.getSpawn();
         if (keepLoaded) {
-            world.getChunkProvider().addTicket(TicketType.START, new ChunkCoordIntPair(chunkcoordinates), 11, Unit.INSTANCE);
+            world.addTicketsForSpawn(world.paperConfig.keepLoadedRange, prevSpawn);
         } else {
-            // TODO: doesn't work well if spawn changed....
-            world.getChunkProvider().removeTicket(TicketType.START, new ChunkCoordIntPair(chunkcoordinates), 11, Unit.INSTANCE);
+            // TODO: doesn't work well if spawn changed.... // paper - resolved
+            world.removeTicketsForSpawn(world.paperConfig.keepLoadedRange, prevSpawn);
         }
+        // Paper end
     }
 
     @Override
