From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Fri, 25 Oct 2019 02:11:30 -0700
Subject: [PATCH] Delay chunk unloads

Chunk unloads are now delayed by 1s. Specifically, ticket level
reduction is delayed by 1s. This is done to allow players to
teleport and have their pets follow them, as the chunks will no longer
unload or have entity ticking status removed.

It's also targetted to reduce performance regressions when
plugins or edge cases in code do not spam sync loads since chunks
without tickets get unloaded immediately.

Configurable under `delay-chunkunloads-by` in config.

This patch replaces the paper patch as the paper patch only
affects player loaded chunks, when we want to target all
loads.

diff --git a/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java b/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java
index f10fa659680f8a574f77d260bbc52be349c244e8..7b12e7610444ff20f2c3f458887bd7d4e6715036 100644
--- a/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java
+++ b/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java
@@ -1,6 +1,7 @@
 package com.tuinity.tuinity.config;
 
 import com.destroystokyo.paper.util.SneakyThrow;
+import net.minecraft.server.TicketType;
 import org.bukkit.Bukkit;
 import org.bukkit.configuration.ConfigurationSection;
 import org.bukkit.configuration.file.YamlConfiguration;
@@ -123,6 +124,15 @@ public final class TuinityConfig {
         tickThreads = TuinityConfig.getInt("server-tick-threads", 1); // will be 4 in the future
     }*/
 
+    public static int delayChunkUnloadsBy;
+
+    private static void delayChunkUnloadsBy() {
+        delayChunkUnloadsBy = TuinityConfig.getInt("delay-chunkunloads-by", 5) * 20;
+        if (delayChunkUnloadsBy >= 0) {
+            TicketType.DELAYED_UNLOAD.loadPeriod = delayChunkUnloadsBy;
+        }
+    }
+
     public static final class WorldConfig {
 
         public final String worldName;
diff --git a/src/main/java/net/minecraft/server/ChunkMapDistance.java b/src/main/java/net/minecraft/server/ChunkMapDistance.java
index 84ff9cfe89defad2e907708f837d33f620a84ef3..7f3887b0894aca0f972922f434382646a6ad6174 100644
--- a/src/main/java/net/minecraft/server/ChunkMapDistance.java
+++ b/src/main/java/net/minecraft/server/ChunkMapDistance.java
@@ -31,7 +31,7 @@ public abstract class ChunkMapDistance {
     private static final int b = 33 + ChunkStatus.a(ChunkStatus.FULL) - 2;
     private final Long2ObjectMap<ObjectSet<EntityPlayer>> c = new Long2ObjectOpenHashMap();
     public final Long2ObjectOpenHashMap<ArraySetSorted<Ticket<?>>> tickets = new Long2ObjectOpenHashMap();
-    private final ChunkMapDistance.a ticketLevelTracker = new ChunkMapDistance.a();
+    private final ChunkMapDistance.a ticketLevelTracker = new ChunkMapDistance.a(); final ChunkMapDistance.a getTicketTracker() { return this.ticketLevelTracker; } // Tuinity - OBFHELPER
     public static final int MOB_SPAWN_RANGE = 8; // private final ChunkMapDistance.b f = new ChunkMapDistance.b(8); // Paper - no longer used
     private final ChunkMapDistance.c g = new ChunkMapDistance.c(33);
     // Paper start use a queue, but still keep unique requirement
@@ -53,6 +53,47 @@ public abstract class ChunkMapDistance {
 
     PlayerChunkMap chunkMap; // Paper
 
+    // Tuinity start - delay chunk unloads
+    private long nextUnloadId; // delay chunk unloads
+    private final Long2ObjectOpenHashMap<Ticket<Long>> delayedChunks = new Long2ObjectOpenHashMap<>();
+    public final void removeTickets(long chunk, TicketType<?> type) {
+        ArraySetSorted<Ticket<?>> tickets = this.tickets.get(chunk);
+        if (tickets == null) {
+            return;
+        }
+        if (type == TicketType.DELAYED_UNLOAD) {
+            this.delayedChunks.remove(chunk);
+        }
+        boolean changed = tickets.removeIf((Ticket<?> ticket) -> {
+            return ticket.getTicketType() == type;
+        });
+        if (changed) {
+            this.getTicketTracker().update(chunk, getLowestTicketLevel(tickets), false);
+        }
+    }
+
+    private final java.util.function.LongFunction<Ticket<Long>> computeFuntion = (long key) -> {
+        Ticket<Long> ret = new Ticket<>(TicketType.DELAYED_UNLOAD, -1, ++ChunkMapDistance.this.nextUnloadId);
+        ret.isCached = true;
+        return ret;
+    };
+
+    private void computeDelayedTicketFor(long chunk, int removedLevel, ArraySetSorted<Ticket<?>> tickets) {
+        int lowestLevel = getLowestTicketLevel(tickets);
+        if (removedLevel > lowestLevel) {
+            return;
+        }
+        final Ticket<Long> ticket = this.delayedChunks.computeIfAbsent(chunk, this.computeFuntion);
+        if (ticket.getTicketLevel() != -1) {
+            // since we modify data used in sorting, we need to remove before
+            tickets.remove(ticket);
+        }
+        ticket.setCreationTick(this.currentTick);
+        ticket.setTicketLevel(removedLevel);
+        tickets.add(ticket); // re-add with new expire time and ticket level
+    }
+    // Tuinity end - delay chunk unloads
+
     protected ChunkMapDistance(Executor executor, Executor executor1) {
         executor1.getClass();
         Mailbox<Runnable> mailbox = Mailbox.a("player ticket throttler", executor1::execute);
@@ -69,18 +110,41 @@ public abstract class ChunkMapDistance {
         ++this.currentTick;
         ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
 
+        // Tuinity start - delay chunk unloads
+        int[] tempLevel = new int[] { PlayerChunkMap.GOLDEN_TICKET + 1 };
+        Entry<ArraySetSorted<Ticket<?>>>[] entryPass = new Entry[1];
+        java.util.function.Predicate<Ticket<?>> isExpired = (ticket) -> { // CraftBukkit - decompile error
+            // Tuinity start - delay chunk unloads
+            boolean ret = ticket.isExpired(this.currentTick);
+            if (com.tuinity.tuinity.config.TuinityConfig.delayChunkUnloadsBy <= 0) {
+                return ret;
+            }
+            if (ret && ticket.getTicketType().delayUnloadViable && ticket.getTicketLevel() < tempLevel[0]) {
+                tempLevel[0] = ticket.getTicketLevel();
+            }
+            if (ret && ticket.getTicketType() == TicketType.DELAYED_UNLOAD && ticket.isCached) {
+                this.delayedChunks.remove(entryPass[0].getLongKey(), ticket); // clean up ticket...
+            }
+            return ret;
+        };
+        // Tuinity end - delay chunk unloads
         while (objectiterator.hasNext()) {
-            Entry<ArraySetSorted<Ticket<?>>> entry = (Entry) objectiterator.next();
+            Entry<ArraySetSorted<Ticket<?>>> entry = (Entry) objectiterator.next(); entryPass[0] = entry; // Tuinity - only allocate lambda once
 
-            if ((entry.getValue()).removeIf((ticket) -> { // CraftBukkit - decompile error
-                return ticket.b(this.currentTick);
-            })) {
+            if ((entry.getValue()).removeIf(isExpired)) { // Tuinity - move above - only allocate once
+                // Tuinity start - delay chunk unloads
+                if (tempLevel[0] < (PlayerChunkMap.GOLDEN_TICKET + 1)) {
+                    this.computeDelayedTicketFor(entry.getLongKey(), tempLevel[0], entry.getValue());
+                }
+                // Tuinity end - delay chunk unloads
                 this.ticketLevelTracker.update(entry.getLongKey(), getLowestTicketLevel((ArraySetSorted) entry.getValue()), false);
             }
 
             if (((ArraySetSorted) entry.getValue()).isEmpty()) {
                 objectiterator.remove();
             }
+
+            tempLevel[0] = PlayerChunkMap.GOLDEN_TICKET + 1; // Tuinity - reset
         }
 
     }
@@ -178,27 +242,11 @@ public abstract class ChunkMapDistance {
         boolean removed = false; // CraftBukkit
         if (arraysetsorted.remove(ticket)) {
             removed = true; // CraftBukkit
-            // Paper start - delay chunk unloads for player tickets
-            long delayChunkUnloadsBy = chunkMap.world.paperConfig.delayChunkUnloadsBy;
-            if (ticket.getTicketType() == TicketType.PLAYER && delayChunkUnloadsBy > 0) {
-                boolean hasPlayer = false;
-                for (Ticket<?> ticket1 : arraysetsorted) {
-                    if (ticket1.getTicketType() == TicketType.PLAYER) {
-                        hasPlayer = true;
-                        break;
-                    }
-                }
-                PlayerChunk playerChunk = chunkMap.getUpdatingChunk(i);
-                if (!hasPlayer && playerChunk != null && playerChunk.isFullChunkReady()) {
-                    Ticket<Long> delayUnload = new Ticket<Long>(TicketType.DELAY_UNLOAD, 33, i);
-                    delayUnload.delayUnloadBy = delayChunkUnloadsBy;
-                    delayUnload.setCurrentTick(this.currentTick);
-                    arraysetsorted.remove(delayUnload);
-                    // refresh ticket
-                    arraysetsorted.add(delayUnload);
-                }
+            // Tuinity start - delay chunk unloads
+            if (com.tuinity.tuinity.config.TuinityConfig.delayChunkUnloadsBy > 0 && ticket.getTicketType().delayUnloadViable) {
+                this.computeDelayedTicketFor(i, ticket.getTicketLevel(), arraysetsorted);
             }
-            // Paper end
+            // Tuinity end - delay chunk unloads
         }
 
         if (arraysetsorted.isEmpty()) {
diff --git a/src/main/java/net/minecraft/server/Ticket.java b/src/main/java/net/minecraft/server/Ticket.java
index e41cb8613efc86499dfe3be36c9130ab6dc9b89e..c19ffb925a02d123da8a5c77186e6105422dccf7 100644
--- a/src/main/java/net/minecraft/server/Ticket.java
+++ b/src/main/java/net/minecraft/server/Ticket.java
@@ -5,17 +5,17 @@ import java.util.Objects;
 public final class Ticket<T> implements Comparable<Ticket<?>> {
 
     private final TicketType<T> a;
-    private final int b;
+    private int b; public final void setTicketLevel(final int value) { this.b = value; } // Tuinity - remove final, add set OBFHELPER
     public final T identifier; public final T getObjectReason() { return this.identifier; } // Paper - OBFHELPER
-    private long d; public final long getCreationTick() { return this.d; } // Paper - OBFHELPER
+    private long d; public final long getCreationTick() { return this.d; } public final void setCreationTick(final long value) { this.d = value; }  // Paper - OBFHELPER // Tuinity - OBFHELPER
     public int priority = 0; // Paper
-    public long delayUnloadBy; // Paper
+    boolean isCached; // Tuinity - delay chunk unloads, this defends against really stupid plugins
 
     protected Ticket(TicketType<T> tickettype, int i, T t0) {
         this.a = tickettype;
         this.b = i;
         this.identifier = t0;
-        this.delayUnloadBy = tickettype.loadPeriod; // Paper
+        // Tuinity - delay chunk unloads
     }
 
     public int compareTo(Ticket<?> ticket) {
@@ -64,8 +64,9 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
         this.d = i;
     }
 
+    protected final boolean isExpired(long time) { return this.b(time); } // Tuinity - OBFHELPER
     protected boolean b(long i) {
-        long j = delayUnloadBy; // Paper
+        long j = this.a.b(); // Tuinity - delay chunk unloads
 
         return j != 0L && i - this.d > j;
     }
diff --git a/src/main/java/net/minecraft/server/TicketType.java b/src/main/java/net/minecraft/server/TicketType.java
index 5c789b25f1df2eae8ea8ceb4ba977ba336fe6d5e..ab0417b3897911ba29602d696f4842bfb77cee16 100644
--- a/src/main/java/net/minecraft/server/TicketType.java
+++ b/src/main/java/net/minecraft/server/TicketType.java
@@ -26,8 +26,18 @@ public class TicketType<T> {
     public static final TicketType<Long> ASYNC_LOAD = a("async_load", Long::compareTo); // Paper
     public static final TicketType<ChunkCoordIntPair> PRIORITY = a("priority", Comparator.comparingLong(ChunkCoordIntPair::pair), 300); // Paper
     public static final TicketType<ChunkCoordIntPair> URGENT = a("urgent", Comparator.comparingLong(ChunkCoordIntPair::pair), 300); // Paper
-    public static final TicketType<Long> DELAY_UNLOAD = a("delay_unload", Long::compareTo, 300); // Paper
+    public static final TicketType<Long> DELAYED_UNLOAD = a("delayed_unload", Long::compareTo); // Tuinity - delay chunk unloads
 
+    // Tuinity start - delay chunk unloads
+    boolean delayUnloadViable = true;
+    static {
+        TicketType.LIGHT.delayUnloadViable = false;
+        TicketType.PLUGIN.delayUnloadViable = false;
+        TicketType.PRIORITY.delayUnloadViable = false;
+        TicketType.URGENT.delayUnloadViable = false;
+        TicketType.DELAYED_UNLOAD.delayUnloadViable = false;
+    }
+    // Tuinity end - delay chunk unloads
     public static <T> TicketType<T> a(String s, Comparator<T> comparator) {
         return new TicketType<>(s, comparator, 0L);
     }
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index cade3ed6079011f44eba613dece3d69ec18f8f04..3c7e3db06be46b20c417b7ef36a17affa4c6ef78 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -505,6 +505,7 @@ public class CraftWorld implements World {
         org.spigotmc.AsyncCatcher.catchOp("chunk unload"); // Spigot
         if (isChunkLoaded(x, z)) {
             world.getChunkProvider().removeTicket(TicketType.PLUGIN, new ChunkCoordIntPair(x, z), 0, Unit.INSTANCE); // Paper
+            ((ChunkMapDistance)world.getChunkProvider().playerChunkMap.chunkDistanceManager).removeTickets(ChunkCoordIntPair.pair(x, z), TicketType.DELAYED_UNLOAD); // Tuinity - delay chunk unloads - let plugins override
         }
 
         return true;
