From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Paul Sauve <paul@technove.co>
Date: Wed, 14 Apr 2021 22:58:15 -0500
Subject: [PATCH] Improve fluid direction caching

Implements a custom cache that better fits the needs of fluids
calculating whether a direction can be moved in or something. There's a
big javadoc on the FluidDirectionCache with some more information.

diff --git a/src/main/java/gg/airplane/structs/FluidDirectionCache.java b/src/main/java/gg/airplane/structs/FluidDirectionCache.java
new file mode 100644
index 0000000000000000000000000000000000000000..11279fb136bbaf3e51d9b080a9e283d8ff0cbb47
--- /dev/null
+++ b/src/main/java/gg/airplane/structs/FluidDirectionCache.java
@@ -0,0 +1,142 @@
+package gg.airplane.structs;
+
+import it.unimi.dsi.fastutil.HashCommon;
+
+/**
+ * This is a replacement for the cache used in FluidTypeFlowing.
+ * The requirements for the previous cache were:
+ *  - Store 200 entries
+ *  - Look for the flag in the cache
+ *  - If it exists, move to front of cache
+ *  - If it doesn't exist, remove last entry in cache and insert in front
+ *
+ * This class accomplishes something similar, however has a few different
+ * requirements put into place to make this more optimize:
+ *
+ *  - maxDistance is the most amount of entries to be checked, instead
+ *    of having to check the entire list.
+ *  - In combination with that, entries are all tracked by age and how
+ *    frequently they're used. This enables us to remove old entries,
+ *    without constantly shifting any around.
+ *
+ * Usage of the previous map would have to reset the head every single usage,
+ * shifting the entire map. Here, nothing happens except an increment when
+ * the cache is hit, and when it needs to replace an old element only a single
+ * element is modified.
+ */
+public class FluidDirectionCache<T> {
+
+    private static class FluidDirectionEntry<T> {
+        private final T data;
+        private final boolean flag;
+        private short uses = 0;
+        private short age = 0;
+
+        private FluidDirectionEntry(T data, boolean flag) {
+            this.data = data;
+            this.flag = flag;
+        }
+
+        public int getValue() {
+            return this.uses - (this.age >> 1); // age isn't as important as uses
+        }
+
+        public void incrementUses() {
+            if (this.uses < Short.MAX_VALUE) {
+                this.uses++;
+            }
+        }
+
+        public void incrementAge() {
+            if (this.age < Short.MAX_VALUE) {
+                this.age++;
+            }
+        }
+    }
+
+    private final FluidDirectionEntry[] entries;
+    private final int mask;
+    private final int maxDistance; // the most amount of entries to check for a value
+
+    public FluidDirectionCache(int size) {
+        float fill = 0.75f;
+
+        int arraySize = HashCommon.arraySize(size, fill);
+        this.entries = new FluidDirectionEntry[arraySize];
+        this.mask = arraySize - 1;
+        this.maxDistance = Math.max(4, arraySize >> 4);
+    }
+
+    public Boolean getValue(T data) {
+        FluidDirectionEntry curr;
+        int pos;
+
+        if ((curr = this.entries[pos = HashCommon.mix(data.hashCode()) & this.mask]) == null) {
+            return null;
+        } else if (data.equals(curr.data)) {
+            curr.incrementUses();
+            return curr.flag;
+        }
+
+        int checked = 1; // start at 1 because we already checked the first spot above
+
+        while ((curr = this.entries[pos = (pos + 1) & this.mask]) != null) {
+            if (data.equals(curr.data)) {
+                curr.incrementUses();
+                return curr.flag;
+            } else if (++checked >= this.maxDistance) {
+                break;
+            }
+        }
+
+        return null;
+    }
+
+    public void putValue(T data, boolean flag) {
+        FluidDirectionEntry<T> curr;
+        int pos;
+
+        if ((curr = this.entries[pos = HashCommon.mix(data.hashCode()) & this.mask]) == null) {
+            this.entries[pos] = new FluidDirectionEntry<>(data, flag); // add
+            return;
+        } else if (data.equals(curr.data)) {
+            curr.incrementUses();
+            return;
+        }
+
+        int checked = 1; // start at 1 because we already checked the first spot above
+
+        while ((curr = this.entries[pos = (pos + 1) & this.mask]) != null) {
+            if (data.equals(curr.data)) {
+                curr.incrementUses();
+                return;
+            } else if (++checked >= this.maxDistance) {
+                this.forceAdd(data, flag);
+                return;
+            }
+        }
+
+        this.entries[pos] = new FluidDirectionEntry<>(data, flag); // add
+    }
+
+    private void forceAdd(T data, boolean flag) {
+        int expectedPos = HashCommon.mix(data.hashCode()) & this.mask;
+
+        int toRemovePos = expectedPos;
+        FluidDirectionEntry<T> entryToRemove = this.entries[toRemovePos];
+
+        for (int i = expectedPos + 1; i < expectedPos + this.maxDistance; i++) {
+            int pos = i & this.mask;
+            FluidDirectionEntry<T> entry = this.entries[pos];
+            if (entry.getValue() < entryToRemove.getValue()) {
+                toRemovePos = pos;
+                entryToRemove = entry;
+            }
+
+            entry.incrementAge(); // use this as a mechanism to age the other entries
+        }
+
+        // remove the least used/oldest entry
+        this.entries[toRemovePos] = new FluidDirectionEntry(data, flag);
+    }
+}
diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java
index 596b4597313b87296d39027b13555b5ad1cba9e6..f8a982add50862f1bc977f3039e7e9aeed9138ae 100644
--- a/src/main/java/net/minecraft/world/level/block/Block.java
+++ b/src/main/java/net/minecraft/world/level/block/Block.java
@@ -392,6 +392,7 @@ public class Block extends BlockBase implements IMaterial {
         return this.d;
     }
 
+    /** currently seems to only return true for MOVING_PISTON and SCAFFOLDING */ public boolean isComplexHitbox() { return this.o(); } // Airplane - OBFHELPER
     public boolean o() {
         return this.aA;
     }
diff --git a/src/main/java/net/minecraft/world/level/material/FluidTypeFlowing.java b/src/main/java/net/minecraft/world/level/material/FluidTypeFlowing.java
index 6bb4ec00e40795ced73648fefcd1f5027e0113cd..b14b0134b42aa6d1eb285aa453ec6067cc702878 100644
--- a/src/main/java/net/minecraft/world/level/material/FluidTypeFlowing.java
+++ b/src/main/java/net/minecraft/world/level/material/FluidTypeFlowing.java
@@ -45,6 +45,8 @@ public abstract class FluidTypeFlowing extends FluidType {
 
     public static final BlockStateBoolean FALLING = BlockProperties.i;
     public static final BlockStateInteger LEVEL = BlockProperties.at;
+    // Airplane start - use our own threadlocal cache
+    /*
     private static final ThreadLocal<Object2ByteLinkedOpenHashMap<Block.a>> e = ThreadLocal.withInitial(() -> {
         Object2ByteLinkedOpenHashMap<Block.a> object2bytelinkedopenhashmap = new Object2ByteLinkedOpenHashMap<Block.a>(200) {
             protected void rehash(int i) {}
@@ -53,6 +55,13 @@ public abstract class FluidTypeFlowing extends FluidType {
         object2bytelinkedopenhashmap.defaultReturnValue((byte) 127);
         return object2bytelinkedopenhashmap;
     });
+     */
+    private static final ThreadLocal<gg.airplane.structs.FluidDirectionCache<Block.a>> localFluidDirectionCache = ThreadLocal.withInitial(() -> {
+        // Airplane todo - mess with this number for performance
+        //  with 1024 it seems very infrequent on a small world that it has to remove old entries
+        return new gg.airplane.structs.FluidDirectionCache<>(1024);
+    });
+    // Airplane end
     private final Map<Fluid, VoxelShape> f = Maps.newIdentityHashMap();
 
     public FluidTypeFlowing() {}
@@ -240,6 +249,8 @@ public abstract class FluidTypeFlowing extends FluidType {
     }
 
     private boolean a(EnumDirection enumdirection, IBlockAccess iblockaccess, BlockPosition blockposition, IBlockData iblockdata, BlockPosition blockposition1, IBlockData iblockdata1) {
+        // Airplane start - modify to use our cache
+        /*
         Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap;
 
         if (!iblockdata.getBlock().o() && !iblockdata1.getBlock().o()) {
@@ -247,9 +258,16 @@ public abstract class FluidTypeFlowing extends FluidType {
         } else {
             object2bytelinkedopenhashmap = null;
         }
+         */
+        gg.airplane.structs.FluidDirectionCache<Block.a> cache = null;
+
+        if (!iblockdata.getBlock().isComplexHitbox() && !iblockdata1.getBlock().isComplexHitbox()) {
+            cache = localFluidDirectionCache.get();
+        }
 
         Block.a block_a;
 
+        /*
         if (object2bytelinkedopenhashmap != null) {
             block_a = new Block.a(iblockdata, iblockdata1, enumdirection);
             byte b0 = object2bytelinkedopenhashmap.getAndMoveToFirst(block_a);
@@ -260,11 +278,22 @@ public abstract class FluidTypeFlowing extends FluidType {
         } else {
             block_a = null;
         }
+         */
+        if (cache != null) {
+            block_a = new Block.a(iblockdata, iblockdata1, enumdirection);
+            Boolean flag = cache.getValue(block_a);
+            if (flag != null) {
+                return flag;
+            }
+        } else {
+            block_a = null;
+        }
 
         VoxelShape voxelshape = iblockdata.getCollisionShape(iblockaccess, blockposition);
         VoxelShape voxelshape1 = iblockdata1.getCollisionShape(iblockaccess, blockposition1);
         boolean flag = !VoxelShapes.b(voxelshape, voxelshape1, enumdirection);
 
+        /*
         if (object2bytelinkedopenhashmap != null) {
             if (object2bytelinkedopenhashmap.size() == 200) {
                 object2bytelinkedopenhashmap.removeLastByte();
@@ -272,6 +301,11 @@ public abstract class FluidTypeFlowing extends FluidType {
 
             object2bytelinkedopenhashmap.putAndMoveToFirst(block_a, (byte) (flag ? 1 : 0));
         }
+         */
+        if (cache != null) {
+            cache.putValue(block_a, flag);
+        }
+        // Airplane end
 
         return flag;
     }
