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/ChunkSystem.java b/src/main/java/net/minecraft/server/ChunkSystem.java
index 2a099fe0d514f181bf2b452d5333bc29b0d29e43..81ea64443a843736f9ada97900d173c302e39ba0 100644
--- a/src/main/java/net/minecraft/server/ChunkSystem.java
+++ b/src/main/java/net/minecraft/server/ChunkSystem.java
@@ -270,6 +270,7 @@ public final class ChunkSystem {
         for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) {
             chunkMap.regionManagers.get(index).addChunk(holder.pos.x, holder.pos.z);
         }
+        chunkMap.getPoiManager().dequeueUnload(holder.pos.longKey); // Paper - unload POI data
     }
 
     public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
@@ -277,6 +278,7 @@ public final class ChunkSystem {
         for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) {
             chunkMap.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
         }
+        chunkMap.getPoiManager().queueUnload(holder.pos.longKey, MinecraftServer.currentTickLong + 1); // Paper - unload POI data
     }
 
     public static void onChunkBorder(LevelChunk chunk, ChunkHolder holder) {
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index fe10c770b511fa8a38ece2bf9679492a85b28eff..a5e74d30045a171f5ed66a115fbd429e9ab412af 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1016,7 +1016,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
 
     private void processUnloads(BooleanSupplier shouldKeepTicking) {
         LongIterator longiterator = this.toDrop.iterator();
-        for (int i = 0; longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longiterator.remove()) {
+        for (int i = 0; longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longiterator.remove()) { // Paper - diff on change
             long j = longiterator.nextLong();
             ChunkHolder playerchunk = this.updatingChunks.queueRemove(j); // Paper - Don't copy
 
@@ -1164,6 +1164,7 @@ 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
 
                 if (chunkHolder.protoChunk != null) {
                     ProtoChunk protochunk = chunkHolder.protoChunk;
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 497a81e49d54380713c18523ae8f09f94c453721..210b0cdd4831421c8f43c3d823ac8e962b56bbbc 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
@@ -1,5 +1,6 @@
 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;
@@ -38,16 +39,145 @@ 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, RegistryAccess registryManager, LevelHeightAccessor world) {
         super(path, PoiSection::codec, PoiSection::new, dataFixer, DataFixTypes.POI_CHUNK, dsync, registryManager, 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
+
+        java.util.Iterator<QueuedUnload> iterator = this.queuedUnloads.iterator();
+        for (int i = 0; iterator.hasNext() && (i < 200 || this.queuedUnloads.size() > 2000 || canSleepForTick.getAsBoolean()); i++) {
+            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, Holder<PoiType> type) {
         this.getOrCreate(SectionPos.asLong(pos)).add(pos, type);
     }
@@ -201,8 +331,8 @@ 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) {
@@ -217,7 +347,7 @@ 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;
@@ -227,19 +357,24 @@ 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) {
@@ -297,7 +432,7 @@ 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 10e8d1e36639cca21aa451e81cdab90ba9e9a496..954819db8ada38ef2c832151be8a96492e76390a 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
@@ -58,6 +58,40 @@ 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.hasWork() && shouldKeepTicking.getAsBoolean()) {
             ChunkPos chunkPos = SectionPos.of(this.dirty.firstLong()).chunk();
@@ -175,6 +209,7 @@ 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
 
     }