From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <spottedleaf@spottedleaf.dev>
Date: Mon, 31 Aug 2020 11:08:17 -0700
Subject: [PATCH] Actually unload POI data

While it's not likely for a poi data leak to be meaningful,
sometimes it is.

This patch also prevents the saving/unloading of POI data when
world saving is disabled.

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 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
                     }
                     // Paper end
                 }
+                this.getPoiManager().dequeueUnload(holder.pos.longKey); // Paper - unload POI data
 
                 this.updatingChunks.queueUpdate(pos, holder); // Paper - Don't copy
                 this.modified = true;
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
         gameprofilerfiller.pop();
     }
 
-    private static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.90; // Spigot // Paper - unload more
+    public static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.90; // Spigot // Paper - unload more
 
     private void processUnloads(BooleanSupplier shouldKeepTicking) {
         LongIterator longiterator = this.toDrop.iterator();
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
                         this.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
                     }
                     // Paper end
+                    this.getPoiManager().queueUnload(holder.pos.longKey, MinecraftServer.currentTickLong + 1); // Paper - unload POI data
                     if (ichunkaccess instanceof LevelChunk) {
                         ((LevelChunk) ichunkaccess).setLoaded(false);
                     }
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
                     for (int index = 0, len = this.regionManagers.size(); index < len; ++index) {
                         this.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
                     }
+                    this.getPoiManager().queueUnload(holder.pos.longKey, MinecraftServer.currentTickLong + 1); // Paper - unload POI data
                 } // Paper end
                 } finally { this.unloadingPlayerChunk = unloadingBefore; } // Paper - do not allow ticket level changes while unloading chunks
 
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
                 }
                 this.poiManager.loadInData(pos, chunkHolder.poiData);
                 chunkHolder.tasks.forEach(Runnable::run);
+                this.getPoiManager().dequeueUnload(pos.longKey); // Paper
                 // Paper end
 
                 if (chunkHolder.protoChunk != null) {try (Timing ignored2 = this.level.timings.chunkLoadLevelTimer.startTimingIfSync()) { // Paper start - timings // Paper - chunk is created async
diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
@@ -0,0 +0,0 @@
 package net.minecraft.world.entity.ai.village.poi;
 
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; // Paper
 import com.mojang.datafixers.DataFixer;
 import com.mojang.datafixers.util.Pair;
 import it.unimi.dsi.fastutil.longs.Long2ByteMap;
@@ -0,0 +0,0 @@ import net.minecraft.world.level.chunk.storage.SectionStorage;
 public class PoiManager extends SectionStorage<PoiSection> {
     public static final int MAX_VILLAGE_DISTANCE = 6;
     public static final int VILLAGE_SECTION_SIZE = 1;
-    private final PoiManager.DistanceTracker distanceTracker;
+    // Paper start - unload poi data
+    // the vanilla tracker needs to be replaced because it does not support level removes
+    private final io.papermc.paper.util.misc.Delayed26WayDistancePropagator3D villageDistanceTracker = new io.papermc.paper.util.misc.Delayed26WayDistancePropagator3D();
+    static final int POI_DATA_SOURCE = 7;
+    public static int convertBetweenLevels(final int level) {
+        return POI_DATA_SOURCE - level;
+    }
+
+    protected void updateDistanceTracking(long section) {
+        if (this.isVillageCenter(section)) {
+            this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE);
+        } else {
+            this.villageDistanceTracker.removeSource(section);
+        }
+    }
+    // Paper end - unload poi data
     private final LongSet loadedChunks = new LongOpenHashSet();
     public final net.minecraft.server.level.ServerLevel world; // Paper // Paper public
 
     public PoiManager(Path path, DataFixer dataFixer, boolean dsync, LevelHeightAccessor world) {
         super(path, PoiSection::codec, PoiSection::new, dataFixer, DataFixTypes.POI_CHUNK, dsync, world);
+        if (world == null) { throw new IllegalStateException("world must be non-null"); } // Paper - require non-null
         this.world = (net.minecraft.server.level.ServerLevel)world; // Paper
-        this.distanceTracker = new PoiManager.DistanceTracker();
     }
 
+    // Paper start - actually unload POI data
+    private final java.util.TreeSet<QueuedUnload> queuedUnloads = new java.util.TreeSet<>();
+    private final Long2ObjectOpenHashMap<QueuedUnload> queuedUnloadsByCoordinate = new Long2ObjectOpenHashMap<>();
+
+    static final class QueuedUnload implements Comparable<QueuedUnload> {
+
+        private final long unloadTick;
+        private final long coordinate;
+
+        public QueuedUnload(long unloadTick, long coordinate) {
+            this.unloadTick = unloadTick;
+            this.coordinate = coordinate;
+        }
+
+        @Override
+        public int compareTo(QueuedUnload other) {
+            if (other.unloadTick == this.unloadTick) {
+                return Long.compare(this.coordinate, other.coordinate);
+            } else {
+                return Long.compare(this.unloadTick, other.unloadTick);
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            int hash = 1;
+            hash = hash * 31 + Long.hashCode(this.unloadTick);
+            hash = hash * 31 + Long.hashCode(this.coordinate);
+            return hash;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null || obj.getClass() != QueuedUnload.class) {
+                return false;
+            }
+            QueuedUnload other = (QueuedUnload)obj;
+            return other.unloadTick == this.unloadTick && other.coordinate == this.coordinate;
+        }
+    }
+
+    long determineDelay(long coordinate) {
+        if (this.isEmpty(coordinate)) {
+            return 5 * 60 * 20;
+        } else {
+            return 60 * 20;
+        }
+    }
+
+    public void queueUnload(long coordinate, long minTarget) {
+        io.papermc.paper.util.TickThread.softEnsureTickThread("async poi unload queue");
+        QueuedUnload unload = new QueuedUnload(minTarget + this.determineDelay(coordinate), coordinate);
+        QueuedUnload existing = this.queuedUnloadsByCoordinate.put(coordinate, unload);
+        if (existing != null) {
+            this.queuedUnloads.remove(existing);
+        }
+        this.queuedUnloads.add(unload);
+    }
+
+    public void dequeueUnload(long coordinate) {
+        io.papermc.paper.util.TickThread.softEnsureTickThread("async poi unload dequeue");
+        QueuedUnload unload = this.queuedUnloadsByCoordinate.remove(coordinate);
+        if (unload != null) {
+            this.queuedUnloads.remove(unload);
+        }
+    }
+
+    public void pollUnloads(BooleanSupplier canSleepForTick) {
+        io.papermc.paper.util.TickThread.softEnsureTickThread("async poi unload");
+        long currentTick = net.minecraft.server.MinecraftServer.currentTickLong;
+        net.minecraft.server.level.ServerChunkCache chunkProvider = this.world.getChunkSource();
+        net.minecraft.server.level.ChunkMap playerChunkMap = chunkProvider.chunkMap;
+        // copied target determination from PlayerChunkMap
+        int target = Math.min(this.queuedUnloads.size() - 100,  (int) (this.queuedUnloads.size() * net.minecraft.server.level.ChunkMap.UNLOAD_QUEUE_RESIZE_FACTOR)); // Paper - Make more aggressive
+        for (java.util.Iterator<QueuedUnload> iterator = this.queuedUnloads.iterator();
+             iterator.hasNext() && (this.queuedUnloads.size() > target || canSleepForTick.getAsBoolean());) {
+            QueuedUnload unload = iterator.next();
+            if (unload.unloadTick > currentTick) {
+                break;
+            }
+
+            long coordinate = unload.coordinate;
+
+            iterator.remove();
+            this.queuedUnloadsByCoordinate.remove(coordinate);
+
+            if (playerChunkMap.getUnloadingChunkHolder(net.minecraft.server.MCUtil.getCoordinateX(coordinate), net.minecraft.server.MCUtil.getCoordinateZ(coordinate)) != null
+                || playerChunkMap.getUpdatingChunkIfPresent(coordinate) != null) {
+                continue;
+            }
+
+            this.unloadData(coordinate);
+        }
+    }
+
+    @Override
+    public void unloadData(long coordinate) {
+        io.papermc.paper.util.TickThread.softEnsureTickThread("async unloading poi data");
+        super.unloadData(coordinate);
+    }
+
+    @Override
+    protected void onUnload(long coordinate) {
+        io.papermc.paper.util.TickThread.softEnsureTickThread("async poi unload callback");
+        this.loadedChunks.remove(coordinate);
+        int chunkX = net.minecraft.server.MCUtil.getCoordinateX(coordinate);
+        int chunkZ = net.minecraft.server.MCUtil.getCoordinateZ(coordinate);
+        for (int section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) {
+            long sectionPos = SectionPos.asLong(chunkX, section, chunkZ);
+            this.updateDistanceTracking(sectionPos);
+        }
+    }
+    // Paper end - actually unload POI data
+
     public void add(BlockPos pos, PoiType type) {
         this.getOrCreate(SectionPos.asLong(pos)).add(pos, type);
     }
@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection> {
     }
 
     public int sectionsToVillage(SectionPos pos) {
-        this.distanceTracker.runAllUpdates();
-        return this.distanceTracker.getLevel(pos.asLong());
+        this.villageDistanceTracker.propagateUpdates(); // Paper - replace distance tracking util
+        return convertBetweenLevels(this.villageDistanceTracker.getLevel(io.papermc.paper.util.CoordinateUtils.getChunkSectionKey(pos))); // Paper - replace distance tracking util
     }
 
     boolean isVillageCenter(long pos) {
@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection> {
     @Override
     public void tick(BooleanSupplier shouldKeepTicking) {
         // Paper start - async chunk io
-        while (!this.dirty.isEmpty() && shouldKeepTicking.getAsBoolean()) {
+        while (!this.dirty.isEmpty() && shouldKeepTicking.getAsBoolean() && !this.world.noSave()) { // Paper - unload POI data - don't write to disk if saving is disabled
             ChunkPos chunkcoordintpair = SectionPos.of(this.dirty.firstLong()).chunk();
 
             net.minecraft.nbt.CompoundTag data;
@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection> {
             com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world,
                 chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY);
         }
+        // Paper start - unload POI data
+        if (!this.world.noSave()) { // don't write to disk if saving is disabled
+            this.pollUnloads(shouldKeepTicking);
+        }
+        // Paper end - unload POI data
         // Paper end
-        this.distanceTracker.runAllUpdates();
+        this.villageDistanceTracker.propagateUpdates(); // Paper - replace distance tracking until
     }
 
     @Override
     protected void setDirty(long pos) {
         super.setDirty(pos);
-        this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
+        this.updateDistanceTracking(pos); // Paper - move to new distance tracking util
     }
 
     @Override
     protected void onSectionLoad(long pos) {
-        this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
+        this.updateDistanceTracking(pos); // Paper - move to new distance tracking util
     }
 
     public void checkConsistencyWithBlocks(ChunkPos chunkPos, LevelChunkSection chunkSection) {
@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection> {
 
         @Override
         protected int getLevelFromSource(long id) {
-            return PoiManager.this.isVillageCenter(id) ? 0 : 7;
+            return PoiManager.this.isVillageCenter(id) ? 0 : 7; // Paper - unload poi data - diff on change, this specifies the source level to use for distance tracking
         }
 
         @Override
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
@@ -0,0 +0,0 @@ public class SectionStorage<R> extends RegionFileStorage implements AutoCloseabl
         // Paper - remove mojang I/O thread
     }
 
+    // Paper start - actually unload POI data
+    public void unloadData(long coordinate) {
+        ChunkPos chunkPos = new ChunkPos(coordinate);
+        this.flush(chunkPos);
+
+        Long2ObjectMap<Optional<R>> data = this.storage;
+        int before = data.size();
+
+        for (int section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) {
+            data.remove(SectionPos.asLong(chunkPos.x, section, chunkPos.z));
+        }
+
+        if (before != data.size()) {
+            this.onUnload(coordinate);
+        }
+    }
+
+    protected void onUnload(long coordinate) {}
+
+    public boolean isEmpty(long coordinate) {
+        Long2ObjectMap<Optional<R>> data = this.storage;
+        int x = net.minecraft.server.MCUtil.getCoordinateX(coordinate);
+        int z = net.minecraft.server.MCUtil.getCoordinateZ(coordinate);
+        for (int section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) {
+            Optional<R> optional = data.get(SectionPos.asLong(x, section, z));
+            if (optional != null && optional.orElse(null) != null) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+    // Paper end - actually unload POI data
+
     protected void tick(BooleanSupplier shouldKeepTicking) {
         while(!this.dirty.isEmpty() && shouldKeepTicking.getAsBoolean()) {
             ChunkPos chunkPos = SectionPos.of(this.dirty.firstLong()).chunk();
@@ -0,0 +0,0 @@ public class SectionStorage<R> extends RegionFileStorage implements AutoCloseabl
                 });
             }
         }
+        if (this instanceof net.minecraft.world.entity.ai.village.poi.PoiManager) { ((net.minecraft.world.entity.ai.village.poi.PoiManager)this).queueUnload(pos.longKey, net.minecraft.server.MinecraftServer.currentTickLong + 1); } // Paper - unload POI data
 
     }