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/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 412380f4bfe8a2d50090904124242e8b2c7bfa1b..1a21f7e590aaeca131256dd7079b9546710ca9ad 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -729,31 +729,34 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
 
     // CraftBukkit start
     public void prepareLevels(ChunkProgressListener worldloadlistener, ServerLevel worldserver) {
+        ServerChunkCache chunkproviderserver = worldserver.getChunkSource(); // Paper
         // WorldServer worldserver = this.overworld();
         this.forceTicks = true;
         // CraftBukkit end
+        if (worldserver.getWorld().getKeepSpawnInMemory()) { // Paper
 
         MinecraftServer.LOGGER.info("Preparing start region for dimension {}", worldserver.dimension().location());
         BlockPos blockposition = worldserver.getSharedSpawnPos();
 
         worldloadlistener.updateSpawnPos(new ChunkPos(blockposition));
-        ServerChunkCache chunkproviderserver = worldserver.getChunkSource();
+        //ChunkProviderServer chunkproviderserver = worldserver.getChunkProvider(); // Paper - move up
 
         chunkproviderserver.getLightEngine().setTaskPerBatch(500);
         this.nextTickTime = Util.getMillis();
-        // CraftBukkit start
-        if (worldserver.getWorld().getKeepSpawnInMemory()) {
-            chunkproviderserver.addRegionTicket(TicketType.START, new ChunkPos(blockposition), 11, Unit.INSTANCE);
-
-            while (chunkproviderserver.getTickingGenerated() != 441) {
-                // this.nextTickTime = SystemUtils.getMillis() + 10L;
-                this.executeModerately();
-            }
-        }
+        // Paper start - configurable spawn reason
+        int radiusBlocks = worldserver.paperConfig().spawn.keepSpawnLoadedRange * 16;
+        int radiusChunks = radiusBlocks / 16 + ((radiusBlocks & 15) != 0 ? 1 : 0);
+        int totalChunks = ((radiusChunks) * 2 + 1);
+        totalChunks *= totalChunks;
+        worldloadlistener.setChunkRadius(radiusBlocks / 16);
+
+        worldserver.addTicketsForSpawn(radiusBlocks, blockposition);
+        // Paper end
 
         // this.nextTickTime = SystemUtils.getMillis() + 10L;
         this.executeModerately();
         // Iterator iterator = this.levels.values().iterator();
+        }
 
         if (true) {
             ServerLevel worldserver1 = worldserver;
@@ -776,7 +779,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
         // this.nextTickTime = SystemUtils.getMillis() + 10L;
         this.executeModerately();
         // CraftBukkit end
-        worldloadlistener.stop();
+        if (worldserver.getWorld().getKeepSpawnInMemory()) worldloadlistener.stop(); // Paper
         chunkproviderserver.getLightEngine().setTaskPerBatch(5);
         // CraftBukkit start
         // this.updateMobSpawningFlags();
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index 0ba8f25d7f54ec2ed9e15a3cf7016464741c0de0..0c5065ac62d8a708f70282e765277866834169bc 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -64,6 +64,7 @@ import net.minecraft.network.protocol.game.ClientboundSoundEntityPacket;
 import net.minecraft.network.protocol.game.ClientboundSoundPacket;
 import net.minecraft.network.protocol.game.DebugPackets;
 import net.minecraft.resources.ResourceKey;
+import io.papermc.paper.util.MCUtil;
 import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.ServerScoreboard;
 import net.minecraft.server.level.progress.ChunkProgressListener;
@@ -1751,12 +1752,84 @@ public class ServerLevel extends Level implements WorldGenLevel {
         return ((MapIndex) this.getServer().overworld().getDataStorage().computeIfAbsent(MapIndex::load, MapIndex::new, "idcounts")).getFreeAuxValueForMap();
     }
 
+    // Paper start - helper function for configurable spawn radius
+    public void addTicketsForSpawn(int radiusInBlocks, BlockPos 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
+        ServerChunkCache chunkproviderserver = this.getChunkSource();
+        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.addRegionTicket(TicketType.START, new ChunkPos(spawn.offset(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.addRegionTicket(TicketType.START, new ChunkPos(spawn.offset(x, 0, radiusInBlocks)), 1, Unit.INSTANCE); // level 32
+            // bottom
+            chunkproviderserver.addRegionTicket(TicketType.START, new ChunkPos(spawn.offset(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.addRegionTicket(TicketType.START, new ChunkPos(spawn.offset(radiusInBlocks, 0, z)), 1, Unit.INSTANCE); // level 32
+            // left
+            chunkproviderserver.addRegionTicket(TicketType.START, new ChunkPos(spawn.offset(-radiusInBlocks, 0, z)), 1, Unit.INSTANCE); // level 32
+        }
+    }
+    public void removeTicketsForSpawn(int radiusInBlocks, BlockPos 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
+        ServerChunkCache chunkproviderserver = this.getChunkSource();
+        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.removeRegionTicket(TicketType.START, new ChunkPos(spawn.offset(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.removeRegionTicket(TicketType.START, new ChunkPos(spawn.offset(x, 0, radiusInBlocks)), 1, Unit.INSTANCE); // level 32
+            // bottom
+            chunkproviderserver.removeRegionTicket(TicketType.START, new ChunkPos(spawn.offset(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.removeRegionTicket(TicketType.START, new ChunkPos(spawn.offset(radiusInBlocks, 0, z)), 1, Unit.INSTANCE); // level 32
+            // left
+            chunkproviderserver.removeRegionTicket(TicketType.START, new ChunkPos(spawn.offset(-radiusInBlocks, 0, z)), 1, Unit.INSTANCE); // level 32
+        }
+    }
+    // Paper end
+
     public void setDefaultSpawnPos(BlockPos pos, float angle) {
-        ChunkPos chunkcoordintpair = new ChunkPos(new BlockPos(this.levelData.getXSpawn(), 0, this.levelData.getZSpawn()));
+        // Paper - configurable spawn radius
+        BlockPos prevSpawn = this.getSharedSpawnPos();
+        //ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(new BlockPosition(this.worldData.a(), 0, this.worldData.c()));
 
         this.levelData.setSpawn(pos, angle);
-        this.getChunkSource().removeRegionTicket(TicketType.START, chunkcoordintpair, 11, Unit.INSTANCE);
-        this.getChunkSource().addRegionTicket(TicketType.START, new ChunkPos(pos), 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().spawn.keepSpawnLoadedRange * 16, prevSpawn);
+            this.addTicketsForSpawn(this.paperConfig().spawn.keepSpawnLoadedRange * 16, pos);
+        }
         this.getServer().getPlayerList().broadcastAll(new ClientboundSetDefaultSpawnPositionPacket(pos, angle));
     }
 
diff --git a/src/main/java/net/minecraft/server/level/progress/ChunkProgressListener.java b/src/main/java/net/minecraft/server/level/progress/ChunkProgressListener.java
index 1b565b2809c2d367e21971c5154f35c9763995e6..b0f899835ded29aff108d1674bf4a1a6c89693db 100644
--- a/src/main/java/net/minecraft/server/level/progress/ChunkProgressListener.java
+++ b/src/main/java/net/minecraft/server/level/progress/ChunkProgressListener.java
@@ -12,4 +12,6 @@ public interface ChunkProgressListener {
     void start();
 
     void stop();
+
+    void setChunkRadius(int radius); // Paper - allow changing chunk radius
 }
diff --git a/src/main/java/net/minecraft/server/level/progress/LoggerChunkProgressListener.java b/src/main/java/net/minecraft/server/level/progress/LoggerChunkProgressListener.java
index 4d2348df25410a0b5364eec066880326d6667dad..286aad3205ef8a9e21a47ef07893844fe857556a 100644
--- a/src/main/java/net/minecraft/server/level/progress/LoggerChunkProgressListener.java
+++ b/src/main/java/net/minecraft/server/level/progress/LoggerChunkProgressListener.java
@@ -11,12 +11,19 @@ import org.slf4j.Logger;
 
 public class LoggerChunkProgressListener implements ChunkProgressListener {
     private static final Logger LOGGER = LogUtils.getLogger();
-    private final int maxCount;
+    private int maxCount;// Paper - remove final
     private int count;
     private long startTime;
     private long nextTickTime = Long.MAX_VALUE;
 
     public LoggerChunkProgressListener(int radius) {
+        // Paper start - Allow changing radius later for configurable spawn patch
+        this.setChunkRadius(radius); // Move to method
+    }
+
+    @Override
+    public void setChunkRadius(int radius) {
+        // Paper end
         int i = radius * 2 + 1;
         this.maxCount = i * i;
     }
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index ce61a2b5b3dfb15e8aeb6816f6c6b1b9e3a9d725..f3235c80c6eecdeade88ddafaf39c52beadda684 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -1346,15 +1346,21 @@ public class CraftWorld extends CraftRegionAccessor implements World {
 
     @Override
     public void setKeepSpawnInMemory(boolean keepLoaded) {
-        world.keepSpawnInMemory = keepLoaded;
+        // Paper start - Configurable spawn radius
+        if (keepLoaded == world.keepSpawnInMemory) {
+            // do nothing, nothing has changed
+            return;
+        }
+        this.world.keepSpawnInMemory = keepLoaded;
         // Grab the worlds spawn chunk
         BlockPos chunkcoordinates = this.world.getSharedSpawnPos();
         if (keepLoaded) {
-            this.world.getChunkSource().addRegionTicket(TicketType.START, new ChunkPos(chunkcoordinates), 11, Unit.INSTANCE);
+            this.world.addTicketsForSpawn(this.world.paperConfig().spawn.keepSpawnLoadedRange * 16, chunkcoordinates);
         } else {
-            // TODO: doesn't work well if spawn changed....
-            this.world.getChunkSource().removeRegionTicket(TicketType.START, new ChunkPos(chunkcoordinates), 11, Unit.INSTANCE);
+            // TODO: doesn't work well if spawn changed.... // Paper - resolved
+            this.world.removeTicketsForSpawn(this.world.paperConfig().spawn.keepSpawnLoadedRange * 16, chunkcoordinates);
         }
+        // Paper end
     }
 
     @Override