From d166381efa45df8c1b2777656fdba13a64ec2ef5 Mon Sep 17 00:00:00 2001
From: Nassim Jahnke <nassim@njahnke.dev>
Date: Fri, 26 Nov 2021 17:25:35 +0100
Subject: [PATCH] Readd incremental chunk saving, merge with player saving

---
 .../server/Do-not-copy-visible-chunks.patch   |   2 +-
 ...I-O-threads-with-chunk-data-while-fl.patch |   2 +-
 ...mprove-Chunk-Status-Transition-Speed.patch |   4 +-
 .../server/Incremental-player-saving.patch    | 126 ------
 .../Make-item-validations-configurable.patch  |   4 +-
 .../Optimise-nearby-player-lookups.patch      |   2 +-
 ...-headless-pistons-from-being-created.patch |   5 +-
 patches/server/incremental-chunk-saving.patch | 414 ++++++++++++++++++
 8 files changed, 424 insertions(+), 135 deletions(-)
 delete mode 100644 patches/server/Incremental-player-saving.patch
 create mode 100644 patches/server/incremental-chunk-saving.patch

diff --git a/patches/server/Do-not-copy-visible-chunks.patch b/patches/server/Do-not-copy-visible-chunks.patch
index 827595fc4e..e9dee5f7b6 100644
--- a/patches/server/Do-not-copy-visible-chunks.patch
+++ b/patches/server/Do-not-copy-visible-chunks.patch
@@ -124,7 +124,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        ObjectIterator objectiterator = this.visibleChunkMap.values().iterator();
 +        Iterator objectiterator = this.updatingChunks.getVisibleValuesCopy().iterator(); // Paper
  
-         while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) {
+         while (false && l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) { // Paper - incremental chunk and player saving
              if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) {
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
          if (!this.modified) {
diff --git a/patches/server/Do-not-overload-I-O-threads-with-chunk-data-while-fl.patch b/patches/server/Do-not-overload-I-O-threads-with-chunk-data-while-fl.patch
index a25fbb4d68..a30e922275 100644
--- a/patches/server/Do-not-overload-I-O-threads-with-chunk-data-while-fl.patch
+++ b/patches/server/Do-not-overload-I-O-threads-with-chunk-data-while-fl.patch
@@ -16,7 +16,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java
 +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-     }
+     // Paper end
  
      protected void saveAllChunks(boolean flush) {
 +        // Paper start - do not overload I/O threads with too much work when saving
diff --git a/patches/server/Improve-Chunk-Status-Transition-Speed.patch b/patches/server/Improve-Chunk-Status-Transition-Speed.patch
index 98a4256fff..8606e368f0 100644
--- a/patches/server/Improve-Chunk-Status-Transition-Speed.patch
+++ b/patches/server/Improve-Chunk-Status-Transition-Speed.patch
@@ -40,9 +40,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- 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
 +    // Paper start - optimize chunk status progression without jumping through thread pool
 +    public boolean canAdvanceStatus() {
 +        ChunkStatus status = getChunkHolderStatus();
diff --git a/patches/server/Incremental-player-saving.patch b/patches/server/Incremental-player-saving.patch
deleted file mode 100644
index c96e60b017..0000000000
--- a/patches/server/Incremental-player-saving.patch
+++ /dev/null
@@ -1,126 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Aikar <aikar@aikar.co>
-Date: Sun, 9 Aug 2020 08:59:25 +0300
-Subject: [PATCH] Incremental player saving
-
-
-diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
-+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
-@@ -0,0 +0,0 @@ public class PaperConfig {
-         config.set("settings.unsupported-settings.allow-headless-pistons-readme", "This setting controls if players should be able to create headless pistons.");
-         allowHeadlessPistons = getBoolean("settings.unsupported-settings.allow-headless-pistons", false);
-     }
-+
-+    public static int playerAutoSaveRate = -1;
-+    public static int maxPlayerAutoSavePerTick = 10;
-+    private static void playerAutoSaveRate() {
-+        playerAutoSaveRate = getInt("settings.player-auto-save-rate", -1);
-+        maxPlayerAutoSavePerTick = getInt("settings.max-player-auto-save-per-tick", -1);
-+        if (maxPlayerAutoSavePerTick == -1) { // -1 Automatic / "Recommended"
-+            // 10 should be safe for everyone unless you mass spamming player auto save
-+            maxPlayerAutoSavePerTick = (playerAutoSaveRate == -1 || playerAutoSaveRate > 100) ? 10 : 20;
-+        }
-+    }
- }
-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
-         return flag3;
-     }
- 
-+    // Paper start
-     public boolean saveEverything(boolean suppressLogs, boolean flush, boolean force) {
-+        return saveEverything(suppressLogs, flush, force, -1);
-+    }
-+    public boolean saveEverything(boolean suppressLogs, boolean flush, boolean force, int playerSaveInterval) {
-+        // Paper end
-         boolean flag3;
- 
-         try {
-             this.isSaving = true;
--            this.getPlayerList().saveAll();
-+            this.getPlayerList().saveAll(playerSaveInterval); // Paper
-             flag3 = this.saveAllChunks(suppressLogs, flush, force);
-         } finally {
-             this.isSaving = false;
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-         this.isSaving = true;
-         if (this.playerList != null) {
-             MinecraftServer.LOGGER.info("Saving players");
--            this.playerList.saveAll();
-             this.playerList.removeAll(this.isRestarting); // Paper
-             try { Thread.sleep(100); } catch (InterruptedException ex) {} // CraftBukkit - SPIGOT-625 - give server at least a chance to send packets
-         }
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-             }
-         }
- 
-+        // Paper start
-+        int playerSaveInterval = com.destroystokyo.paper.PaperConfig.playerAutoSaveRate;
-+        if (playerSaveInterval < 0) {
-+            playerSaveInterval = autosavePeriod;
-+        }
-+        // Paper end
-         if (this.autosavePeriod > 0 && this.tickCount % this.autosavePeriod == 0) { // CraftBukkit
-             MinecraftServer.LOGGER.debug("Autosave started");
-             this.profiler.push("save");
--            this.saveEverything(true, false, false);
-+            this.saveEverything(true, false, false, playerSaveInterval); // Paper
-             this.profiler.pop();
-             MinecraftServer.LOGGER.debug("Autosave finished");
--        }
-+        } else this.getPlayerList().saveAll(playerSaveInterval); // Paper
-         io.papermc.paper.util.CachedLists.reset(); // Paper
-         // Paper start - move executeAll() into full server tick timing
-         try (co.aikar.timings.Timing ignored = MinecraftTimings.processTasksTimer.startTiming()) {
-diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
-+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-@@ -0,0 +0,0 @@ public class ServerPlayer extends Player {
-     public final int getViewDistance() { return this.getLevel().getChunkSource().chunkMap.viewDistance - 1; } // Paper - placeholder
- 
-     private static final Logger LOGGER = LogManager.getLogger();
-+    public long lastSave = MinecraftServer.currentTick; // Paper
-     private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32;
-     private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10;
-     public ServerGamePacketListenerImpl connection;
-diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/players/PlayerList.java
-+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
-@@ -0,0 +0,0 @@ public abstract class PlayerList {
-     protected void save(ServerPlayer player) {
-         if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit
-         if (!player.didPlayerJoinEvent) return; // Paper - If we never fired PJE, we disconnected during login. Data has not changed, and additionally, our saved vehicle is not loaded! If we save now, we will lose our vehicle (CraftBukkit bug)
-+        player.lastSave = MinecraftServer.currentTick; // Paper
-         this.playerIo.save(player);
-         ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit
- 
-@@ -0,0 +0,0 @@ public abstract class PlayerList {
-     }
- 
-     public void saveAll() {
-+        // Paper start - incremental player saving
-+        saveAll(-1);
-+    }
-+    public void saveAll(int interval) {
-         net.minecraft.server.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main
-         MinecraftTimings.savePlayers.startTiming(); // Paper
-+        int numSaved = 0;
-+        long now = MinecraftServer.currentTick;
-         for (int i = 0; i < this.players.size(); ++i) {
--            this.save(this.players.get(i));
-+            ServerPlayer entityplayer = this.players.get(i);
-+            if (interval == -1 || now - entityplayer.lastSave >= interval) {
-+                this.save(entityplayer);
-+                if (interval != -1 && ++numSaved <= com.destroystokyo.paper.PaperConfig.maxPlayerAutoSavePerTick) { break; }
-+            }
-+            // Paper end
-         }
-         MinecraftTimings.savePlayers.stopTiming(); // Paper
-         return null; }); // Paper - ensure main
diff --git a/patches/server/Make-item-validations-configurable.patch b/patches/server/Make-item-validations-configurable.patch
index 75ca412237..ea9b4d0284 100644
--- a/patches/server/Make-item-validations-configurable.patch
+++ b/patches/server/Make-item-validations-configurable.patch
@@ -9,8 +9,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
 +++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
 @@ -0,0 +0,0 @@ public class PaperConfig {
-             maxPlayerAutoSavePerTick = (playerAutoSaveRate == -1 || playerAutoSaveRate > 100) ? 10 : 20;
-         }
+         config.set("settings.unsupported-settings.allow-headless-pistons-readme", "This setting controls if players should be able to create headless pistons.");
+         allowHeadlessPistons = getBoolean("settings.unsupported-settings.allow-headless-pistons", false);
      }
 +
 +    public static int itemValidationDisplayNameLength = 8192;
diff --git a/patches/server/Optimise-nearby-player-lookups.patch b/patches/server/Optimise-nearby-player-lookups.patch
index 131025b266..33efdb0d39 100644
--- a/patches/server/Optimise-nearby-player-lookups.patch
+++ b/patches/server/Optimise-nearby-player-lookups.patch
@@ -24,7 +24,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - optimise checkDespawn
      }
      // Paper end - optimise isOutsideOfRange
-     // Paper start - optimize chunk status progression without jumping through thread pool
+     long lastAutoSaveTime; // Paper - incremental autosave
 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
diff --git a/patches/server/Prevent-headless-pistons-from-being-created.patch b/patches/server/Prevent-headless-pistons-from-being-created.patch
index 701e0cfe37..9c1e1ebe00 100644
--- a/patches/server/Prevent-headless-pistons-from-being-created.patch
+++ b/patches/server/Prevent-headless-pistons-from-being-created.patch
@@ -10,9 +10,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
 +++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
 @@ -0,0 +0,0 @@ public class PaperConfig {
-         set("settings.unsupported-settings.allow-tnt-duplication", null);
+             maxPlayerAutoSavePerTick = (playerAutoSaveRate == -1 || playerAutoSaveRate > 100) ? 10 : 20;
+         }
      }
- 
++
 +    public static boolean allowHeadlessPistons;
 +    private static void allowHeadlessPistons() {
 +        config.set("settings.unsupported-settings.allow-headless-pistons-readme", "This setting controls if players should be able to create headless pistons.");
diff --git a/patches/server/incremental-chunk-saving.patch b/patches/server/incremental-chunk-saving.patch
new file mode 100644
index 0000000000..2c51852aeb
--- /dev/null
+++ b/patches/server/incremental-chunk-saving.patch
@@ -0,0 +1,414 @@
+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/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
++++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
+@@ -0,0 +0,0 @@ public class PaperConfig {
+         set("settings.unsupported-settings.allow-tnt-duplication", null);
+     }
+ 
++    public static int playerAutoSaveRate = -1;
++    public static int maxPlayerAutoSavePerTick = 10;
++    private static void playerAutoSaveRate() {
++        playerAutoSaveRate = getInt("settings.player-auto-save-rate", -1);
++        maxPlayerAutoSavePerTick = getInt("settings.max-player-auto-save-per-tick", -1);
++        if (maxPlayerAutoSavePerTick == -1) { // -1 Automatic / "Recommended"
++            // 10 should be safe for everyone unless you mass spamming player auto save
++            maxPlayerAutoSavePerTick = (playerAutoSaveRate == -1 || playerAutoSaveRate > 100) ? 10 : 20;
++        }
++    }
+ }
+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
+ 
+         try {
+             this.isSaving = true;
+-            this.getPlayerList().saveAll();
++            this.getPlayerList().saveAll(); // Diff on change
+             flag3 = this.saveAllChunks(suppressLogs, flush, force);
+         } finally {
+             this.isSaving = false;
+@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+             }
+         }
+ 
+-        if (this.autosavePeriod > 0 && this.tickCount % this.autosavePeriod == 0) { // CraftBukkit
+-            MinecraftServer.LOGGER.debug("Autosave started");
+-            this.profiler.push("save");
+-            this.saveEverything(true, false, false);
+-            this.profiler.pop();
+-            MinecraftServer.LOGGER.debug("Autosave finished");
++        // Paper start - incremental chunk and player saving
++        int playerSaveInterval = com.destroystokyo.paper.PaperConfig.playerAutoSaveRate;
++        if (playerSaveInterval < 0) {
++            playerSaveInterval = autosavePeriod;
+         }
++        this.profiler.push("save");
++        try {
++            this.isSaving = true;
++            if (playerSaveInterval > 0) {
++                this.playerList.saveAll(playerSaveInterval);
++            }
++            for (ServerLevel level : this.getAllLevels()) {
++                if (level.paperConfig.autoSavePeriod > 0) {
++                    level.saveIncrementally(autosavePeriod > 0 && this.tickCount % autosavePeriod == 0);
++                }
++            }
++        } finally {
++            this.isSaving = false;
++        }
++        this.profiler.pop();
++        // Paper end
+         io.papermc.paper.util.CachedLists.reset(); // Paper
+         // Paper start - move executeAll() into full server tick timing
+         try (co.aikar.timings.Timing ignored = MinecraftTimings.processTasksTimer.startTiming()) {
+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.commons.lang3.mutable.MutableObject;
+ import org.apache.logging.log4j.LogManager;
+@@ -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) && this.level.entityManager.storeChunkSections(playerchunk.pos.toLong(), entity -> {})) {
++                    ++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) {
+         if (flush) {
+             List<ChunkHolder> list = (List) this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList());
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+         int l = 0;
+         ObjectIterator objectiterator = this.visibleChunkMap.values().iterator();
+ 
+-        while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) {
++        while (false && l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) { // Paper - incremental chunk and player saving
+             if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) {
+                 ++l;
+             }
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ 
+                         this.level.unload(chunk);
+                     }
++                    this.autoSaveQueue.remove(holder); // Paper
+ 
+                     this.lightEngine.updateChunkStatus(ichunkaccess.getPos());
+                     this.lightEngine.tryScheduleUpdate();
+@@ -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
+         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 Level implements WorldGenLevel {
+         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 savingDisabled) {
+         ServerChunkCache chunkproviderserver = this.getChunkSource();
+ 
+diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+@@ -0,0 +0,0 @@ public class ServerPlayer extends Player {
+     public final int getViewDistance() { return this.getLevel().getChunkSource().chunkMap.viewDistance - 1; } // Paper - placeholder
+ 
+     private static final Logger LOGGER = LogManager.getLogger();
++    public long lastSave = MinecraftServer.currentTick; // Paper
+     private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32;
+     private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10;
+     public ServerGamePacketListenerImpl connection;
+diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/players/PlayerList.java
++++ b/src/main/java/net/minecraft/server/players/PlayerList.java
+@@ -0,0 +0,0 @@ public abstract class PlayerList {
+     protected void save(ServerPlayer player) {
+         if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit
+         if (!player.didPlayerJoinEvent) return; // Paper - If we never fired PJE, we disconnected during login. Data has not changed, and additionally, our saved vehicle is not loaded! If we save now, we will lose our vehicle (CraftBukkit bug)
++        player.lastSave = MinecraftServer.currentTick; // Paper
+         this.playerIo.save(player);
+         ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit
+ 
+@@ -0,0 +0,0 @@ public abstract class PlayerList {
+     }
+ 
+     public void saveAll() {
++        // Paper start - incremental player saving
++        this.saveAll(-1);
++    }
++
++    public void saveAll(int interval) {
+         net.minecraft.server.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main
+         MinecraftTimings.savePlayers.startTiming(); // Paper
++        int numSaved = 0;
++        long now = MinecraftServer.currentTick;
+         for (int i = 0; i < this.players.size(); ++i) {
+-            this.save(this.players.get(i));
++            ServerPlayer entityplayer = this.players.get(i);
++            if (interval == -1 || now - entityplayer.lastSave >= interval) {
++                this.save(entityplayer);
++                if (interval != -1 && ++numSaved <= com.destroystokyo.paper.PaperConfig.maxPlayerAutoSavePerTick) { break; }
++            }
++            // Paper end
+         }
+         MinecraftTimings.savePlayers.stopTiming(); // Paper
+         return null; }); // Paper - ensure main
+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 abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
+     public LevelHeightAccessor getHeightAccessorForGeneration() {
+         return this;
+     }
++    public void setLastSaved(long ticks) {} // Paper
+ 
+     // CraftBukkit start - decompile error
+     public static record TicksToSave(SerializableTickContainer<Block> blocks, SerializableTickContainer<Fluid> fluids) {
+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 extends ChunkAccess {
+     private final Int2ObjectMap<GameEventDispatcher> gameEventDispatcherSections;
+     private final LevelChunkTicks<Block> blockTicks;
+     private final LevelChunkTicks<Fluid> fluidTicks;
++    // Paper start - track last save time
++    public long lastSaveTime;
++    public void setLastSaved(long ticks) {
++        this.lastSaveTime = ticks;
++    }
++    // Paper end
+ 
+     public LevelChunk(Level world, ChunkPos pos) {
+         this(world, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, (LevelChunkSection[]) null, (LevelChunk.PostLoadProcessor) null, (BlendingData) null);