From 6cb2a55e24461888eea0f4582d8508afc0faf50f Mon Sep 17 00:00:00 2001
From: Jake Potrebic <jake.m.potrebic@gmail.com>
Date: Sun, 13 Jun 2021 22:34:40 -0700
Subject: [PATCH] Add back incremental chunk saving patch

---
 patches/server/incremental-chunk-saving.patch | 334 ++++++++++++++++++
 1 file changed, 334 insertions(+)
 create mode 100644 patches/server/incremental-chunk-saving.patch

diff --git a/patches/server/incremental-chunk-saving.patch b/patches/server/incremental-chunk-saving.patch
new file mode 100644
index 0000000000..ab856140f3
--- /dev/null
+++ b/patches/server/incremental-chunk-saving.patch
@@ -0,0 +1,334 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Shane Freeder <theboyetronic@gmail.com>
+Date: Sun, 9 Jun 2019 03:53:22 +0100
+Subject: [PATCH] incremental chunk saving
+
+
+diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
++++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+@@ -0,0 +0,0 @@ public class PaperWorldConfig {
+         log( "Keep Spawn Loaded Range: " + (keepLoadedRange/16));
+     }
+ 
++    public int autoSavePeriod = -1;
++    private void autoSavePeriod() {
++        autoSavePeriod = getInt("auto-save-interval", -1);
++        if (autoSavePeriod > 0) {
++            log("Auto Save Interval: " +autoSavePeriod + " (" + (autoSavePeriod / 20) + "s)");
++        } else if (autoSavePeriod < 0) {
++            autoSavePeriod = net.minecraft.server.MinecraftServer.getServer().autosavePeriod;
++        }
++    }
++
++    public int maxAutoSaveChunksPerTick = 24;
++    private void maxAutoSaveChunksPerTick() {
++        maxAutoSaveChunksPerTick = getInt("max-auto-save-chunks-per-tick", 24);
++    }
++
+     private boolean getBoolean(String path, boolean def) {
+         config.addDefault("world-settings.default." + path, def);
+         return config.getBoolean("world-settings." + worldName + "." + path, config.getBoolean("world-settings.default." + path));
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+     public static int currentTick = 0; // Paper - Further improve tick loop
+     public java.util.Queue<Runnable> processQueue = new java.util.concurrent.ConcurrentLinkedQueue<Runnable>();
+     public int autosavePeriod;
++    public boolean serverAutoSave = false; // Paper
+     public Commands vanillaCommandDispatcher;
+     public boolean forceTicks; // Paper
+     // CraftBukkit end
+@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+             this.status.getPlayers().setSample(agameprofile);
+         }
+ 
+-        if (this.autosavePeriod > 0 && this.tickCount % this.autosavePeriod == 0) { // CraftBukkit
+-            MinecraftServer.LOGGER.debug("Autosave started");
++        // if (this.autosavePeriod > 0 && this.tickCount % this.autosavePeriod == 0) { // CraftBukkit // Paper - move down
++        //     MinecraftServer.LOGGER.debug("Autosave started"); // Paper
++        serverAutoSave = (autosavePeriod > 0 && this.tickCount % autosavePeriod == 0); // Paper
+             this.profiler.push("save");
++        if (this.autosavePeriod > 0 && this.tickCount % this.autosavePeriod == 0) { // Paper - moved from above
+             this.playerList.saveAll();
+-            this.saveAllChunks(true, false, false);
+-            this.profiler.pop();
+-            MinecraftServer.LOGGER.debug("Autosave finished");
++            // this.saveAllChunks(true, false, false); // Paper - saved incrementally below
++        } // Paper start
++        for (ServerLevel level : this.getAllLevels()) {
++            if (level.paperConfig.autoSavePeriod > 0) {
++                level.saveIncrementally(this.serverAutoSave);
++            }
+         }
++        // Paper end
++            this.profiler.pop();
++            // MinecraftServer.LOGGER.debug("Autosave finished"); // Paper
++        //} // Paper
+ 
+         this.profiler.push("snooper");
+         if (((DedicatedServer) this).getProperties().snooperEnabled && !this.snooper.isStarted() && this.tickCount > 100) { // Spigot
+diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+@@ -0,0 +0,0 @@ public class ChunkHolder {
+         this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key);
+     }
+     // Paper end - optimise isOutsideOfRange
++    long lastAutoSaveTime; // Paper - incremental autosave
++    long inactiveTimeStart; // Paper - incremental autosave
+ 
+     public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) {
+         this.futures = new AtomicReferenceArray(ChunkHolder.CHUNK_STATUSES.size());
+@@ -0,0 +0,0 @@ public class ChunkHolder {
+         boolean flag2 = playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER);
+         boolean flag3 = playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER);
+ 
++        boolean prevHasBeenLoaded = this.wasAccessibleSinceLastSave; // Paper
+         this.wasAccessibleSinceLastSave |= flag3;
++        // Paper start - incremental autosave
++        if (this.wasAccessibleSinceLastSave & !prevHasBeenLoaded) {
++            long timeSinceAutoSave = this.inactiveTimeStart - this.lastAutoSaveTime;
++            if (timeSinceAutoSave < 0) {
++                // safest bet is to assume autosave is needed here
++                timeSinceAutoSave = this.chunkMap.level.paperConfig.autoSavePeriod;
++            }
++            this.lastAutoSaveTime = this.chunkMap.level.getGameTime() - timeSinceAutoSave;
++            this.chunkMap.autoSaveQueue.add(this);
++        }
++        // Paper end
+         if (!flag2 && flag3) {
+             int expectCreateCount = ++this.fullChunkCreateCount; // Paper
+             this.fullChunkFuture = chunkStorage.prepareAccessibleChunk(this);
+@@ -0,0 +0,0 @@ public class ChunkHolder {
+     }
+ 
+     public void refreshAccessibility() {
++        boolean prev = this.wasAccessibleSinceLastSave; // Paper
+         this.wasAccessibleSinceLastSave = ChunkHolder.getFullChunkStatus(this.ticketLevel).isOrAfter(ChunkHolder.FullChunkStatus.BORDER);
++        // Paper start - incremental autosave
++        if (prev != this.wasAccessibleSinceLastSave) {
++            if (this.wasAccessibleSinceLastSave) {
++                long timeSinceAutoSave = this.inactiveTimeStart - this.lastAutoSaveTime;
++                if (timeSinceAutoSave < 0) {
++                    // safest bet is to assume autosave is needed here
++                    timeSinceAutoSave = this.chunkMap.level.paperConfig.autoSavePeriod;
++                }
++                this.lastAutoSaveTime = this.chunkMap.level.getGameTime() - timeSinceAutoSave;
++                this.chunkMap.autoSaveQueue.add(this);
++            } else {
++                this.inactiveTimeStart = this.chunkMap.level.getGameTime();
++                this.chunkMap.autoSaveQueue.remove(this);
++            }
++        }
++        // Paper end
+     }
+ 
++    // Paper start - incremental autosave
++    public boolean setHasBeenLoaded() {
++        this.wasAccessibleSinceLastSave = getFullChunkStatus(this.ticketLevel).isOrAfter(ChunkHolder.FullChunkStatus.BORDER);
++        return this.wasAccessibleSinceLastSave;
++    }
++    // Paper end
++
+     public void replaceProtoChunk(ImposterProtoChunk chunk) {
+         for (int i = 0; i < this.futures.length(); ++i) {
+             CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completablefuture = (CompletableFuture) this.futures.get(i);
+diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
+@@ -0,0 +0,0 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureMana
+ import net.minecraft.world.level.storage.DimensionDataStorage;
+ import net.minecraft.world.level.storage.LevelStorageSource;
+ import net.minecraft.world.phys.Vec3;
++import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; // Paper
+ import org.apache.commons.lang3.mutable.MutableBoolean;
+ import org.apache.logging.log4j.LogManager;
+ import org.apache.logging.log4j.Logger;
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ 
+     }
+ 
++    // Paper start - incremental autosave
++    final ObjectRBTreeSet<ChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((playerchunk1, playerchunk2) -> {
++        int timeCompare =  Long.compare(playerchunk1.lastAutoSaveTime, playerchunk2.lastAutoSaveTime);
++        if (timeCompare != 0) {
++            return timeCompare;
++        }
++
++        return Long.compare(MCUtil.getCoordinateKey(playerchunk1.pos), MCUtil.getCoordinateKey(playerchunk2.pos));
++    });
++
++    protected void saveIncrementally() {
++        int savedThisTick = 0;
++        // optimized since we search far less chunks to hit ones that need to be saved
++        List<ChunkHolder> reschedule = new java.util.ArrayList<>(this.level.paperConfig.maxAutoSaveChunksPerTick);
++        long currentTick = this.level.getGameTime();
++        long maxSaveTime = currentTick - this.level.paperConfig.autoSavePeriod;
++
++        for (Iterator<ChunkHolder> iterator = this.autoSaveQueue.iterator(); iterator.hasNext();) {
++            ChunkHolder playerchunk = iterator.next();
++            if (playerchunk.lastAutoSaveTime > maxSaveTime) {
++                break;
++            }
++
++            iterator.remove();
++
++            ChunkAccess ichunkaccess = playerchunk.getChunkToSave().getNow(null);
++            if (ichunkaccess instanceof LevelChunk) {
++                boolean shouldSave = ((LevelChunk)ichunkaccess).lastSaveTime <= maxSaveTime;
++
++                if (shouldSave && this.save(ichunkaccess)) {
++                    ++savedThisTick;
++
++                    if (!playerchunk.setHasBeenLoaded()) {
++                        // do not fall through to reschedule logic
++                        playerchunk.inactiveTimeStart = currentTick;
++                        if (savedThisTick >= this.level.paperConfig.maxAutoSaveChunksPerTick) {
++                            break;
++                        }
++                        continue;
++                    }
++                }
++            }
++
++            reschedule.add(playerchunk);
++
++            if (savedThisTick >= this.level.paperConfig.maxAutoSaveChunksPerTick) {
++                break;
++            }
++        }
++
++        for (int i = 0, len = reschedule.size(); i < len; ++i) {
++            ChunkHolder playerchunk = reschedule.get(i);
++            playerchunk.lastAutoSaveTime = this.level.getGameTime();
++            this.autoSaveQueue.add(playerchunk);
++        }
++    }
++    // Paper end
++
+     protected void saveAllChunks(boolean flush) {
+         Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunks = this.getVisibleChunks(); // Paper remove clone of visible Chunks unless saving off main thread (watchdog kill)
+         if (flush) {
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+             asyncSaveData, chunk);
+ 
+         chunk.setUnsaved(false);
++        chunk.setLastSaved(this.level.getGameTime()); // Paper - track last saved time
+     }
+     // Paper end
+ 
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ 
+                         this.level.unload(chunk);
+                     }
++                    this.autoSaveQueue.remove(holder); // Paper
+ 
+                     // Paper start - async chunk saving
+                     try {
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+         if (!chunk.isUnsaved()) {
+             return false;
+         } else {
++            chunk.setLastSaved(this.level.getGameTime()); // Paper - track save time
+             chunk.setUnsaved(false);
+             ChunkPos chunkcoordintpair = chunk.getPos();
+ 
+diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+         } // Paper - Timings
+     }
+ 
++    // Paper start - duplicate save, but call incremental
++    public void saveIncrementally() {
++        this.runDistanceManagerUpdates();
++        try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings
++            this.chunkMap.saveIncrementally();
++        } // Paper - Timings
++    }
++    // Paper end
++
+     @Override
+     public void close() throws IOException {
+         // CraftBukkit start
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -0,0 +0,0 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
+         return !this.server.isUnderSpawnProtection(this, pos, player) && this.getWorldBorder().isWithinBounds(pos);
+     }
+ 
++    // Paper start - derived from below
++    public void saveIncrementally(boolean doFull) {
++        ServerChunkCache chunkproviderserver = this.getChunkSource();
++
++        if (doFull) {
++            org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(getWorld()));
++        }
++
++        try (co.aikar.timings.Timing ignored = this.timings.worldSave.startTiming()) {
++            if (doFull) {
++                this.saveLevelData();
++            }
++
++            this.timings.worldSaveChunks.startTiming(); // Paper
++            if (!this.noSave()) chunkproviderserver.saveIncrementally();
++            this.timings.worldSaveChunks.stopTiming(); // Paper
++
++
++            // Copied from save()
++            // CraftBukkit start - moved from MinecraftServer.saveChunks
++            if (doFull) { // Paper
++                ServerLevel worldserver1 = this;
++
++                this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings());
++                this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save());
++                this.convertable.saveDataTag(this.server.registryHolder, this.serverLevelData, this.server.getPlayerList().getSingleplayerData());
++            }
++            // CraftBukkit end
++        }
++    }
++    // Paper end
++
+     public void save(@Nullable ProgressListener progressListener, boolean flush, boolean flag1) {
+         ServerChunkCache chunkproviderserver = this.getChunkSource();
+ 
+diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
+@@ -0,0 +0,0 @@ public interface ChunkAccess extends BlockGetter, FeatureAccess {
+         return GameEventDispatcher.NOOP;
+     }
+ 
++    default void setLastSaved(long ticks) {}
+     // Paper start
+     default boolean generateFlatBedrock() {
+         if (this instanceof ProtoChunk) {
+diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+@@ -0,0 +0,0 @@ public class LevelChunk implements ChunkAccess {
+     private final ShortList[] postProcessing;
+     private TickList<Block> blockTicks;
+     private TickList<Fluid> liquidTicks;
++    // Paper start - track last save time
++    public long lastSaveTime;
++    @Override
++    public void setLastSaved(long ticks) {
++        this.lastSaveTime = ticks;
++    }
++    // Paper end
+     private volatile boolean unsaved;
+     private long inhabitedTime;
+     @Nullable