aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0009-MC-Utils.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/server/0009-MC-Utils.patch')
-rw-r--r--patches/server/0009-MC-Utils.patch4943
1 files changed, 2773 insertions, 2170 deletions
diff --git a/patches/server/0009-MC-Utils.patch b/patches/server/0009-MC-Utils.patch
index 653f48c157..44889e7585 100644
--- a/patches/server/0009-MC-Utils.patch
+++ b/patches/server/0009-MC-Utils.patch
@@ -12,147 +12,13 @@ public net.minecraft.server.level.ServerChunkCache mainThreadProcessor
public net.minecraft.server.level.ServerChunkCache$MainThreadExecutor
public net.minecraft.world.level.chunk.LevelChunkSection states
-diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java b/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
new file mode 100644
-index 0000000000000000000000000000000000000000..554f4d4e63c1431721989e6f502a32ccc53a8807
+index 0000000000000000000000000000000000000000..ba68998f6ef57b24c72fd833bd7de440de9501cc
--- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java
-@@ -0,0 +1,128 @@
-+package com.destroystokyo.paper.util.maplist;
-+
-+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
-+import java.util.Arrays;
-+import java.util.Iterator;
-+import java.util.NoSuchElementException;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+
-+// list with O(1) remove & contains
-+/**
-+ * @author Spottedleaf
-+ */
-+public final class ChunkList implements Iterable<LevelChunk> {
-+
-+ protected final Long2IntOpenHashMap chunkToIndex = new Long2IntOpenHashMap(2, 0.8f);
-+ {
-+ this.chunkToIndex.defaultReturnValue(Integer.MIN_VALUE);
-+ }
-+
-+ protected static final LevelChunk[] EMPTY_LIST = new LevelChunk[0];
-+
-+ protected LevelChunk[] chunks = EMPTY_LIST;
-+ protected int count;
-+
-+ public int size() {
-+ return this.count;
-+ }
-+
-+ public boolean contains(final LevelChunk chunk) {
-+ return this.chunkToIndex.containsKey(chunk.coordinateKey);
-+ }
-+
-+ public boolean remove(final LevelChunk chunk) {
-+ final int index = this.chunkToIndex.remove(chunk.coordinateKey);
-+ if (index == Integer.MIN_VALUE) {
-+ return false;
-+ }
-+
-+ // move the entity at the end to this index
-+ final int endIndex = --this.count;
-+ final LevelChunk end = this.chunks[endIndex];
-+ if (index != endIndex) {
-+ // not empty after this call
-+ this.chunkToIndex.put(end.coordinateKey, index); // update index
-+ }
-+ this.chunks[index] = end;
-+ this.chunks[endIndex] = null;
-+
-+ return true;
-+ }
-+
-+ public boolean add(final LevelChunk chunk) {
-+ final int count = this.count;
-+ final int currIndex = this.chunkToIndex.putIfAbsent(chunk.coordinateKey, count);
-+
-+ if (currIndex != Integer.MIN_VALUE) {
-+ return false; // already in this list
-+ }
-+
-+ LevelChunk[] list = this.chunks;
-+
-+ if (list.length == count) {
-+ // resize required
-+ list = this.chunks = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
-+ }
-+
-+ list[count] = chunk;
-+ this.count = count + 1;
-+
-+ return true;
-+ }
-+
-+ public LevelChunk getChecked(final int index) {
-+ if (index < 0 || index >= this.count) {
-+ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
-+ }
-+ return this.chunks[index];
-+ }
-+
-+ public LevelChunk getUnchecked(final int index) {
-+ return this.chunks[index];
-+ }
-+
-+ public LevelChunk[] getRawData() {
-+ return this.chunks;
-+ }
-+
-+ public void clear() {
-+ this.chunkToIndex.clear();
-+ Arrays.fill(this.chunks, 0, this.count, null);
-+ this.count = 0;
-+ }
-+
-+ @Override
-+ public Iterator<LevelChunk> iterator() {
-+ return new Iterator<LevelChunk>() {
-+
-+ LevelChunk lastRet;
-+ int current;
-+
-+ @Override
-+ public boolean hasNext() {
-+ return this.current < ChunkList.this.count;
-+ }
-+
-+ @Override
-+ public LevelChunk next() {
-+ if (this.current >= ChunkList.this.count) {
-+ throw new NoSuchElementException();
-+ }
-+ return this.lastRet = ChunkList.this.chunks[this.current++];
-+ }
-+
-+ @Override
-+ public void remove() {
-+ final LevelChunk lastRet = this.lastRet;
-+
-+ if (lastRet == null) {
-+ throw new IllegalStateException();
-+ }
-+ this.lastRet = null;
-+
-+ ChunkList.this.remove(lastRet);
-+ --this.current;
-+ }
-+ };
-+ }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java b/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0133ea6feb1ab88f021f66855669f58367e7420b
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java
-@@ -0,0 +1,128 @@
-+package com.destroystokyo.paper.util.maplist;
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
+@@ -0,0 +1,129 @@
++package ca.spottedleaf.moonrise.common.list;
+
+import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
+import net.minecraft.world.entity.Entity;
@@ -161,6 +27,7 @@ index 0000000000000000000000000000000000000000..0133ea6feb1ab88f021f66855669f583
+import java.util.NoSuchElementException;
+
+// list with O(1) remove & contains
++
+/**
+ * @author Spottedleaf
+ */
@@ -280,13 +147,13 @@ index 0000000000000000000000000000000000000000..0133ea6feb1ab88f021f66855669f583
+ };
+ }
+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java b/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java
new file mode 100644
-index 0000000000000000000000000000000000000000..277cfd9d1e8fff5d9b5e534b75c3c5162d58b0b7
+index 0000000000000000000000000000000000000000..fcfbca333234c09f7c056bbfcd9ac8860b20a8db
--- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java
-@@ -0,0 +1,128 @@
-+package com.destroystokyo.paper.util.maplist;
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java
+@@ -0,0 +1,125 @@
++package ca.spottedleaf.moonrise.common.list;
+
+import it.unimi.dsi.fastutil.longs.LongIterator;
+import it.unimi.dsi.fastutil.shorts.Short2LongOpenHashMap;
@@ -295,12 +162,9 @@ index 0000000000000000000000000000000000000000..277cfd9d1e8fff5d9b5e534b75c3c516
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.GlobalPalette;
+
-+/**
-+ * @author Spottedleaf
-+ */
+public final class IBlockDataList {
+
-+ static final GlobalPalette<BlockState> GLOBAL_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY);
++ private static final GlobalPalette<BlockState> GLOBAL_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY);
+
+ // map of location -> (index | (location << 16) | (palette id << 32))
+ private final Short2LongOpenHashMap map = new Short2LongOpenHashMap(2, 0.8f);
@@ -414,33 +278,365 @@ index 0000000000000000000000000000000000000000..277cfd9d1e8fff5d9b5e534b75c3c516
+ return this.map.values().iterator();
+ }
+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java b/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java
new file mode 100644
-index 0000000000000000000000000000000000000000..190c5f0b02a3d99054704ae1afbffb3498ddffe1
+index 0000000000000000000000000000000000000000..c21e00812f1aaa1279834a0562d360d6b89e146c
--- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java
-@@ -0,0 +1,125 @@
-+package com.destroystokyo.paper.util.maplist;
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java
+@@ -0,0 +1,312 @@
++package ca.spottedleaf.moonrise.common.list;
++
++import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2IntMap;
++import java.util.Arrays;
++import java.util.NoSuchElementException;
++
++public final class IteratorSafeOrderedReferenceSet<E> {
++
++ public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0;
++
++ private final Reference2IntLinkedOpenHashMap<E> indexMap;
++ private int firstInvalidIndex = -1;
++
++ /* list impl */
++ private E[] listElements;
++ private int listSize;
++
++ private final double maxFragFactor;
++
++ private int iteratorCount;
++
++ public IteratorSafeOrderedReferenceSet() {
++ this(16, 0.75f, 16, 0.2);
++ }
++
++ public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity,
++ final double maxFragFactor) {
++ this.indexMap = new Reference2IntLinkedOpenHashMap<>(setCapacity, setLoadFactor);
++ this.indexMap.defaultReturnValue(-1);
++ this.maxFragFactor = maxFragFactor;
++ this.listElements = (E[])new Object[arrayCapacity];
++ }
++
++ /*
++ public void check() {
++ int iterated = 0;
++ ReferenceOpenHashSet<E> check = new ReferenceOpenHashSet<>();
++ if (this.listElements != null) {
++ for (int i = 0; i < this.listSize; ++i) {
++ Object obj = this.listElements[i];
++ if (obj != null) {
++ iterated++;
++ if (!check.add((E)obj)) {
++ throw new IllegalStateException("contains duplicate");
++ }
++ if (!this.contains((E)obj)) {
++ throw new IllegalStateException("desync");
++ }
++ }
++ }
++ }
++
++ if (iterated != this.size()) {
++ throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size());
++ }
++
++ check.clear();
++ iterated = 0;
++ for (final java.util.Iterator<E> iterator = this.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
++ final E element = iterator.next();
++ iterated++;
++ if (!check.add(element)) {
++ throw new IllegalStateException("contains duplicate (iterator is wrong)");
++ }
++ if (!this.contains(element)) {
++ throw new IllegalStateException("desync (iterator is wrong)");
++ }
++ }
++
++ if (iterated != this.size()) {
++ throw new IllegalStateException("Size is mismatched! (iterator is wrong) Got " + iterated + ", expected " + this.size());
++ }
++ }
++ */
++
++ private double getFragFactor() {
++ return 1.0 - ((double)this.indexMap.size() / (double)this.listSize);
++ }
++
++ public int createRawIterator() {
++ ++this.iteratorCount;
++ if (this.indexMap.isEmpty()) {
++ return -1;
++ } else {
++ return this.firstInvalidIndex == 0 ? this.indexMap.getInt(this.indexMap.firstKey()) : 0;
++ }
++ }
++
++ public int advanceRawIterator(final int index) {
++ final E[] elements = this.listElements;
++ int ret = index + 1;
++ for (int len = this.listSize; ret < len; ++ret) {
++ if (elements[ret] != null) {
++ return ret;
++ }
++ }
++
++ return -1;
++ }
++
++ public void finishRawIterator() {
++ if (--this.iteratorCount == 0) {
++ if (this.getFragFactor() >= this.maxFragFactor) {
++ this.defrag();
++ }
++ }
++ }
++
++ public boolean remove(final E element) {
++ final int index = this.indexMap.removeInt(element);
++ if (index >= 0) {
++ if (this.firstInvalidIndex < 0 || index < this.firstInvalidIndex) {
++ this.firstInvalidIndex = index;
++ }
++ if (this.listElements[index] != element) {
++ throw new IllegalStateException();
++ }
++ this.listElements[index] = null;
++ if (this.iteratorCount == 0 && this.getFragFactor() >= this.maxFragFactor) {
++ this.defrag();
++ }
++ //this.check();
++ return true;
++ }
++ return false;
++ }
++
++ public boolean contains(final E element) {
++ return this.indexMap.containsKey(element);
++ }
++
++ public boolean add(final E element) {
++ final int listSize = this.listSize;
++
++ final int previous = this.indexMap.putIfAbsent(element, listSize);
++ if (previous != -1) {
++ return false;
++ }
++
++ if (listSize >= this.listElements.length) {
++ this.listElements = Arrays.copyOf(this.listElements, listSize * 2);
++ }
++ this.listElements[listSize] = element;
++ this.listSize = listSize + 1;
++
++ //this.check();
++ return true;
++ }
++
++ private void defrag() {
++ if (this.firstInvalidIndex < 0) {
++ return; // nothing to do
++ }
++
++ if (this.indexMap.isEmpty()) {
++ Arrays.fill(this.listElements, 0, this.listSize, null);
++ this.listSize = 0;
++ this.firstInvalidIndex = -1;
++ //this.check();
++ return;
++ }
++
++ final E[] backingArray = this.listElements;
++
++ int lastValidIndex;
++ java.util.Iterator<Reference2IntMap.Entry<E>> iterator;
++
++ if (this.firstInvalidIndex == 0) {
++ iterator = this.indexMap.reference2IntEntrySet().fastIterator();
++ lastValidIndex = 0;
++ } else {
++ lastValidIndex = this.firstInvalidIndex;
++ final E key = backingArray[lastValidIndex - 1];
++ iterator = this.indexMap.reference2IntEntrySet().fastIterator(new Reference2IntMap.Entry<E>() {
++ @Override
++ public int getIntValue() {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public int setValue(int i) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public E getKey() {
++ return key;
++ }
++ });
++ }
++
++ while (iterator.hasNext()) {
++ final Reference2IntMap.Entry<E> entry = iterator.next();
++
++ final int newIndex = lastValidIndex++;
++ backingArray[newIndex] = entry.getKey();
++ entry.setValue(newIndex);
++ }
++
++ // cleanup end
++ Arrays.fill(backingArray, lastValidIndex, this.listSize, null);
++ this.listSize = lastValidIndex;
++ this.firstInvalidIndex = -1;
++ //this.check();
++ }
++
++ public E rawGet(final int index) {
++ return this.listElements[index];
++ }
++
++ public int size() {
++ // always returns the correct amount - listSize can be different
++ return this.indexMap.size();
++ }
++
++ public IteratorSafeOrderedReferenceSet.Iterator<E> iterator() {
++ return this.iterator(0);
++ }
++
++ public IteratorSafeOrderedReferenceSet.Iterator<E> iterator(final int flags) {
++ ++this.iteratorCount;
++ return new BaseIterator<>(this, true, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
++ }
++
++ public java.util.Iterator<E> unsafeIterator() {
++ return this.unsafeIterator(0);
++ }
++ public java.util.Iterator<E> unsafeIterator(final int flags) {
++ return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
++ }
++
++ public static interface Iterator<E> extends java.util.Iterator<E> {
++
++ public void finishedIterating();
++
++ }
++
++ private static final class BaseIterator<E> implements IteratorSafeOrderedReferenceSet.Iterator<E> {
++
++ private final IteratorSafeOrderedReferenceSet<E> set;
++ private final boolean canFinish;
++ private final int maxIndex;
++ private int nextIndex;
++ private E pendingValue;
++ private boolean finished;
++ private E lastReturned;
++
++ private BaseIterator(final IteratorSafeOrderedReferenceSet<E> set, final boolean canFinish, final int maxIndex) {
++ this.set = set;
++ this.canFinish = canFinish;
++ this.maxIndex = maxIndex;
++ }
++
++ @Override
++ public boolean hasNext() {
++ if (this.finished) {
++ return false;
++ }
++ if (this.pendingValue != null) {
++ return true;
++ }
++
++ final E[] elements = this.set.listElements;
++ int index, len;
++ for (index = this.nextIndex, len = Math.min(this.maxIndex, this.set.listSize); index < len; ++index) {
++ final E element = elements[index];
++ if (element != null) {
++ this.pendingValue = element;
++ this.nextIndex = index + 1;
++ return true;
++ }
++ }
++
++ this.nextIndex = index;
++ return false;
++ }
++
++ @Override
++ public E next() {
++ if (!this.hasNext()) {
++ throw new NoSuchElementException();
++ }
++ final E ret = this.pendingValue;
++
++ this.pendingValue = null;
++ this.lastReturned = ret;
++
++ return ret;
++ }
++
++ @Override
++ public void remove() {
++ final E lastReturned = this.lastReturned;
++ if (lastReturned == null) {
++ throw new IllegalStateException();
++ }
++ this.lastReturned = null;
++ this.set.remove(lastReturned);
++ }
++
++ @Override
++ public void finishedIterating() {
++ if (this.finished || !this.canFinish) {
++ throw new IllegalStateException();
++ }
++ this.lastReturned = null;
++ this.finished = true;
++ this.set.finishRawIterator();
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2e876b918672e8ef3b5197b7e6b1597247fdeaa1
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
+@@ -0,0 +1,142 @@
++package ca.spottedleaf.moonrise.common.list;
+
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
-+/**
-+ * @author Spottedleaf
-+ */
+public final class ReferenceList<E> implements Iterable<E> {
+
-+ protected final Reference2IntOpenHashMap<E> referenceToIndex = new Reference2IntOpenHashMap<>(2, 0.8f);
-+ {
++ private static final Object[] EMPTY_LIST = new Object[0];
++
++ private final Reference2IntOpenHashMap<E> referenceToIndex;
++ private E[] references;
++ private int count;
++
++ public ReferenceList() {
++ this((E[])EMPTY_LIST);
++ }
++
++ public ReferenceList(final E[] referenceArray) {
++ this.references = referenceArray;
++ this.referenceToIndex = new Reference2IntOpenHashMap<>(2, 0.8f);
+ this.referenceToIndex.defaultReturnValue(Integer.MIN_VALUE);
+ }
+
-+ protected static final Object[] EMPTY_LIST = new Object[0];
++ private ReferenceList(final E[] references, final int count, final Reference2IntOpenHashMap<E> referenceToIndex) {
++ this.references = references;
++ this.count = count;
++ this.referenceToIndex = referenceToIndex;
++ }
+
-+ protected Object[] references = EMPTY_LIST;
-+ protected int count;
++ public ReferenceList<E> copy() {
++ return new ReferenceList<>(this.references.clone(), this.count, this.referenceToIndex.clone());
++ }
+
+ public int size() {
+ return this.count;
@@ -477,7 +673,7 @@ index 0000000000000000000000000000000000000000..190c5f0b02a3d99054704ae1afbffb34
+ return false; // already in this list
+ }
+
-+ Object[] list = this.references;
++ E[] list = this.references;
+
+ if (list.length == count) {
+ // resize required
@@ -494,17 +690,21 @@ index 0000000000000000000000000000000000000000..190c5f0b02a3d99054704ae1afbffb34
+ if (index < 0 || index >= this.count) {
+ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
+ }
-+ return (E)this.references[index];
++ return this.references[index];
+ }
+
+ public E getUnchecked(final int index) {
-+ return (E)this.references[index];
++ return this.references[index];
+ }
+
+ public Object[] getRawData() {
+ return this.references;
+ }
+
++ public E[] getRawDataUnchecked() {
++ return this.references;
++ }
++
+ public void clear() {
+ this.referenceToIndex.clear();
+ Arrays.fill(this.references, 0, this.count, null);
@@ -527,7 +727,7 @@ index 0000000000000000000000000000000000000000..190c5f0b02a3d99054704ae1afbffb34
+ if (this.current >= ReferenceList.this.count) {
+ throw new NoSuchElementException();
+ }
-+ return this.lastRet = (E)ReferenceList.this.references[this.current++];
++ return this.lastRet = ReferenceList.this.references[this.current++];
+ }
+
+ @Override
@@ -545,1111 +745,2255 @@ index 0000000000000000000000000000000000000000..190c5f0b02a3d99054704ae1afbffb34
+ };
+ }
+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java
new file mode 100644
-index 0000000000000000000000000000000000000000..41b9405d6759d865e0d14dd4f95163e9690e967d
+index 0000000000000000000000000000000000000000..db92261a6cb3758391108361096417c61bc82cdc
--- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java
-@@ -0,0 +1,453 @@
-+package com.destroystokyo.paper.util.misc;
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java
+@@ -0,0 +1,117 @@
++package ca.spottedleaf.moonrise.common.list;
+
-+import io.papermc.paper.util.IntegerUtil;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
-+import io.papermc.paper.util.MCUtil;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.world.level.ChunkPos;
-+import javax.annotation.Nullable;
-+import java.util.Iterator;
++import java.lang.reflect.Array;
++import java.util.Arrays;
++import java.util.Comparator;
+
-+/** @author Spottedleaf */
-+public abstract class AreaMap<E> {
++public final class SortedList<E> {
+
-+ /* Tested via https://gist.github.com/Spottedleaf/520419c6f41ef348fe9926ce674b7217 */
++ private static final Object[] EMPTY_LIST = new Object[0];
+
-+ protected final Object2LongOpenHashMap<E> objectToLastCoordinate = new Object2LongOpenHashMap<>();
-+ protected final Object2IntOpenHashMap<E> objectToViewDistance = new Object2IntOpenHashMap<>();
++ private Comparator<? super E> comparator;
++ private E[] elements;
++ private int count;
+
-+ {
-+ this.objectToViewDistance.defaultReturnValue(-1);
-+ this.objectToLastCoordinate.defaultReturnValue(Long.MIN_VALUE);
++ public SortedList(final Comparator<? super E> comparator) {
++ this((E[])EMPTY_LIST, comparator);
+ }
+
-+ // we use linked for better iteration.
-+ // map of: coordinate to set of objects in coordinate
-+ protected final Long2ObjectOpenHashMap<PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E>> areaMap = new Long2ObjectOpenHashMap<>(1024, 0.7f);
-+ protected final PooledLinkedHashSets<E> pooledHashSets;
-+
-+ protected final ChangeCallback<E> addCallback;
-+ protected final ChangeCallback<E> removeCallback;
-+ protected final ChangeSourceCallback<E> changeSourceCallback;
-+
-+ public AreaMap() {
-+ this(new PooledLinkedHashSets<>());
++ public SortedList(final E[] elements, final Comparator<? super E> comparator) {
++ this.elements = elements;
++ this.comparator = comparator;
+ }
+
-+ // let users define a "global" or "shared" pooled sets if they wish
-+ public AreaMap(final PooledLinkedHashSets<E> pooledHashSets) {
-+ this(pooledHashSets, null, null);
-+ }
++ // start, end are inclusive
++ private static <E> int insertIdx(final E[] elements, final E element, final Comparator<E> comparator,
++ int start, int end) {
++ while (start <= end) {
++ final int middle = (start + end) >>> 1;
+
-+ public AreaMap(final PooledLinkedHashSets<E> pooledHashSets, final ChangeCallback<E> addCallback, final ChangeCallback<E> removeCallback) {
-+ this(pooledHashSets, addCallback, removeCallback, null);
-+ }
-+ public AreaMap(final PooledLinkedHashSets<E> pooledHashSets, final ChangeCallback<E> addCallback, final ChangeCallback<E> removeCallback, final ChangeSourceCallback<E> changeSourceCallback) {
-+ this.pooledHashSets = pooledHashSets;
-+ this.addCallback = addCallback;
-+ this.removeCallback = removeCallback;
-+ this.changeSourceCallback = changeSourceCallback;
-+ }
++ final E middleVal = elements[middle];
+
-+ @Nullable
-+ public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getObjectsInRange(final long key) {
-+ return this.areaMap.get(key);
-+ }
++ final int cmp = comparator.compare(element, middleVal);
+
-+ @Nullable
-+ public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getObjectsInRange(final ChunkPos chunkPos) {
-+ return this.areaMap.get(MCUtil.getCoordinateKey(chunkPos));
-+ }
++ if (cmp < 0) {
++ end = middle - 1;
++ } else {
++ start = middle + 1;
++ }
++ }
+
-+ @Nullable
-+ public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getObjectsInRange(final int chunkX, final int chunkZ) {
-+ return this.areaMap.get(MCUtil.getCoordinateKey(chunkX, chunkZ));
++ return start;
+ }
+
-+ // Long.MIN_VALUE indicates the object is not mapped
-+ public final long getLastCoordinate(final E object) {
-+ return this.objectToLastCoordinate.getOrDefault(object, Long.MIN_VALUE);
++ public int size() {
++ return this.count;
+ }
+
-+ // -1 indicates the object is not mapped
-+ public final int getLastViewDistance(final E object) {
-+ return this.objectToViewDistance.getOrDefault(object, -1);
++ public boolean isEmpty() {
++ return this.count == 0;
+ }
+
-+ // returns the total number of mapped chunks
-+ public final int size() {
-+ return this.areaMap.size();
-+ }
++ public int add(final E element) {
++ E[] elements = this.elements;
++ final int count = this.count;
++ this.count = count + 1;
++ final Comparator<? super E> comparator = this.comparator;
+
-+ public final void addOrUpdate(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
-+ final int oldViewDistance = this.objectToViewDistance.put(object, viewDistance);
-+ final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ);
-+ final long oldPos = this.objectToLastCoordinate.put(object, newPos);
++ final int idx = insertIdx(elements, element, comparator, 0, count - 1);
+
-+ if (oldViewDistance == -1) {
-+ this.addObject(object, chunkX, chunkZ, Integer.MIN_VALUE, Integer.MIN_VALUE, viewDistance);
-+ this.addObjectCallback(object, chunkX, chunkZ, viewDistance);
++ if (count >= elements.length) {
++ // copy and insert at the same time
++ if (idx == count) {
++ this.elements = elements = Arrays.copyOf(elements, (int)Math.max(4L, count * 2L)); // overflow results in negative
++ elements[count] = element;
++ return idx;
++ } else {
++ final E[] newElements = (E[])Array.newInstance(elements.getClass().getComponentType(), (int)Math.max(4L, count * 2L));
++ System.arraycopy(elements, 0, newElements, 0, idx);
++ newElements[idx] = element;
++ System.arraycopy(elements, idx, newElements, idx + 1, count - idx);
++ this.elements = newElements;
++ return idx;
++ }
+ } else {
-+ this.updateObject(object, oldPos, newPos, oldViewDistance, viewDistance);
-+ this.updateObjectCallback(object, oldPos, newPos, oldViewDistance, viewDistance);
++ if (idx == count) {
++ // no copy needed
++ elements[idx] = element;
++ return idx;
++ } else {
++ // shift elements down
++ System.arraycopy(elements, idx, elements, idx + 1, count - idx);
++ elements[idx] = element;
++ return idx;
++ }
+ }
-+ //this.validate(object, viewDistance);
+ }
+
-+ public final boolean update(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
-+ final int oldViewDistance = this.objectToViewDistance.replace(object, viewDistance);
-+ if (oldViewDistance == -1) {
-+ return false;
-+ } else {
-+ final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ);
-+ final long oldPos = this.objectToLastCoordinate.put(object, newPos);
-+ this.updateObject(object, oldPos, newPos, oldViewDistance, viewDistance);
-+ this.updateObjectCallback(object, oldPos, newPos, oldViewDistance, viewDistance);
++ public E get(final int idx) {
++ if (idx < 0 || idx >= this.count) {
++ throw new IndexOutOfBoundsException(idx);
+ }
-+ //this.validate(object, viewDistance);
-+ return true;
++ return this.elements[idx];
+ }
+
-+ // called after the distance map updates
-+ protected void updateObjectCallback(final E Object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) {
-+ if (newPosition != oldPosition && this.changeSourceCallback != null) {
-+ this.changeSourceCallback.accept(Object, oldPosition, newPosition);
-+ }
-+ }
+
-+ public final boolean add(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
-+ final int oldViewDistance = this.objectToViewDistance.putIfAbsent(object, viewDistance);
-+ if (oldViewDistance != -1) {
-+ return false;
++ public E remove(final E element) {
++ E[] elements = this.elements;
++ final int count = this.count;
++ final Comparator<? super E> comparator = this.comparator;
++
++ final int idx = Arrays.binarySearch(elements, 0, count, element, comparator);
++ if (idx < 0) {
++ return null;
+ }
+
-+ final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ);
-+ this.objectToLastCoordinate.put(object, newPos);
-+ this.addObject(object, chunkX, chunkZ, Integer.MIN_VALUE, Integer.MIN_VALUE, viewDistance);
-+ this.addObjectCallback(object, chunkX, chunkZ, viewDistance);
++ final int last = this.count - 1;
++ this.count = last;
+
-+ //this.validate(object, viewDistance);
++ final E ret = elements[idx];
+
-+ return true;
++ System.arraycopy(elements, idx + 1, elements, idx, last - idx);
++
++ elements[last] = null;
++
++ return ret;
+ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..62caf61a4b0b7ebc764006ea8bbd0274594d9f4a
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2IntArraySortedMap.java
+@@ -0,0 +1,77 @@
++package ca.spottedleaf.moonrise.common.map;
+
-+ // called after the distance map updates
-+ protected void addObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {}
++import it.unimi.dsi.fastutil.ints.Int2IntFunction;
+
-+ public final boolean remove(final E object) {
-+ final long position = this.objectToLastCoordinate.removeLong(object);
-+ final int viewDistance = this.objectToViewDistance.removeInt(object);
++import java.util.Arrays;
+
-+ if (viewDistance == -1) {
-+ return false;
-+ }
++public class Int2IntArraySortedMap {
+
-+ final int currentX = MCUtil.getCoordinateX(position);
-+ final int currentZ = MCUtil.getCoordinateZ(position);
++ protected int[] key;
++ protected int[] val;
++ protected int size;
+
-+ this.removeObject(object, currentX, currentZ, currentX, currentZ, viewDistance);
-+ this.removeObjectCallback(object, currentX, currentZ, viewDistance);
-+ //this.validate(object, -1);
-+ return true;
++ public Int2IntArraySortedMap() {
++ this.key = new int[8];
++ this.val = new int[8];
+ }
+
-+ // called after the distance map updates
-+ protected void removeObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {}
++ public int put(final int key, final int value) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ final int current = this.val[index];
++ this.val[index] = value;
++ return current;
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
++
++ this.key[insert] = key;
++ this.val[insert] = value;
+
-+ protected abstract PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getEmptySetFor(final E object);
++ return 0;
++ }
+
-+ // expensive op, only for debug
-+ protected void validate(final E object, final int viewDistance) {
-+ int entiesGot = 0;
-+ int expectedEntries = (2 * viewDistance + 1);
-+ expectedEntries *= expectedEntries;
-+ if (viewDistance < 0) {
-+ expectedEntries = 0;
++ public int computeIfAbsent(final int key, final Int2IntFunction producer) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ return this.val[index];
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
+ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
+
-+ final long currPosition = this.objectToLastCoordinate.getLong(object);
++ this.key[insert] = key;
+
-+ final int centerX = MCUtil.getCoordinateX(currPosition);
-+ final int centerZ = MCUtil.getCoordinateZ(currPosition);
++ return this.val[insert] = producer.apply(key);
++ }
+
-+ for (Iterator<Long2ObjectLinkedOpenHashMap.Entry<PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E>>> iterator = this.areaMap.long2ObjectEntrySet().fastIterator();
-+ iterator.hasNext();) {
++ public int get(final int key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ return 0;
++ }
++ return this.val[index];
++ }
+
-+ final Long2ObjectLinkedOpenHashMap.Entry<PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E>> entry = iterator.next();
-+ final long key = entry.getLongKey();
-+ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> map = entry.getValue();
++ public int getFloor(final int key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ final int insert = -(index + 1) - 1;
++ return insert < 0 ? 0 : this.val[insert];
++ }
++ return this.val[index];
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..fea9e8ba7caaf6259614090d4f872619470d32f9
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Int2ObjectArraySortedMap.java
+@@ -0,0 +1,74 @@
++package ca.spottedleaf.moonrise.common.map;
+
-+ if (map.referenceCount == 0) {
-+ throw new IllegalStateException("Invalid map");
-+ }
++import java.util.Arrays;
++import java.util.function.IntFunction;
+
-+ if (map.contains(object)) {
-+ ++entiesGot;
++public class Int2ObjectArraySortedMap<V> {
+
-+ final int chunkX = MCUtil.getCoordinateX(key);
-+ final int chunkZ = MCUtil.getCoordinateZ(key);
++ protected int[] key;
++ protected V[] val;
++ protected int size;
+
-+ final int dist = Math.max(IntegerUtil.branchlessAbs(chunkX - centerX), IntegerUtil.branchlessAbs(chunkZ - centerZ));
++ public Int2ObjectArraySortedMap() {
++ this.key = new int[8];
++ this.val = (V[])new Object[8];
++ }
+
-+ if (dist > viewDistance) {
-+ throw new IllegalStateException("Expected view distance " + viewDistance + ", got " + dist);
-+ }
-+ }
++ public V put(final int key, final V value) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ final V current = this.val[index];
++ this.val[index] = value;
++ return current;
+ }
-+
-+ if (entiesGot != expectedEntries) {
-+ throw new IllegalStateException("Expected " + expectedEntries + ", got " + entiesGot);
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
+ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++
++ this.key[insert] = key;
++ this.val[insert] = value;
++
++ return null;
+ }
+
-+ private void addObjectTo(final E object, final int chunkX, final int chunkZ, final int currChunkX,
-+ final int currChunkZ, final int prevChunkX, final int prevChunkZ) {
-+ final long key = MCUtil.getCoordinateKey(chunkX, chunkZ);
++ public V computeIfAbsent(final int key, final IntFunction<V> producer) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ return this.val[index];
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
+
-+ PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> empty = this.getEmptySetFor(object);
-+ PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> current = this.areaMap.putIfAbsent(key, empty);
++ this.key[insert] = key;
+
-+ if (current != null) {
-+ PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> next = this.pooledHashSets.findMapWith(current, object);
-+ if (next == current) {
-+ throw new IllegalStateException("Expected different map: got " + next.toString());
-+ }
-+ this.areaMap.put(key, next);
++ return this.val[insert] = producer.apply(key);
++ }
+
-+ current = next;
-+ // fall through to callback
-+ } else {
-+ current = empty;
++ public V get(final int key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ return null;
+ }
++ return this.val[index];
++ }
+
-+ if (this.addCallback != null) {
-+ try {
-+ this.addCallback.accept(object, chunkX, chunkZ, currChunkX, currChunkZ, prevChunkX, prevChunkZ, current);
-+ } catch (final Throwable ex) {
-+ if (ex instanceof ThreadDeath) {
-+ throw (ThreadDeath)ex;
-+ }
-+ MinecraftServer.LOGGER.error("Add callback for map threw exception ", ex);
-+ }
++ public V getFloor(final int key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ final int insert = -(index + 1);
++ return this.val[insert];
+ }
++ return this.val[index];
+ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c077ca606934e9f13da3a8e2a194f82a99fe9ae9
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2IntArraySortedMap.java
+@@ -0,0 +1,77 @@
++package ca.spottedleaf.moonrise.common.map;
+
-+ private void removeObjectFrom(final E object, final int chunkX, final int chunkZ, final int currChunkX,
-+ final int currChunkZ, final int prevChunkX, final int prevChunkZ) {
-+ final long key = MCUtil.getCoordinateKey(chunkX, chunkZ);
++import it.unimi.dsi.fastutil.longs.Long2IntFunction;
+
-+ PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> current = this.areaMap.get(key);
++import java.util.Arrays;
+
-+ if (current == null) {
-+ throw new IllegalStateException("Current map may not be null for " + object + ", (" + chunkX + "," + chunkZ + ")");
-+ }
++public class Long2IntArraySortedMap {
+
-+ PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> next = this.pooledHashSets.findMapWithout(current, object);
++ protected long[] key;
++ protected int[] val;
++ protected int size;
+
-+ if (next == current) {
-+ throw new IllegalStateException("Current map [" + next.toString() + "] should have contained " + object + ", (" + chunkX + "," + chunkZ + ")");
-+ }
++ public Long2IntArraySortedMap() {
++ this.key = new long[8];
++ this.val = new int[8];
++ }
+
-+ if (next != null) {
-+ this.areaMap.put(key, next);
-+ } else {
-+ this.areaMap.remove(key);
++ public int put(final long key, final int value) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ final int current = this.val[index];
++ this.val[index] = value;
++ return current;
+ }
-+
-+ if (this.removeCallback != null) {
-+ try {
-+ this.removeCallback.accept(object, chunkX, chunkZ, currChunkX, currChunkZ, prevChunkX, prevChunkZ, next);
-+ } catch (final Throwable ex) {
-+ if (ex instanceof ThreadDeath) {
-+ throw (ThreadDeath)ex;
-+ }
-+ MinecraftServer.LOGGER.error("Remove callback for map threw exception ", ex);
-+ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
+ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
++
++ this.key[insert] = key;
++ this.val[insert] = value;
++
++ return 0;
+ }
+
-+ private void addObject(final E object, final int chunkX, final int chunkZ, final int prevChunkX, final int prevChunkZ, final int viewDistance) {
-+ final int maxX = chunkX + viewDistance;
-+ final int maxZ = chunkZ + viewDistance;
-+ final int minX = chunkX - viewDistance;
-+ final int minZ = chunkZ - viewDistance;
-+ for (int x = minX; x <= maxX; ++x) {
-+ for (int z = minZ; z <= maxZ; ++z) {
-+ this.addObjectTo(object, x, z, chunkX, chunkZ, prevChunkX, prevChunkZ);
-+ }
++ public int computeIfAbsent(final long key, final Long2IntFunction producer) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ return this.val[index];
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
+ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
++
++ this.key[insert] = key;
++
++ return this.val[insert] = producer.apply(key);
+ }
+
-+ private void removeObject(final E object, final int chunkX, final int chunkZ, final int currentChunkX, final int currentChunkZ, final int viewDistance) {
-+ final int maxX = chunkX + viewDistance;
-+ final int maxZ = chunkZ + viewDistance;
-+ final int minX = chunkX - viewDistance;
-+ final int minZ = chunkZ - viewDistance;
-+ for (int x = minX; x <= maxX; ++x) {
-+ for (int z = minZ; z <= maxZ; ++z) {
-+ this.removeObjectFrom(object, x, z, currentChunkX, currentChunkZ, chunkX, chunkZ);
-+ }
++ public int get(final long key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ return 0;
+ }
++ return this.val[index];
+ }
+
-+ /* math sign function except 0 returns 1 */
-+ protected static int sign(int val) {
-+ return 1 | (val >> (Integer.SIZE - 1));
++ public int getFloor(final long key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ final int insert = -(index + 1) - 1;
++ return insert < 0 ? 0 : this.val[insert];
++ }
++ return this.val[index];
+ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b24d037af5709196b66c79c692e1814cd5b20e49
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/Long2ObjectArraySortedMap.java
+@@ -0,0 +1,76 @@
++package ca.spottedleaf.moonrise.common.map;
+
-+ private void updateObject(final E object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) {
-+ final int toX = MCUtil.getCoordinateX(newPosition);
-+ final int toZ = MCUtil.getCoordinateZ(newPosition);
-+ final int fromX = MCUtil.getCoordinateX(oldPosition);
-+ final int fromZ = MCUtil.getCoordinateZ(oldPosition);
++import java.util.Arrays;
++import java.util.function.LongFunction;
+
-+ final int dx = toX - fromX;
-+ final int dz = toZ - fromZ;
++public class Long2ObjectArraySortedMap<V> {
+
-+ final int totalX = IntegerUtil.branchlessAbs(fromX - toX);
-+ final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
++ protected long[] key;
++ protected V[] val;
++ protected int size;
+
-+ if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
-+ // teleported?
-+ this.removeObject(object, fromX, fromZ, fromX, fromZ, oldViewDistance);
-+ this.addObject(object, toX, toZ, fromX, fromZ, newViewDistance);
-+ return;
++ public Long2ObjectArraySortedMap() {
++ this.key = new long[8];
++ this.val = (V[])new Object[8];
++ }
++
++ public V put(final long key, final V value) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ final V current = this.val[index];
++ this.val[index] = value;
++ return current;
+ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
+
-+ if (oldViewDistance != newViewDistance) {
-+ // remove loop
++ this.key[insert] = key;
++ this.val[insert] = value;
+
-+ final int oldMinX = fromX - oldViewDistance;
-+ final int oldMinZ = fromZ - oldViewDistance;
-+ final int oldMaxX = fromX + oldViewDistance;
-+ final int oldMaxZ = fromZ + oldViewDistance;
-+ for (int currX = oldMinX; currX <= oldMaxX; ++currX) {
-+ for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) {
++ return null;
++ }
+
-+ // only remove if we're outside the new view distance...
-+ if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) {
-+ this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ);
-+ }
-+ }
-+ }
++ public V computeIfAbsent(final long key, final LongFunction<V> producer) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index >= 0) {
++ return this.val[index];
++ }
++ final int insert = -(index + 1);
++ // shift entries down
++ if (this.size >= this.val.length) {
++ this.key = Arrays.copyOf(this.key, this.key.length * 2);
++ this.val = Arrays.copyOf(this.val, this.val.length * 2);
++ }
++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert);
++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert);
++ ++this.size;
+
-+ // add loop
++ this.key[insert] = key;
+
-+ final int newMinX = toX - newViewDistance;
-+ final int newMinZ = toZ - newViewDistance;
-+ final int newMaxX = toX + newViewDistance;
-+ final int newMaxZ = toZ + newViewDistance;
-+ for (int currX = newMinX; currX <= newMaxX; ++currX) {
-+ for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) {
++ return this.val[insert] = producer.apply(key);
++ }
+
-+ // only add if we're outside the old view distance...
-+ if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) {
-+ this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ);
-+ }
-+ }
-+ }
++ public V get(final long key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ return null;
++ }
++ return this.val[index];
++ }
+
-+ return;
++ public V getFloor(final long key) {
++ final int index = Arrays.binarySearch(this.key, 0, this.size, key);
++ if (index < 0) {
++ final int insert = -(index + 1) - 1;
++ return insert < 0 ? null : this.val[insert];
+ }
++ return this.val[index];
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..aa86882bb7b0712f29d7344009093c0e7a81be84
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2BooleanMap.java
+@@ -0,0 +1,48 @@
++package ca.spottedleaf.moonrise.common.map;
+
-+ // x axis is width
-+ // z axis is height
-+ // right refers to the x axis of where we moved
-+ // top refers to the z axis of where we moved
++import it.unimi.dsi.fastutil.longs.Long2BooleanFunction;
++import it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap;
+
-+ // same view distance
++public final class SynchronisedLong2BooleanMap {
++ private final Long2BooleanLinkedOpenHashMap map = new Long2BooleanLinkedOpenHashMap();
++ private final int limit;
+
-+ // used for relative positioning
-+ final int up = sign(dz); // 1 if dz >= 0, -1 otherwise
-+ final int right = sign(dx); // 1 if dx >= 0, -1 otherwise
++ public SynchronisedLong2BooleanMap(final int limit) {
++ this.limit = limit;
++ }
+
-+ // The area excluded by overlapping the two view distance squares creates four rectangles:
-+ // Two on the left, and two on the right. The ones on the left we consider the "removed" section
-+ // and on the right the "added" section.
-+ // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually
-+ // exclusive to the regions they surround.
++ // must hold lock on map
++ private void purgeEntries() {
++ while (this.map.size() > this.limit) {
++ this.map.removeLastBoolean();
++ }
++ }
+
-+ // 4 points of the rectangle
-+ int maxX; // exclusive
-+ int minX; // inclusive
-+ int maxZ; // exclusive
-+ int minZ; // inclusive
++ public boolean remove(final long key) {
++ synchronized (this.map) {
++ return this.map.remove(key);
++ }
++ }
+
-+ if (dx != 0) {
-+ // handle right addition
++ // note:
++ public boolean getOrCompute(final long key, final Long2BooleanFunction ifAbsent) {
++ synchronized (this.map) {
++ if (this.map.containsKey(key)) {
++ return this.map.getAndMoveToFirst(key);
++ }
++ }
+
-+ maxX = toX + (oldViewDistance * right) + right; // exclusive
-+ minX = fromX + (oldViewDistance * right) + right; // inclusive
-+ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
-+ minZ = toZ - (oldViewDistance * up); // inclusive
++ final boolean put = ifAbsent.get(key);
+
-+ for (int currX = minX; currX != maxX; currX += right) {
-+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+ this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ);
-+ }
++ synchronized (this.map) {
++ if (this.map.containsKey(key)) {
++ return this.map.getAndMoveToFirst(key);
+ }
++ this.map.putAndMoveToFirst(key, put);
++
++ this.purgeEntries();
++
++ return put;
+ }
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..dbb51afc6cefe0071fe3ddcd2c1109f2755c3b4d
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/map/SynchronisedLong2ObjectMap.java
+@@ -0,0 +1,47 @@
++package ca.spottedleaf.moonrise.common.map;
+
-+ if (dz != 0) {
-+ // handle up addition
++import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
++import java.util.function.BiFunction;
+
-+ maxX = toX + (oldViewDistance * right) + right; // exclusive
-+ minX = toX - (oldViewDistance * right); // inclusive
-+ maxZ = toZ + (oldViewDistance * up) + up; // exclusive
-+ minZ = fromZ + (oldViewDistance * up) + up; // inclusive
++public final class SynchronisedLong2ObjectMap<V> {
++ private final Long2ObjectLinkedOpenHashMap<V> map = new Long2ObjectLinkedOpenHashMap<>();
++ private final int limit;
+
-+ for (int currX = minX; currX != maxX; currX += right) {
-+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+ this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ);
-+ }
-+ }
++ public SynchronisedLong2ObjectMap(final int limit) {
++ this.limit = limit;
++ }
++
++ // must hold lock on map
++ private void purgeEntries() {
++ while (this.map.size() > this.limit) {
++ this.map.removeLast();
+ }
++ }
+
-+ if (dx != 0) {
-+ // handle left removal
++ public V get(final long key) {
++ synchronized (this.map) {
++ return this.map.getAndMoveToFirst(key);
++ }
++ }
+
-+ maxX = toX - (oldViewDistance * right); // exclusive
-+ minX = fromX - (oldViewDistance * right); // inclusive
-+ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
-+ minZ = toZ - (oldViewDistance * up); // inclusive
++ public V put(final long key, final V value) {
++ synchronized (this.map) {
++ final V ret = this.map.putAndMoveToFirst(key, value);
++ this.purgeEntries();
++ return ret;
++ }
++ }
+
-+ for (int currX = minX; currX != maxX; currX += right) {
-+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+ this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ);
-+ }
-+ }
++ public V compute(final long key, final BiFunction<? super Long, ? super V, ? extends V> remappingFunction) {
++ synchronized (this.map) {
++ // first, compute the value - if one is added, it will be at the last entry
++ this.map.compute(key, remappingFunction);
++ // move the entry to first, just in case it was added at last
++ final V ret = this.map.getAndMoveToFirst(key);
++ // now purge the last entries
++ this.purgeEntries();
++
++ return ret;
+ }
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9c0eff9017b24bb65b1029cefb5d0bfcb9beff01
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/AllocatingRateLimiter.java
+@@ -0,0 +1,75 @@
++package ca.spottedleaf.moonrise.common.misc;
+
-+ if (dz != 0) {
-+ // handle down removal
++public final class AllocatingRateLimiter {
+
-+ maxX = fromX + (oldViewDistance * right) + right; // exclusive
-+ minX = fromX - (oldViewDistance * right); // inclusive
-+ maxZ = toZ - (oldViewDistance * up); // exclusive
-+ minZ = fromZ - (oldViewDistance * up); // inclusive
++ // max difference granularity in ns
++ private final long maxGranularity;
+
-+ for (int currX = minX; currX != maxX; currX += right) {
-+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+ this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ);
-+ }
-+ }
-+ }
++ private double allocation = 0.0;
++ private long lastAllocationUpdate;
++ // the carry is used to store the remainder of the last take, so that the take amount remains the same (minus floating point error)
++ // over any time period using take regardless of the number of take calls or the intervals between the take calls
++ // i.e. take obtains 3.5 elements, stores 0.5 to this field for the next take() call to use and returns 3
++ private double takeCarry = 0.0;
++ private long lastTakeUpdate;
++
++ public AllocatingRateLimiter(final long maxGranularity) {
++ this.maxGranularity = maxGranularity;
+ }
+
-+ @FunctionalInterface
-+ public static interface ChangeCallback<E> {
++ public void reset(final long time) {
++ this.allocation = 0.0;
++ this.lastAllocationUpdate = time;
++ this.takeCarry = 0.0;
++ this.lastTakeUpdate = time;
++ }
+
-+ // if there is no previous position, then prevPos = Integer.MIN_VALUE
-+ void accept(final E object, final int rangeX, final int rangeZ, final int currPosX, final int currPosZ, final int prevPosX, final int prevPosZ,
-+ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> newState);
++ // rate in units/s, and time in ns
++ public void tickAllocation(final long time, final double rate, final double maxAllocation) {
++ final long diff = Math.min(this.maxGranularity, time - this.lastAllocationUpdate);
++ this.lastAllocationUpdate = time;
+
++ this.allocation = Math.min(maxAllocation - this.takeCarry, this.allocation + rate * (diff*1.0E-9D));
+ }
+
-+ @FunctionalInterface
-+ public static interface ChangeSourceCallback<E> {
-+ void accept(final E object, final long prevPos, final long newPos);
++ public long previewAllocation(final long time, final double rate, final long maxTake) {
++ if (maxTake < 1L) {
++ return 0L;
++ }
++
++ final long diff = Math.min(this.maxGranularity, time - this.lastTakeUpdate);
++
++ // note: abs(takeCarry) <= 1.0
++ final double take = Math.min(
++ Math.min((double)maxTake - this.takeCarry, this.allocation),
++ rate * (diff*1.0E-9)
++ );
++
++ return (long)Math.floor(this.takeCarry + take);
++ }
++
++ // rate in units/s, and time in ns
++ public long takeAllocation(final long time, final double rate, final long maxTake) {
++ if (maxTake < 1L) {
++ return 0L;
++ }
++
++ double ret = this.takeCarry;
++ final long diff = Math.min(this.maxGranularity, time - this.lastTakeUpdate);
++ this.lastTakeUpdate = time;
++
++ // note: abs(takeCarry) <= 1.0
++ final double take = Math.min(
++ Math.min((double)maxTake - this.takeCarry, this.allocation),
++ rate * (diff*1.0E-9)
++ );
++
++ ret += take;
++ this.allocation -= take;
++
++ final long retInteger = (long)Math.floor(ret);
++ this.takeCarry = ret - (double)retInteger;
++
++ return retInteger;
+ }
+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java
new file mode 100644
-index 0000000000000000000000000000000000000000..896c3ff7ddb07f1f6f05f90e1e3fe7fb615071d4
+index 0000000000000000000000000000000000000000..460e27ab0506c83a28934800ee74ee886d4b025e
--- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java
-@@ -0,0 +1,175 @@
-+package com.destroystokyo.paper.util.misc;
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java
+@@ -0,0 +1,297 @@
++package ca.spottedleaf.moonrise.common.misc;
+
-+import io.papermc.paper.util.IntegerUtil;
-+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
-+import io.papermc.paper.util.MCUtil;
-+import net.minecraft.world.level.ChunkPos;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
+
-+/** @author Spottedleaf */
-+public abstract class DistanceTrackingAreaMap<E> extends AreaMap<E> {
++public final class Delayed26WayDistancePropagator3D {
+
-+ // use this map only if you need distance tracking, the tracking here is obviously going to hit harder.
++ // this map is considered "stale" unless updates are propagated.
++ protected final Delayed8WayDistancePropagator2D.LevelMap levels = new Delayed8WayDistancePropagator2D.LevelMap(8192*2, 0.6f);
++
++ // this map is never stale
++ protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f);
++
++ // Generally updates to positions are made close to other updates, so we link to decrease cache misses when
++ // propagating updates
++ protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet();
++
++ @FunctionalInterface
++ public static interface LevelChangeCallback {
++
++ /**
++ * This can be called for intermediate updates. So do not rely on newLevel being close to or
++ * the exact level that is expected after a full propagation has occured.
++ */
++ public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel);
+
-+ protected final Long2IntOpenHashMap chunkToNearestDistance = new Long2IntOpenHashMap(1024, 0.7f);
-+ {
-+ this.chunkToNearestDistance.defaultReturnValue(-1);
+ }
+
-+ protected final DistanceChangeCallback<E> distanceChangeCallback;
++ protected final LevelChangeCallback changeCallback;
+
-+ public DistanceTrackingAreaMap() {
-+ this(new PooledLinkedHashSets<>());
++ public Delayed26WayDistancePropagator3D() {
++ this(null);
+ }
+
-+ // let users define a "global" or "shared" pooled sets if they wish
-+ public DistanceTrackingAreaMap(final PooledLinkedHashSets<E> pooledHashSets) {
-+ this(pooledHashSets, null, null, null);
++ public Delayed26WayDistancePropagator3D(final LevelChangeCallback changeCallback) {
++ this.changeCallback = changeCallback;
+ }
+
-+ public DistanceTrackingAreaMap(final PooledLinkedHashSets<E> pooledHashSets, final ChangeCallback<E> addCallback, final ChangeCallback<E> removeCallback,
-+ final DistanceChangeCallback<E> distanceChangeCallback) {
-+ super(pooledHashSets, addCallback, removeCallback);
-+ this.distanceChangeCallback = distanceChangeCallback;
++ public int getLevel(final long pos) {
++ return this.levels.get(pos);
+ }
+
-+ // ret -1 if there is nothing mapped
-+ public final int getNearestObjectDistance(final long key) {
-+ return this.chunkToNearestDistance.get(key);
++ public int getLevel(final int x, final int y, final int z) {
++ return this.levels.get(CoordinateUtils.getChunkSectionKey(x, y, z));
+ }
+
-+ // ret -1 if there is nothing mapped
-+ public final int getNearestObjectDistance(final ChunkPos chunkPos) {
-+ return this.chunkToNearestDistance.get(MCUtil.getCoordinateKey(chunkPos));
++ public void setSource(final int x, final int y, final int z, final int level) {
++ this.setSource(CoordinateUtils.getChunkSectionKey(x, y, z), level);
+ }
+
-+ // ret -1 if there is nothing mapped
-+ public final int getNearestObjectDistance(final int chunkX, final int chunkZ) {
-+ return this.chunkToNearestDistance.get(MCUtil.getCoordinateKey(chunkX, chunkZ));
++ public void setSource(final long coordinate, final int level) {
++ if ((level & 63) != level || level == 0) {
++ throw new IllegalArgumentException("Level must be in (0, 63], not " + level);
++ }
++
++ final byte byteLevel = (byte)level;
++ final byte oldLevel = this.sources.put(coordinate, byteLevel);
++
++ if (oldLevel == byteLevel) {
++ return; // nothing to do
++ }
++
++ // queue to update later
++ this.updatedSources.add(coordinate);
+ }
+
-+ protected final void recalculateDistance(final int chunkX, final int chunkZ) {
-+ final long key = MCUtil.getCoordinateKey(chunkX, chunkZ);
-+ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> state = this.areaMap.get(key);
-+ if (state == null) {
-+ final int oldDistance = this.chunkToNearestDistance.remove(key);
-+ // nothing here.
-+ if (oldDistance == -1) {
-+ // nothing was here previously
-+ return;
-+ }
-+ if (this.distanceChangeCallback != null) {
-+ this.distanceChangeCallback.accept(chunkX, chunkZ, oldDistance, -1, null);
-+ }
-+ return;
++ public void removeSource(final int x, final int y, final int z) {
++ this.removeSource(CoordinateUtils.getChunkSectionKey(x, y, z));
++ }
++
++ public void removeSource(final long coordinate) {
++ if (this.sources.remove(coordinate) != 0) {
++ this.updatedSources.add(coordinate);
+ }
++ }
+
-+ int newDistance = Integer.MAX_VALUE;
++ // queues used for BFS propagating levels
++ protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelIncreaseWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) {
++ this.levelIncreaseWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue();
++ }
++ }
++ protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelRemoveWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) {
++ this.levelRemoveWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue();
++ }
++ }
++ protected long levelIncreaseWorkQueueBitset;
++ protected long levelRemoveWorkQueueBitset;
+
-+ final Object[] rawData = state.getBackingSet();
-+ for (int i = 0, len = rawData.length; i < len; ++i) {
-+ final Object raw = rawData[i];
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) {
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
+
-+ if (raw == null) {
-+ continue;
-+ }
++ this.levelIncreaseWorkQueueBitset |= (1L << level);
++ }
+
-+ final E object = (E)raw;
-+ final long location = this.objectToLastCoordinate.getLong(object);
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) {
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[index];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
+
-+ final int distance = Math.max(IntegerUtil.branchlessAbs(chunkX - MCUtil.getCoordinateX(location)), IntegerUtil.branchlessAbs(chunkZ - MCUtil.getCoordinateZ(location)));
++ this.levelIncreaseWorkQueueBitset |= (1L << index);
++ }
+
-+ if (distance < newDistance) {
-+ newDistance = distance;
-+ }
++ protected final void addToRemoveWorkQueue(final long coordinate, final byte level) {
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelRemoveWorkQueueBitset |= (1L << level);
++ }
++
++ public boolean propagateUpdates() {
++ if (this.updatedSources.isEmpty()) {
++ return false;
+ }
+
-+ final int oldDistance = this.chunkToNearestDistance.put(key, newDistance);
++ boolean ret = false;
++
++ for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) {
++ final long coordinate = iterator.nextLong();
+
-+ if (oldDistance != newDistance) {
-+ if (this.distanceChangeCallback != null) {
-+ this.distanceChangeCallback.accept(chunkX, chunkZ, oldDistance, newDistance, state);
++ final byte currentLevel = this.levels.get(coordinate);
++ final byte updatedSource = this.sources.get(coordinate);
++
++ if (currentLevel == updatedSource) {
++ continue;
++ }
++ ret = true;
++
++ if (updatedSource > currentLevel) {
++ // level increase
++ this.addToIncreaseWorkQueue(coordinate, updatedSource);
++ } else {
++ // level decrease
++ this.addToRemoveWorkQueue(coordinate, currentLevel);
++ // if the current coordinate is a source, then the decrease propagation will detect that and queue
++ // the source propagation
+ }
+ }
++
++ this.updatedSources.clear();
++
++ // propagate source level increases first for performance reasons (in crowded areas hopefully the additions
++ // make the removes remove less)
++ this.propagateIncreases();
++
++ // now we propagate the decreases (which will then re-propagate clobbered sources)
++ this.propagateDecreases();
++
++ return ret;
+ }
+
-+ @Override
-+ protected void addObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
-+ final int maxX = chunkX + viewDistance;
-+ final int maxZ = chunkZ + viewDistance;
-+ final int minX = chunkX - viewDistance;
-+ final int minZ = chunkZ - viewDistance;
-+ for (int x = minX; x <= maxX; ++x) {
-+ for (int z = minZ; z <= maxZ; ++z) {
-+ this.recalculateDistance(x, z);
++ protected void propagateIncreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset);
++ this.levelIncreaseWorkQueueBitset != 0L;
++ this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) {
++
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ byte level = queue.queuedLevels.removeFirstByte();
++
++ final boolean neighbourCheck = level < 0;
++
++ final byte currentLevel;
++ if (neighbourCheck) {
++ level = (byte)-level;
++ currentLevel = this.levels.get(coordinate);
++ } else {
++ currentLevel = this.levels.putIfGreater(coordinate, level);
++ }
++
++ if (neighbourCheck) {
++ // used when propagating from decrease to indicate that this level needs to check its neighbours
++ // this means the level at coordinate could be equal, but would still need neighbours checked
++
++ if (currentLevel != level) {
++ // something caused the level to change, which means something propagated to it (which means
++ // us propagating here is redundant), or something removed the level (which means we
++ // cannot propagate further)
++ continue;
++ }
++ } else if (currentLevel >= level) {
++ // something higher/equal propagated
++ continue;
++ }
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, level);
++ }
++
++ if (level == 1) {
++ // can't propagate 0 to neighbours
++ continue;
++ }
++
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = CoordinateUtils.getChunkSectionX(coordinate);
++ final int y = CoordinateUtils.getChunkSectionY(coordinate);
++ final int z = CoordinateUtils.getChunkSectionZ(coordinate);
++
++ for (int dy = -1; dy <= 1; ++dy) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ if ((dy | dz | dx) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z);
++ this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
++ }
+ }
+ }
+ }
+
-+ @Override
-+ protected void removeObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
-+ final int maxX = chunkX + viewDistance;
-+ final int maxZ = chunkZ + viewDistance;
-+ final int minX = chunkX - viewDistance;
-+ final int minZ = chunkZ - viewDistance;
-+ for (int x = minX; x <= maxX; ++x) {
-+ for (int z = minZ; z <= maxZ; ++z) {
-+ this.recalculateDistance(x, z);
++ protected void propagateDecreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset);
++ this.levelRemoveWorkQueueBitset != 0L;
++ this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) {
++
++ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ final byte level = queue.queuedLevels.removeFirstByte();
++
++ final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level);
++ if (currentLevel == 0) {
++ // something else removed
++ continue;
++ }
++
++ if (currentLevel > level) {
++ // something higher propagated here or we hit the propagation of another source
++ // in the second case we need to re-propagate because we could have just clobbered another source's
++ // propagation
++ this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking
++ continue;
++ }
++
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0);
++ }
++
++ final byte source = this.sources.get(coordinate);
++ if (source != 0) {
++ // must re-propagate source later
++ this.addToIncreaseWorkQueue(coordinate, source);
++ }
++
++ if (level == 0) {
++ // can't propagate -1 to neighbours
++ // we have to check neighbours for removing 1 just in case the neighbour is 2
++ continue;
++ }
++
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = CoordinateUtils.getChunkSectionX(coordinate);
++ final int y = CoordinateUtils.getChunkSectionY(coordinate);
++ final int z = CoordinateUtils.getChunkSectionZ(coordinate);
++
++ for (int dy = -1; dy <= 1; ++dy) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ if ((dy | dz | dx) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z);
++ this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
++ }
+ }
+ }
++
++ // propagate sources we clobbered in the process
++ this.propagateIncreases();
+ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ab2fa1563d5e32a5313dfcc1da411cab45fb5ca0
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed8WayDistancePropagator2D.java
+@@ -0,0 +1,718 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import it.unimi.dsi.fastutil.HashCommon;
++import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue;
++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
++import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
+
-+ @Override
-+ protected void updateObjectCallback(final E object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) {
-+ if (oldPosition == newPosition && newViewDistance == oldViewDistance) {
-+ return;
++public final class Delayed8WayDistancePropagator2D {
++
++ // Test
++ /*
++ protected static void test(int x, int z, com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference, Delayed8WayDistancePropagator2D test) {
++ int got = test.getLevel(x, z);
++
++ int expect = 0;
++ Object[] nearest = reference.getObjectsInRange(x, z) == null ? null : reference.getObjectsInRange(x, z).getBackingSet();
++ if (nearest != null) {
++ for (Object _obj : nearest) {
++ if (_obj instanceof Ticket) {
++ Ticket ticket = (Ticket)_obj;
++ long ticketCoord = reference.getLastCoordinate(ticket);
++ int viewDistance = reference.getLastViewDistance(ticket);
++ int distance = Math.max(com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateX(ticketCoord) - x),
++ com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateZ(ticketCoord) - z));
++ int level = viewDistance - distance;
++ if (level > expect) {
++ expect = level;
++ }
++ }
++ }
+ }
+
-+ final int toX = MCUtil.getCoordinateX(newPosition);
-+ final int toZ = MCUtil.getCoordinateZ(newPosition);
-+ final int fromX = MCUtil.getCoordinateX(oldPosition);
-+ final int fromZ = MCUtil.getCoordinateZ(oldPosition);
++ if (expect != got) {
++ throw new IllegalStateException("Expected " + expect + " at pos (" + x + "," + z + ") but got " + got);
++ }
++ }
+
-+ final int totalX = IntegerUtil.branchlessAbs(fromX - toX);
-+ final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
++ static class Ticket {
+
-+ if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
-+ // teleported?
-+ this.removeObjectCallback(object, fromX, fromZ, oldViewDistance);
-+ this.addObjectCallback(object, toX, toZ, newViewDistance);
-+ return;
++ int x;
++ int z;
++
++ final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> empty
++ = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this);
++
++ }
++
++ public static void main(final String[] args) {
++ com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket>() {
++ @Override
++ protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> getEmptySetFor(Ticket object) {
++ return object.empty;
++ }
++ };
++ Delayed8WayDistancePropagator2D test = new Delayed8WayDistancePropagator2D();
++
++ final int maxDistance = 64;
++ // test origin
++ {
++ Ticket originTicket = new Ticket();
++ int originDistance = 31;
++ // test single source
++ reference.add(originTicket, 0, 0, originDistance);
++ test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ // test single source decrease
++ reference.update(originTicket, 0, 0, originDistance/2);
++ test.setSource(0, 0, originDistance/2); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ // test source increase
++ originDistance = 2*originDistance;
++ reference.update(originTicket, 0, 0, originDistance);
++ test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) {
++ for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ reference.remove(originTicket);
++ test.removeSource(0, 0); test.propagateUpdates();
+ }
+
-+ final int minX = Math.min(fromX - oldViewDistance, toX - newViewDistance);
-+ final int maxX = Math.max(fromX + oldViewDistance, toX + newViewDistance);
-+ final int minZ = Math.min(fromZ - oldViewDistance, toZ - newViewDistance);
-+ final int maxZ = Math.max(fromZ + oldViewDistance, toZ + newViewDistance);
++ // test multiple sources at origin
++ {
++ int originDistance = 31;
++ java.util.List<Ticket> list = new java.util.ArrayList<>();
++ for (int i = 0; i < 10; ++i) {
++ Ticket a = new Ticket();
++ list.add(a);
++ a.x = (i & 1) == 1 ? -i : i;
++ a.z = (i & 1) == 1 ? -i : i;
++ }
++ for (Ticket ticket : list) {
++ reference.add(ticket, ticket.x, ticket.z, originDistance);
++ test.setSource(ticket.x, ticket.z, originDistance);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket level decrease
+
-+ for (int x = minX; x <= maxX; ++x) {
-+ for (int z = minZ; z <= maxZ; ++z) {
-+ final int distXOld = IntegerUtil.branchlessAbs(x - fromX);
-+ final int distZOld = IntegerUtil.branchlessAbs(z - fromZ);
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance/2);
++ test.setSource(ticket.x, ticket.z, originDistance/2);
++ }
++ test.propagateUpdates();
+
-+ if (Math.max(distXOld, distZOld) <= oldViewDistance) {
-+ this.recalculateDistance(x, z);
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket level increase
++
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance*2);
++ test.setSource(ticket.x, ticket.z, originDistance*2);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket remove
++ for (int i = 0, len = list.size(); i < len; ++i) {
++ if ((i & 3) != 0) {
+ continue;
+ }
++ Ticket ticket = list.get(i);
++ reference.remove(ticket);
++ test.removeSource(ticket.x, ticket.z);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++ }
++
++ // now test at coordinate offsets
++ // test offset
++ {
++ Ticket originTicket = new Ticket();
++ int originDistance = 31;
++ int offX = 54432;
++ int offZ = -134567;
++ // test single source
++ reference.add(originTicket, offX, offZ, originDistance);
++ test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx + offX, dz + offZ, reference, test);
++ }
++ }
++ // test single source decrease
++ reference.update(originTicket, offX, offZ, originDistance/2);
++ test.setSource(offX, offZ, originDistance/2); test.propagateUpdates(); // set and propagate
++ for (int dx = -originDistance; dx <= originDistance; ++dx) {
++ for (int dz = -originDistance; dz <= originDistance; ++dz) {
++ test(dx + offX, dz + offZ, reference, test);
++ }
++ }
++ // test source increase
++ originDistance = 2*originDistance;
++ reference.update(originTicket, offX, offZ, originDistance);
++ test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate
++ for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) {
++ for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) {
++ test(dx + offX, dz + offZ, reference, test);
++ }
++ }
++
++ reference.remove(originTicket);
++ test.removeSource(offX, offZ); test.propagateUpdates();
++ }
++
++ // test multiple sources at origin
++ {
++ int originDistance = 31;
++ int offX = 54432;
++ int offZ = -134567;
++ java.util.List<Ticket> list = new java.util.ArrayList<>();
++ for (int i = 0; i < 10; ++i) {
++ Ticket a = new Ticket();
++ list.add(a);
++ a.x = offX + ((i & 1) == 1 ? -i : i);
++ a.z = offZ + ((i & 1) == 1 ? -i : i);
++ }
++ for (Ticket ticket : list) {
++ reference.add(ticket, ticket.x, ticket.z, originDistance);
++ test.setSource(ticket.x, ticket.z, originDistance);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket level decrease
++
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance/2);
++ test.setSource(ticket.x, ticket.z, originDistance/2);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) {
++ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket level increase
+
-+ final int distXNew = IntegerUtil.branchlessAbs(x - toX);
-+ final int distZNew = IntegerUtil.branchlessAbs(z - toZ);
++ for (Ticket ticket : list) {
++ reference.update(ticket, ticket.x, ticket.z, originDistance*2);
++ test.setSource(ticket.x, ticket.z, originDistance*2);
++ }
++ test.propagateUpdates();
+
-+ if (Math.max(distXNew, distZNew) <= newViewDistance) {
-+ this.recalculateDistance(x, z);
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
++ }
++
++ // test ticket remove
++ for (int i = 0, len = list.size(); i < len; ++i) {
++ if ((i & 3) != 0) {
+ continue;
+ }
++ Ticket ticket = list.get(i);
++ reference.remove(ticket);
++ test.removeSource(ticket.x, ticket.z);
++ }
++ test.propagateUpdates();
++
++ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) {
++ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) {
++ test(dx, dz, reference, test);
++ }
+ }
+ }
+ }
++ */
++
++ // this map is considered "stale" unless updates are propagated.
++ protected final LevelMap levels = new LevelMap(8192*2, 0.6f);
++
++ // this map is never stale
++ protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f);
++
++ // Generally updates to positions are made close to other updates, so we link to decrease cache misses when
++ // propagating updates
++ protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet();
+
+ @FunctionalInterface
-+ public static interface DistanceChangeCallback<E> {
++ public static interface LevelChangeCallback {
+
-+ void accept(final int posX, final int posZ, final int oldNearestDistance, final int newNearestDistance,
-+ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> state);
++ /**
++ * This can be called for intermediate updates. So do not rely on newLevel being close to or
++ * the exact level that is expected after a full propagation has occured.
++ */
++ public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel);
+
+ }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..46954db7ecd35ac4018fdf476df7c8020d7ce6c8
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java
-@@ -0,0 +1,32 @@
-+package com.destroystokyo.paper.util.misc;
-+
-+import net.minecraft.server.level.ServerPlayer;
+
-+/**
-+ * @author Spottedleaf
-+ */
-+public final class PlayerAreaMap extends AreaMap<ServerPlayer> {
++ protected final LevelChangeCallback changeCallback;
+
-+ public PlayerAreaMap() {
-+ super();
++ public Delayed8WayDistancePropagator2D() {
++ this(null);
+ }
+
-+ public PlayerAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets) {
-+ super(pooledHashSets);
++ public Delayed8WayDistancePropagator2D(final LevelChangeCallback changeCallback) {
++ this.changeCallback = changeCallback;
+ }
+
-+ public PlayerAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets, final ChangeCallback<ServerPlayer> addCallback,
-+ final ChangeCallback<ServerPlayer> removeCallback) {
-+ this(pooledHashSets, addCallback, removeCallback, null);
++ public int getLevel(final long pos) {
++ return this.levels.get(pos);
+ }
+
-+ public PlayerAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets, final ChangeCallback<ServerPlayer> addCallback,
-+ final ChangeCallback<ServerPlayer> removeCallback, final ChangeSourceCallback<ServerPlayer> changeSourceCallback) {
-+ super(pooledHashSets, addCallback, removeCallback, changeSourceCallback);
++ public int getLevel(final int x, final int z) {
++ return this.levels.get(CoordinateUtils.getChunkKey(x, z));
+ }
+
-+ @Override
-+ protected PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> getEmptySetFor(final ServerPlayer player) {
-+ return player.cachedSingleHashSet;
++ public void setSource(final int x, final int z, final int level) {
++ this.setSource(CoordinateUtils.getChunkKey(x, z), level);
+ }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..d05dcea15f7047b58736c7c0e07920a04d6c5abe
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java
-@@ -0,0 +1,24 @@
-+package com.destroystokyo.paper.util.misc;
+
-+import net.minecraft.server.level.ServerPlayer;
++ public void setSource(final long coordinate, final int level) {
++ if ((level & 63) != level || level == 0) {
++ throw new IllegalArgumentException("Level must be in (0, 63], not " + level);
++ }
+
-+public class PlayerDistanceTrackingAreaMap extends DistanceTrackingAreaMap<ServerPlayer> {
++ final byte byteLevel = (byte)level;
++ final byte oldLevel = this.sources.put(coordinate, byteLevel);
+
-+ public PlayerDistanceTrackingAreaMap() {
-+ super();
++ if (oldLevel == byteLevel) {
++ return; // nothing to do
++ }
++
++ // queue to update later
++ this.updatedSources.add(coordinate);
+ }
+
-+ public PlayerDistanceTrackingAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets) {
-+ super(pooledHashSets);
++ public void removeSource(final int x, final int z) {
++ this.removeSource(CoordinateUtils.getChunkKey(x, z));
+ }
+
-+ public PlayerDistanceTrackingAreaMap(final PooledLinkedHashSets<ServerPlayer> pooledHashSets, final ChangeCallback<ServerPlayer> addCallback,
-+ final ChangeCallback<ServerPlayer> removeCallback, final DistanceChangeCallback<ServerPlayer> distanceChangeCallback) {
-+ super(pooledHashSets, addCallback, removeCallback, distanceChangeCallback);
++ public void removeSource(final long coordinate) {
++ if (this.sources.remove(coordinate) != 0) {
++ this.updatedSources.add(coordinate);
++ }
+ }
+
-+ @Override
-+ protected PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> getEmptySetFor(final ServerPlayer player) {
-+ return player.cachedSingleHashSet;
++ // queues used for BFS propagating levels
++ protected final WorkQueue[] levelIncreaseWorkQueues = new WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) {
++ this.levelIncreaseWorkQueues[i] = new WorkQueue();
++ }
+ }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java b/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..e51104e65a07b6ea7bbbcbb6afb066ef6401cc5b
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java
-@@ -0,0 +1,287 @@
-+package com.destroystokyo.paper.util.misc;
++ protected final WorkQueue[] levelRemoveWorkQueues = new WorkQueue[64];
++ {
++ for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) {
++ this.levelRemoveWorkQueues[i] = new WorkQueue();
++ }
++ }
++ protected long levelIncreaseWorkQueueBitset;
++ protected long levelRemoveWorkQueueBitset;
+
-+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
-+import java.lang.ref.WeakReference;
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) {
++ final WorkQueue queue = this.levelIncreaseWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
+
-+/** @author Spottedleaf */
-+public class PooledLinkedHashSets<E> {
++ this.levelIncreaseWorkQueueBitset |= (1L << level);
++ }
+
-+ /* Tested via https://gist.github.com/Spottedleaf/a93bb7a8993d6ce142d3efc5932bf573 */
++ protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) {
++ final WorkQueue queue = this.levelIncreaseWorkQueues[index];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
++
++ this.levelIncreaseWorkQueueBitset |= (1L << index);
++ }
+
-+ // we really want to avoid that equals() check as much as possible...
-+ protected final Object2ObjectOpenHashMap<PooledObjectLinkedOpenHashSet<E>, PooledObjectLinkedOpenHashSet<E>> mapPool = new Object2ObjectOpenHashMap<>(128, 0.25f);
++ protected final void addToRemoveWorkQueue(final long coordinate, final byte level) {
++ final WorkQueue queue = this.levelRemoveWorkQueues[level];
++ queue.queuedCoordinates.enqueue(coordinate);
++ queue.queuedLevels.enqueue(level);
+
-+ protected void decrementReferenceCount(final PooledObjectLinkedOpenHashSet<E> current) {
-+ if (current.referenceCount == 0) {
-+ throw new IllegalStateException("Cannot decrement reference count for " + current);
++ this.levelRemoveWorkQueueBitset |= (1L << level);
++ }
++
++ public boolean propagateUpdates() {
++ if (this.updatedSources.isEmpty()) {
++ return false;
+ }
-+ if (current.referenceCount == -1 || --current.referenceCount > 0) {
-+ return;
++
++ boolean ret = false;
++
++ for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) {
++ final long coordinate = iterator.nextLong();
++
++ final byte currentLevel = this.levels.get(coordinate);
++ final byte updatedSource = this.sources.get(coordinate);
++
++ if (currentLevel == updatedSource) {
++ continue;
++ }
++ ret = true;
++
++ if (updatedSource > currentLevel) {
++ // level increase
++ this.addToIncreaseWorkQueue(coordinate, updatedSource);
++ } else {
++ // level decrease
++ this.addToRemoveWorkQueue(coordinate, currentLevel);
++ // if the current coordinate is a source, then the decrease propagation will detect that and queue
++ // the source propagation
++ }
+ }
+
-+ this.mapPool.remove(current);
-+ return;
++ this.updatedSources.clear();
++
++ // propagate source level increases first for performance reasons (in crowded areas hopefully the additions
++ // make the removes remove less)
++ this.propagateIncreases();
++
++ // now we propagate the decreases (which will then re-propagate clobbered sources)
++ this.propagateDecreases();
++
++ return ret;
+ }
+
-+ public PooledObjectLinkedOpenHashSet<E> findMapWith(final PooledObjectLinkedOpenHashSet<E> current, final E object) {
-+ final PooledObjectLinkedOpenHashSet<E> cached = current.getAddCache(object);
++ protected void propagateIncreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset);
++ this.levelIncreaseWorkQueueBitset != 0L;
++ this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) {
++
++ final WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ byte level = queue.queuedLevels.removeFirstByte();
++
++ final boolean neighbourCheck = level < 0;
++
++ final byte currentLevel;
++ if (neighbourCheck) {
++ level = (byte)-level;
++ currentLevel = this.levels.get(coordinate);
++ } else {
++ currentLevel = this.levels.putIfGreater(coordinate, level);
++ }
++
++ if (neighbourCheck) {
++ // used when propagating from decrease to indicate that this level needs to check its neighbours
++ // this means the level at coordinate could be equal, but would still need neighbours checked
++
++ if (currentLevel != level) {
++ // something caused the level to change, which means something propagated to it (which means
++ // us propagating here is redundant), or something removed the level (which means we
++ // cannot propagate further)
++ continue;
++ }
++ } else if (currentLevel >= level) {
++ // something higher/equal propagated
++ continue;
++ }
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, level);
++ }
+
-+ if (cached != null) {
-+ decrementReferenceCount(current);
++ if (level == 1) {
++ // can't propagate 0 to neighbours
++ continue;
++ }
+
-+ if (cached.referenceCount == 0) {
-+ // bring the map back from the dead
-+ PooledObjectLinkedOpenHashSet<E> contending = this.mapPool.putIfAbsent(cached, cached);
-+ if (contending != null) {
-+ // a map already exists with the elements we want
-+ if (contending.referenceCount != -1) {
-+ ++contending.referenceCount;
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = (int)coordinate;
++ final int z = (int)(coordinate >>> 32);
++
++ for (int dx = -1; dx <= 1; ++dx) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ if ((dx | dz) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = CoordinateUtils.getChunkKey(x + dx, z + dz);
++ this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel);
+ }
-+ current.updateAddCache(object, contending);
-+ return contending;
++ }
++ }
++ }
++ }
++
++ protected void propagateDecreases() {
++ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset);
++ this.levelRemoveWorkQueueBitset != 0L;
++ this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) {
++
++ final WorkQueue queue = this.levelRemoveWorkQueues[queueIndex];
++ while (!queue.queuedLevels.isEmpty()) {
++ final long coordinate = queue.queuedCoordinates.removeFirstLong();
++ final byte level = queue.queuedLevels.removeFirstByte();
++
++ final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level);
++ if (currentLevel == 0) {
++ // something else removed
++ continue;
++ }
++
++ if (currentLevel > level) {
++ // something higher propagated here or we hit the propagation of another source
++ // in the second case we need to re-propagate because we could have just clobbered another source's
++ // propagation
++ this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking
++ continue;
++ }
++
++ if (this.changeCallback != null) {
++ this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0);
++ }
++
++ final byte source = this.sources.get(coordinate);
++ if (source != 0) {
++ // must re-propagate source later
++ this.addToIncreaseWorkQueue(coordinate, source);
++ }
++
++ if (level == 0) {
++ // can't propagate -1 to neighbours
++ // we have to check neighbours for removing 1 just in case the neighbour is 2
++ continue;
+ }
+
-+ cached.referenceCount = 1;
-+ } else if (cached.referenceCount != -1) {
-+ ++cached.referenceCount;
++ // propagate to neighbours
++ final byte neighbourLevel = (byte)(level - 1);
++ final int x = (int)coordinate;
++ final int z = (int)(coordinate >>> 32);
++
++ for (int dx = -1; dx <= 1; ++dx) {
++ for (int dz = -1; dz <= 1; ++dz) {
++ if ((dx | dz) == 0) {
++ // already propagated to coordinate
++ continue;
++ }
++
++ // sure we can check the neighbour level in the map right now and avoid a propagation,
++ // but then we would still have to recheck it when popping the value off of the queue!
++ // so just avoid the double lookup
++ final long neighbourCoordinate = CoordinateUtils.getChunkKey(x + dx, z + dz);
++ this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel);
++ }
++ }
+ }
++ }
+
-+ return cached;
++ // propagate sources we clobbered in the process
++ this.propagateIncreases();
++ }
++
++ protected static final class LevelMap extends Long2ByteOpenHashMap {
++ public LevelMap() {
++ super();
+ }
+
-+ if (!current.add(object)) {
-+ return current;
++ public LevelMap(final int expected, final float loadFactor) {
++ super(expected, loadFactor);
+ }
+
-+ // we use get/put since we use a different key on put
-+ PooledObjectLinkedOpenHashSet<E> ret = this.mapPool.get(current);
++ // copied from superclass
++ private int find(final long k) {
++ if (k == 0L) {
++ return this.containsNullKey ? this.n : -(this.n + 1);
++ } else {
++ final long[] key = this.key;
++ long curr;
++ int pos;
++ if ((curr = key[pos = (int)HashCommon.mix(k) & this.mask]) == 0L) {
++ return -(pos + 1);
++ } else if (k == curr) {
++ return pos;
++ } else {
++ while((curr = key[pos = pos + 1 & this.mask]) != 0L) {
++ if (k == curr) {
++ return pos;
++ }
++ }
+
-+ if (ret == null) {
-+ ret = new PooledObjectLinkedOpenHashSet<>(current);
-+ current.remove(object);
-+ this.mapPool.put(ret, ret);
-+ ret.referenceCount = 1;
-+ } else {
-+ if (ret.referenceCount != -1) {
-+ ++ret.referenceCount;
++ return -(pos + 1);
++ }
+ }
-+ current.remove(object);
+ }
+
-+ current.updateAddCache(object, ret);
++ // copied from superclass
++ private void insert(final int pos, final long k, final byte v) {
++ if (pos == this.n) {
++ this.containsNullKey = true;
++ }
+
-+ decrementReferenceCount(current);
-+ return ret;
-+ }
++ this.key[pos] = k;
++ this.value[pos] = v;
++ if (this.size++ >= this.maxFill) {
++ this.rehash(HashCommon.arraySize(this.size + 1, this.f));
++ }
++ }
+
-+ // rets null if current.size() == 1
-+ public PooledObjectLinkedOpenHashSet<E> findMapWithout(final PooledObjectLinkedOpenHashSet<E> current, final E object) {
-+ if (current.set.size() == 1) {
-+ decrementReferenceCount(current);
-+ return null;
++ // copied from superclass
++ public byte putIfGreater(final long key, final byte value) {
++ final int pos = this.find(key);
++ if (pos < 0) {
++ if (this.defRetValue < value) {
++ this.insert(-pos - 1, key, value);
++ }
++ return this.defRetValue;
++ } else {
++ final byte curr = this.value[pos];
++ if (value > curr) {
++ this.value[pos] = value;
++ return curr;
++ }
++ return curr;
++ }
+ }
+
-+ final PooledObjectLinkedOpenHashSet<E> cached = current.getRemoveCache(object);
++ // copied from superclass
++ private void removeEntry(final int pos) {
++ --this.size;
++ this.shiftKeys(pos);
++ if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) {
++ this.rehash(this.n / 2);
++ }
++ }
+
-+ if (cached != null) {
-+ decrementReferenceCount(current);
++ // copied from superclass
++ private void removeNullEntry() {
++ this.containsNullKey = false;
++ --this.size;
++ if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) {
++ this.rehash(this.n / 2);
++ }
++ }
+
-+ if (cached.referenceCount == 0) {
-+ // bring the map back from the dead
-+ PooledObjectLinkedOpenHashSet<E> contending = this.mapPool.putIfAbsent(cached, cached);
-+ if (contending != null) {
-+ // a map already exists with the elements we want
-+ if (contending.referenceCount != -1) {
-+ ++contending.referenceCount;
-+ }
-+ current.updateRemoveCache(object, contending);
-+ return contending;
++ // copied from superclass
++ public byte removeIfGreaterOrEqual(final long key, final byte value) {
++ if (key == 0L) {
++ if (!this.containsNullKey) {
++ return this.defRetValue;
+ }
++ final byte current = this.value[this.n];
++ if (value >= current) {
++ this.removeNullEntry();
++ return current;
++ }
++ return current;
++ } else {
++ long[] keys = this.key;
++ byte[] values = this.value;
++ long curr;
++ int pos;
++ if ((curr = keys[pos = (int)HashCommon.mix(key) & this.mask]) == 0L) {
++ return this.defRetValue;
++ } else if (key == curr) {
++ final byte current = values[pos];
++ if (value >= current) {
++ this.removeEntry(pos);
++ return current;
++ }
++ return current;
++ } else {
++ while((curr = keys[pos = pos + 1 & this.mask]) != 0L) {
++ if (key == curr) {
++ final byte current = values[pos];
++ if (value >= current) {
++ this.removeEntry(pos);
++ return current;
++ }
++ return current;
++ }
++ }
+
-+ cached.referenceCount = 1;
-+ } else if (cached.referenceCount != -1) {
-+ ++cached.referenceCount;
++ return this.defRetValue;
++ }
+ }
-+
-+ return cached;
+ }
++ }
+
-+ if (!current.remove(object)) {
-+ return current;
++ protected static final class WorkQueue {
++
++ public final NoResizeLongArrayFIFODeque queuedCoordinates = new NoResizeLongArrayFIFODeque();
++ public final NoResizeByteArrayFIFODeque queuedLevels = new NoResizeByteArrayFIFODeque();
++
++ }
++
++ protected static final class NoResizeLongArrayFIFODeque extends LongArrayFIFOQueue {
++
++ /**
++ * Assumes non-empty. If empty, undefined behaviour.
++ */
++ public long removeFirstLong() {
++ // copied from superclass
++ long t = this.array[this.start];
++ if (++this.start == this.length) {
++ this.start = 0;
++ }
++
++ return t;
+ }
++ }
+
-+ // we use get/put since we use a different key on put
-+ PooledObjectLinkedOpenHashSet<E> ret = this.mapPool.get(current);
++ protected static final class NoResizeByteArrayFIFODeque extends ByteArrayFIFOQueue {
+
-+ if (ret == null) {
-+ ret = new PooledObjectLinkedOpenHashSet<>(current);
-+ current.add(object);
-+ this.mapPool.put(ret, ret);
-+ ret.referenceCount = 1;
-+ } else {
-+ if (ret.referenceCount != -1) {
-+ ++ret.referenceCount;
++ /**
++ * Assumes non-empty. If empty, undefined behaviour.
++ */
++ public byte removeFirstByte() {
++ // copied from superclass
++ byte t = this.array[this.start];
++ if (++this.start == this.length) {
++ this.start = 0;
+ }
-+ current.add(object);
++
++ return t;
+ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ab093b0e8ac6f762921eb1d15f5217345c4eba05
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
+@@ -0,0 +1,211 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++import ca.spottedleaf.moonrise.common.list.ReferenceList;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.MoonriseConstants;
++import ca.spottedleaf.moonrise.common.util.ChunkSystem;
++import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants;
++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
++import net.minecraft.core.BlockPos;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.level.ChunkPos;
+
-+ current.updateRemoveCache(object, ret);
++public final class NearbyPlayers {
+
-+ decrementReferenceCount(current);
-+ return ret;
++ public static enum NearbyMapType {
++ GENERAL,
++ GENERAL_SMALL,
++ GENERAL_REALLY_SMALL,
++ TICK_VIEW_DISTANCE,
++ VIEW_DISTANCE,
++ SPAWN_RANGE, // Moonrise - chunk tick iteration
+ }
+
-+ static final class RawSetObjectLinkedOpenHashSet<E> extends ObjectOpenHashSet<E> {
++ private static final NearbyMapType[] MAP_TYPES = NearbyMapType.values();
++ public static final int TOTAL_MAP_TYPES = MAP_TYPES.length;
+
-+ public RawSetObjectLinkedOpenHashSet() {
-+ super();
++ private static final int GENERAL_AREA_VIEW_DISTANCE = MoonriseConstants.MAX_VIEW_DISTANCE + 1;
++ private static final int GENERAL_SMALL_VIEW_DISTANCE = 10;
++ private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3;
++
++ public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4);
++ public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4);
++ public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4);
++
++ private final ServerLevel world;
++ private final Reference2ReferenceOpenHashMap<ServerPlayer, TrackedPlayer[]> players = new Reference2ReferenceOpenHashMap<>();
++ private final Long2ReferenceOpenHashMap<TrackedChunk> byChunk = new Long2ReferenceOpenHashMap<>();
++
++ public NearbyPlayers(final ServerLevel world) {
++ this.world = world;
++ }
++
++ public void addPlayer(final ServerPlayer player) {
++ final TrackedPlayer[] newTrackers = new TrackedPlayer[TOTAL_MAP_TYPES];
++ if (this.players.putIfAbsent(player, newTrackers) != null) {
++ throw new IllegalStateException("Already have player " + player);
+ }
+
-+ public RawSetObjectLinkedOpenHashSet(final int capacity) {
-+ super(capacity);
++ final ChunkPos chunk = player.chunkPosition();
++
++ for (int i = 0; i < TOTAL_MAP_TYPES; ++i) {
++ // use 0 for default, will be updated by tickPlayer
++ (newTrackers[i] = new TrackedPlayer(player, MAP_TYPES[i])).add(chunk.x, chunk.z, 0);
+ }
+
-+ public RawSetObjectLinkedOpenHashSet(final int capacity, final float loadFactor) {
-+ super(capacity, loadFactor);
++ // update view distances
++ this.tickPlayer(player);
++ }
++
++ public void removePlayer(final ServerPlayer player) {
++ final TrackedPlayer[] players = this.players.remove(player);
++ if (players == null) {
++ return; // May be called during teleportation before the player is actually placed
+ }
+
-+ @Override
-+ public RawSetObjectLinkedOpenHashSet<E> clone() {
-+ return (RawSetObjectLinkedOpenHashSet<E>)super.clone();
++ for (final TrackedPlayer tracker : players) {
++ tracker.remove();
+ }
++ }
+
-+ public E[] getRawSet() {
-+ return this.key;
++ public void tickPlayer(final ServerPlayer player) {
++ final TrackedPlayer[] players = this.players.get(player);
++ if (players == null) {
++ throw new IllegalStateException("Don't have player " + player);
+ }
++
++ final ChunkPos chunk = player.chunkPosition();
++
++ players[NearbyMapType.GENERAL.ordinal()].update(chunk.x, chunk.z, GENERAL_AREA_VIEW_DISTANCE);
++ players[NearbyMapType.GENERAL_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_SMALL_VIEW_DISTANCE);
++ players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE);
++ players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player));
++ players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getLoadViewDistance(player));
++ players[NearbyMapType.SPAWN_RANGE.ordinal()].update(chunk.x, chunk.z, ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Moonrise - chunk tick iteration
+ }
+
-+ public static final class PooledObjectLinkedOpenHashSet<E> {
++ public TrackedChunk getChunk(final ChunkPos pos) {
++ return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
++ }
+
-+ private static final WeakReference NULL_REFERENCE = new WeakReference<>(null);
++ public TrackedChunk getChunk(final BlockPos pos) {
++ return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
++ }
+
-+ final RawSetObjectLinkedOpenHashSet<E> set;
-+ int referenceCount; // -1 if special
-+ int hash; // optimize hashcode
++ public ReferenceList<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) {
++ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos));
+
-+ // add cache
-+ WeakReference<E> lastAddObject = NULL_REFERENCE;
-+ WeakReference<PooledObjectLinkedOpenHashSet<E>> lastAddMap = NULL_REFERENCE;
++ return chunk == null ? null : chunk.players[type.ordinal()];
++ }
+
-+ // remove cache
-+ WeakReference<E> lastRemoveObject = NULL_REFERENCE;
-+ WeakReference<PooledObjectLinkedOpenHashSet<E>> lastRemoveMap = NULL_REFERENCE;
++ public ReferenceList<ServerPlayer> getPlayers(final ChunkPos pos, final NearbyMapType type) {
++ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos));
+
-+ public PooledObjectLinkedOpenHashSet(final PooledLinkedHashSets<E> pooledSets) {
-+ this.set = new RawSetObjectLinkedOpenHashSet<>(2, 0.8f);
-+ }
++ return chunk == null ? null : chunk.players[type.ordinal()];
++ }
+
-+ public PooledObjectLinkedOpenHashSet(final E single) {
-+ this((PooledLinkedHashSets<E>)null);
-+ this.referenceCount = -1;
-+ this.add(single);
++ public ReferenceList<ServerPlayer> getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) {
++ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++
++ return chunk == null ? null : chunk.players[type.ordinal()];
++ }
++
++ public ReferenceList<ServerPlayer> getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) {
++ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4));
++
++ return chunk == null ? null : chunk.players[type.ordinal()];
++ }
++
++ public static final class TrackedChunk {
++
++ private static final ServerPlayer[] EMPTY_PLAYERS_ARRAY = new ServerPlayer[0];
++
++ private final ReferenceList<ServerPlayer>[] players = new ReferenceList[TOTAL_MAP_TYPES];
++ private int nonEmptyLists;
++ private long updateCount;
++
++ public boolean isEmpty() {
++ return this.nonEmptyLists == 0;
+ }
+
-+ public PooledObjectLinkedOpenHashSet(final PooledObjectLinkedOpenHashSet<E> other) {
-+ this.set = other.set.clone();
-+ this.hash = other.hash;
++ public long getUpdateCount() {
++ return this.updateCount;
+ }
+
-+ // from https://github.com/Spottedleaf/ConcurrentUtil/blob/master/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
-+ // generated by https://github.com/skeeto/hash-prospector
-+ private static int hash0(int x) {
-+ x *= 0x36935555;
-+ x ^= x >>> 16;
-+ return x;
++ public ReferenceList<ServerPlayer> getPlayers(final NearbyMapType type) {
++ return this.players[type.ordinal()];
+ }
+
-+ PooledObjectLinkedOpenHashSet<E> getAddCache(final E element) {
-+ final E currentAdd = this.lastAddObject.get();
++ public void addPlayer(final ServerPlayer player, final NearbyMapType type) {
++ ++this.updateCount;
+
-+ if (currentAdd == null || !(currentAdd == element || currentAdd.equals(element))) {
-+ return null;
++ final int idx = type.ordinal();
++ final ReferenceList<ServerPlayer> list = this.players[idx];
++ if (list == null) {
++ ++this.nonEmptyLists;
++ (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY)).add(player);
++ return;
+ }
+
-+ return this.lastAddMap.get();
++ if (!list.add(player)) {
++ throw new IllegalStateException("Already contains player " + player);
++ }
+ }
+
-+ PooledObjectLinkedOpenHashSet<E> getRemoveCache(final E element) {
-+ final E currentRemove = this.lastRemoveObject.get();
++ public void removePlayer(final ServerPlayer player, final NearbyMapType type) {
++ ++this.updateCount;
+
-+ if (currentRemove == null || !(currentRemove == element || currentRemove.equals(element))) {
-+ return null;
++ final int idx = type.ordinal();
++ final ReferenceList<ServerPlayer> list = this.players[idx];
++ if (list == null) {
++ throw new IllegalStateException("Does not contain player " + player);
+ }
+
-+ return this.lastRemoveMap.get();
++ if (!list.remove(player)) {
++ throw new IllegalStateException("Does not contain player " + player);
++ }
++
++ if (list.size() == 0) {
++ this.players[idx] = null;
++ --this.nonEmptyLists;
++ }
+ }
++ }
+
-+ void updateAddCache(final E element, final PooledObjectLinkedOpenHashSet<E> map) {
-+ this.lastAddObject = new WeakReference<>(element);
-+ this.lastAddMap = new WeakReference<>(map);
++ private final class TrackedPlayer extends SingleUserAreaMap<ServerPlayer> {
++
++ private final NearbyMapType type;
++
++ public TrackedPlayer(final ServerPlayer player, final NearbyMapType type) {
++ super(player);
++ this.type = type;
+ }
+
-+ void updateRemoveCache(final E element, final PooledObjectLinkedOpenHashSet<E> map) {
-+ this.lastRemoveObject = new WeakReference<>(element);
-+ this.lastRemoveMap = new WeakReference<>(map);
++ @Override
++ protected void addCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) {
++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++
++ NearbyPlayers.this.byChunk.computeIfAbsent(chunkKey, (final long keyInMap) -> {
++ return new TrackedChunk();
++ }).addPlayer(parameter, this.type);
+ }
+
-+ boolean add(final E element) {
-+ boolean added = this.set.add(element);
++ @Override
++ protected void removeCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) {
++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
-+ if (added) {
-+ this.hash += hash0(element.hashCode());
++ final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey);
++ if (chunk == null) {
++ throw new IllegalStateException("Chunk should exist at " + new ChunkPos(chunkKey));
+ }
+
-+ return added;
++ chunk.removePlayer(parameter, this.type);
++
++ if (chunk.isEmpty()) {
++ NearbyPlayers.this.byChunk.remove(chunkKey);
++ }
+ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/PositionCountingAreaMap.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/PositionCountingAreaMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..efefd94b652228d877db5dbca8b28354ad42529f
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/PositionCountingAreaMap.java
+@@ -0,0 +1,94 @@
++package ca.spottedleaf.moonrise.common.misc;
+
-+ boolean remove(Object element) {
-+ boolean removed = this.set.remove(element);
++import ca.spottedleaf.concurrentutil.util.IntPairUtil;
++import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
++import it.unimi.dsi.fastutil.objects.ReferenceSet;
+
-+ if (removed) {
-+ this.hash -= hash0(element.hashCode());
-+ }
++public final class PositionCountingAreaMap<T> {
++
++ private final Reference2ReferenceOpenHashMap<T, PositionCounter> counters = new Reference2ReferenceOpenHashMap<>();
++ private final Long2IntOpenHashMap positions = new Long2IntOpenHashMap();
++
++ public ReferenceSet<T> getObjects() {
++ return this.counters.keySet();
++ }
++
++ public int getTotalPositions() {
++ return this.positions.size();
++ }
++
++ public boolean hasObjectsNear(final int toX, final int toZ) {
++ return this.positions.containsKey(IntPairUtil.key(toX, toZ));
++ }
+
-+ return removed;
++ public int getObjectsNear(final int toX, final int toZ) {
++ return this.positions.get(IntPairUtil.key(toX, toZ));
++ }
++
++ public boolean add(final T parameter, final int toX, final int toZ, final int distance) {
++ final PositionCounter existing = this.counters.get(parameter);
++ if (existing != null) {
++ return false;
+ }
+
-+ public boolean contains(final Object element) {
-+ return this.set.contains(element);
++ final PositionCounter counter = new PositionCounter(parameter);
++
++ this.counters.put(parameter, counter);
++
++ return counter.add(toX, toZ, distance);
++ }
++
++ public boolean addOrUpdate(final T parameter, final int toX, final int toZ, final int distance) {
++ final PositionCounter existing = this.counters.get(parameter);
++ if (existing != null) {
++ return existing.update(toX, toZ, distance);
+ }
+
-+ public E[] getBackingSet() {
-+ return this.set.getRawSet();
++ final PositionCounter counter = new PositionCounter(parameter);
++
++ this.counters.put(parameter, counter);
++
++ return counter.add(toX, toZ, distance);
++ }
++
++ public boolean remove(final T parameter) {
++ final PositionCounter counter = this.counters.remove(parameter);
++ if (counter == null) {
++ return false;
+ }
+
-+ public int size() {
-+ return this.set.size();
++ counter.remove();
++
++ return true;
++ }
++
++ public boolean update(final T parameter, final int toX, final int toZ, final int distance) {
++ final PositionCounter counter = this.counters.get(parameter);
++ if (counter == null) {
++ return false;
+ }
+
-+ @Override
-+ public int hashCode() {
-+ return this.hash;
++ return counter.update(toX, toZ, distance);
++ }
++
++ private final class PositionCounter extends SingleUserAreaMap<T> {
++
++ public PositionCounter(final T parameter) {
++ super(parameter);
+ }
+
+ @Override
-+ public boolean equals(final Object other) {
-+ if (!(other instanceof PooledObjectLinkedOpenHashSet)) {
-+ return false;
-+ }
-+ if (this.referenceCount == 0) {
-+ return other == this;
-+ } else {
-+ if (other == this) {
-+ // Unfortunately we are never equal to our own instance while in use!
-+ return false;
-+ }
-+ return this.hash == ((PooledObjectLinkedOpenHashSet)other).hash && this.set.equals(((PooledObjectLinkedOpenHashSet)other).set);
-+ }
++ protected void addCallback(final T parameter, final int toX, final int toZ) {
++ PositionCountingAreaMap.this.positions.addTo(IntPairUtil.key(toX, toZ), 1);
+ }
+
+ @Override
-+ public String toString() {
-+ return "PooledHashSet: size: " + this.set.size() + ", reference count: " + this.referenceCount + ", hash: " +
-+ this.hashCode() + ", identity: " + System.identityHashCode(this) + " map: " + this.set.toString();
++ protected void removeCallback(final T parameter, final int toX, final int toZ) {
++ final long key = IntPairUtil.key(toX, toZ);
++ if (PositionCountingAreaMap.this.positions.addTo(key, -1) == 1) {
++ PositionCountingAreaMap.this.positions.remove(key);
++ }
+ }
+ }
+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java b/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
new file mode 100644
-index 0000000000000000000000000000000000000000..a743703502cea333bd4231b6557de50e8eaf81eb
+index 0000000000000000000000000000000000000000..94689e0342cf95dbedec955d67c95fa07a219678
--- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java
-@@ -0,0 +1,85 @@
-+package com.destroystokyo.paper.util.pooled;
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
+@@ -0,0 +1,248 @@
++package ca.spottedleaf.moonrise.common.misc;
+
-+import io.papermc.paper.util.MCUtil;
-+import org.apache.commons.lang3.mutable.MutableInt;
++import ca.spottedleaf.concurrentutil.util.IntegerUtil;
+
-+import java.util.ArrayDeque;
-+import java.util.function.Consumer;
-+import java.util.function.Supplier;
++public abstract class SingleUserAreaMap<T> {
+
-+public final class PooledObjects<E> {
++ public static final int NOT_SET = Integer.MIN_VALUE;
+
-+ /**
-+ * Wrapper for an object that will be have a cleaner registered for it, and may be automatically returned to pool.
-+ */
-+ public class AutoReleased {
-+ private final E object;
-+ private final Runnable cleaner;
++ private final T parameter;
++ private int lastChunkX = NOT_SET;
++ private int lastChunkZ = NOT_SET;
++ private int distance = NOT_SET;
+
-+ public AutoReleased(E object, Runnable cleaner) {
-+ this.object = object;
-+ this.cleaner = cleaner;
-+ }
++ public SingleUserAreaMap(final T parameter) {
++ this.parameter = parameter;
++ }
++
++ public final T getParameter() {
++ return this.parameter;
++ }
++
++ public final int getLastChunkX() {
++ return this.lastChunkX;
++ }
++
++ public final int getLastChunkZ() {
++ return this.lastChunkZ;
++ }
+
-+ public final E getObject() {
-+ return object;
++ public final int getLastDistance() {
++ return this.distance;
++ }
++
++ /* math sign function except 0 returns 1 */
++ protected static int sign(int val) {
++ return 1 | (val >> (Integer.SIZE - 1));
++ }
++
++ protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ);
++
++ protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ);
++
++ private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) {
++ final int maxX = chunkX + distance;
++ final int maxZ = chunkZ + distance;
++
++ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
++ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
++ this.addCallback(parameter, cx, cz);
++ }
+ }
++ }
++
++ private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) {
++ final int maxX = chunkX + distance;
++ final int maxZ = chunkZ + distance;
+
-+ public final Runnable getCleaner() {
-+ return cleaner;
++ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
++ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
++ this.removeCallback(parameter, cx, cz);
++ }
+ }
+ }
+
-+ public static final PooledObjects<MutableInt> POOLED_MUTABLE_INTEGERS = new PooledObjects<>(MutableInt::new, 1024);
++ public final boolean add(final int chunkX, final int chunkZ, final int distance) {
++ if (distance < 0) {
++ throw new IllegalArgumentException(Integer.toString(distance));
++ }
++ if (this.lastChunkX != NOT_SET) {
++ return false;
++ }
++ this.lastChunkX = chunkX;
++ this.lastChunkZ = chunkZ;
++ this.distance = distance;
+
-+ private final Supplier<E> creator;
-+ private final Consumer<E> releaser;
-+ private final int maxPoolSize;
-+ private final ArrayDeque<E> queue;
++ this.addToNew(this.parameter, chunkX, chunkZ, distance);
+
-+ public PooledObjects(final Supplier<E> creator, int maxPoolSize) {
-+ this(creator, maxPoolSize, null);
++ return true;
+ }
-+ public PooledObjects(final Supplier<E> creator, int maxPoolSize, Consumer<E> releaser) {
-+ if (creator == null) {
-+ throw new NullPointerException("Creator must not be null");
++
++ public final boolean update(final int toX, final int toZ, final int newViewDistance) {
++ if (newViewDistance < 0) {
++ throw new IllegalArgumentException(Integer.toString(newViewDistance));
+ }
-+ if (maxPoolSize <= 0) {
-+ throw new IllegalArgumentException("Max pool size must be greater-than 0");
++ final int fromX = this.lastChunkX;
++ final int fromZ = this.lastChunkZ;
++ final int oldViewDistance = this.distance;
++ if (fromX == NOT_SET) {
++ return false;
+ }
+
-+ this.queue = new ArrayDeque<>(maxPoolSize);
-+ this.maxPoolSize = maxPoolSize;
-+ this.creator = creator;
-+ this.releaser = releaser;
-+ }
++ this.lastChunkX = toX;
++ this.lastChunkZ = toZ;
++ this.distance = newViewDistance;
+
-+ public AutoReleased acquireCleaner(Object holder) {
-+ return acquireCleaner(holder, this::release);
-+ }
++ final T parameter = this.parameter;
+
-+ public AutoReleased acquireCleaner(Object holder, Consumer<E> releaser) {
-+ E resource = acquire();
-+ Runnable cleaner = MCUtil.registerCleaner(holder, resource, releaser);
-+ return new AutoReleased(resource, cleaner);
-+ }
+
-+ public final E acquire() {
-+ E value;
-+ synchronized (queue) {
-+ value = this.queue.pollLast();
++ final int dx = toX - fromX;
++ final int dz = toZ - fromZ;
++
++ final int totalX = IntegerUtil.branchlessAbs(fromX - toX);
++ final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
++
++ if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
++ // teleported
++ this.removeFromOld(parameter, fromX, fromZ, oldViewDistance);
++ this.addToNew(parameter, toX, toZ, newViewDistance);
++ return true;
+ }
-+ return value != null ? value : this.creator.get();
-+ }
+
-+ public final void release(final E value) {
-+ if (this.releaser != null) {
-+ this.releaser.accept(value);
++ if (oldViewDistance != newViewDistance) {
++ // remove loop
++
++ final int oldMinX = fromX - oldViewDistance;
++ final int oldMinZ = fromZ - oldViewDistance;
++ final int oldMaxX = fromX + oldViewDistance;
++ final int oldMaxZ = fromZ + oldViewDistance;
++ for (int currX = oldMinX; currX <= oldMaxX; ++currX) {
++ for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) {
++
++ // only remove if we're outside the new view distance...
++ if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) {
++ this.removeCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ // add loop
++
++ final int newMinX = toX - newViewDistance;
++ final int newMinZ = toZ - newViewDistance;
++ final int newMaxX = toX + newViewDistance;
++ final int newMaxZ = toZ + newViewDistance;
++ for (int currX = newMinX; currX <= newMaxX; ++currX) {
++ for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) {
++
++ // only add if we're outside the old view distance...
++ if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) {
++ this.addCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ return true;
+ }
-+ synchronized (this.queue) {
-+ if (queue.size() < this.maxPoolSize) {
-+ this.queue.addLast(value);
++
++ // x axis is width
++ // z axis is height
++ // right refers to the x axis of where we moved
++ // top refers to the z axis of where we moved
++
++ // same view distance
++
++ // used for relative positioning
++ final int up = sign(dz); // 1 if dz >= 0, -1 otherwise
++ final int right = sign(dx); // 1 if dx >= 0, -1 otherwise
++
++ // The area excluded by overlapping the two view distance squares creates four rectangles:
++ // Two on the left, and two on the right. The ones on the left we consider the "removed" section
++ // and on the right the "added" section.
++ // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually
++ // exclusive to the regions they surround.
++
++ // 4 points of the rectangle
++ int maxX; // exclusive
++ int minX; // inclusive
++ int maxZ; // exclusive
++ int minZ; // inclusive
++
++ if (dx != 0) {
++ // handle right addition
++
++ maxX = toX + (oldViewDistance * right) + right; // exclusive
++ minX = fromX + (oldViewDistance * right) + right; // inclusive
++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
++ minZ = toZ - (oldViewDistance * up); // inclusive
++
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.addCallback(parameter, currX, currZ);
++ }
+ }
+ }
++
++ if (dz != 0) {
++ // handle up addition
++
++ maxX = toX + (oldViewDistance * right) + right; // exclusive
++ minX = toX - (oldViewDistance * right); // inclusive
++ maxZ = toZ + (oldViewDistance * up) + up; // exclusive
++ minZ = fromZ + (oldViewDistance * up) + up; // inclusive
++
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.addCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ if (dx != 0) {
++ // handle left removal
++
++ maxX = toX - (oldViewDistance * right); // exclusive
++ minX = fromX - (oldViewDistance * right); // inclusive
++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
++ minZ = toZ - (oldViewDistance * up); // inclusive
++
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.removeCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ if (dz != 0) {
++ // handle down removal
++
++ maxX = fromX + (oldViewDistance * right) + right; // exclusive
++ minX = fromX - (oldViewDistance * right); // inclusive
++ maxZ = toZ - (oldViewDistance * up); // exclusive
++ minZ = fromZ - (oldViewDistance * up); // inclusive
++
++ for (int currX = minX; currX != maxX; currX += right) {
++ for (int currZ = minZ; currZ != maxZ; currZ += up) {
++ this.removeCallback(parameter, currX, currZ);
++ }
++ }
++ }
++
++ return true;
++ }
++
++ public final boolean remove() {
++ final int chunkX = this.lastChunkX;
++ final int chunkZ = this.lastChunkZ;
++ final int distance = this.distance;
++ if (chunkX == NOT_SET) {
++ return false;
++ }
++
++ this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET;
++
++ this.removeFromOld(this.parameter, chunkX, chunkZ, distance);
++
++ return true;
+ }
+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java b/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java
new file mode 100644
-index 0000000000000000000000000000000000000000..8066e27ff88454cb4bc8075d936e58a067dbe9b4
+index 0000000000000000000000000000000000000000..4123edddc556c47f3f8d83523c125fd2e46b30e2
--- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java
-@@ -0,0 +1,71 @@
-+package com.destroystokyo.paper.util.set;
++++ b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java
+@@ -0,0 +1,68 @@
++package ca.spottedleaf.moonrise.common.set;
+
+import java.util.Collection;
+
-+/**
-+ * @author Spottedleaf &lt;[email protected]>
-+ */
+public final class OptimizedSmallEnumSet<E extends Enum<E>> {
+
+ private final Class<E> enumClass;
@@ -1665,7 +3009,7 @@ index 0000000000000000000000000000000000000000..8066e27ff88454cb4bc8075d936e58a0
+ this.enumClass = clazz;
+ }
+
-+ public boolean add(final E element) {
++ public boolean addUnchecked(final E element) {
+ final int ordinal = element.ordinal();
+ final long key = 1L << ordinal;
+
@@ -1675,7 +3019,7 @@ index 0000000000000000000000000000000000000000..8066e27ff88454cb4bc8075d936e58a0
+ return (prev & key) == 0;
+ }
+
-+ public boolean remove(final E element) {
++ public boolean removeUnchecked(final E element) {
+ final int ordinal = element.ordinal();
+ final long key = 1L << ordinal;
+
@@ -1693,7 +3037,7 @@ index 0000000000000000000000000000000000000000..8066e27ff88454cb4bc8075d936e58a0
+ return Long.bitCount(this.backingSet);
+ }
+
-+ public void addAll(final Collection<E> enums) {
++ public void addAllUnchecked(final Collection<E> enums) {
+ for (final E element : enums) {
+ if (element == null) {
+ throw new NullPointerException("Null element");
@@ -1710,63 +3054,38 @@ index 0000000000000000000000000000000000000000..8066e27ff88454cb4bc8075d936e58a0
+ return (other.backingSet & this.backingSet) != 0;
+ }
+
-+ public boolean contains(final E element) {
++ public boolean hasElement(final E element) {
+ return (this.backingSet & (1L << element.ordinal())) != 0;
+ }
+}
-diff --git a/src/main/java/com/mojang/logging/LogUtils.java b/src/main/java/com/mojang/logging/LogUtils.java
-index 46cab7a8c7b87ab01b26074b04f5a02b3907cfc4..49019b4a9bc4e634d54a9b0acaf9229a5c896f85 100644
---- a/src/main/java/com/mojang/logging/LogUtils.java
-+++ b/src/main/java/com/mojang/logging/LogUtils.java
-@@ -61,4 +61,9 @@ public class LogUtils {
- public static Logger getLogger() {
- return LoggerFactory.getLogger(STACK_WALKER.getCallerClass());
- }
-+ // Paper start
-+ public static Logger getClassLogger() {
-+ return LoggerFactory.getLogger(STACK_WALKER.getCallerClass().getSimpleName());
-+ }
-+ // Paper end
- }
-diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
new file mode 100644
-index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783075ae17a
+index 0000000000000000000000000000000000000000..da323a1105347d5cf4b946df10ded78a953236f2
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
-@@ -0,0 +1,297 @@
-+package io.papermc.paper.chunk.system;
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
+@@ -0,0 +1,284 @@
++package ca.spottedleaf.moonrise.common.util;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+import com.destroystokyo.paper.util.SneakyThrow;
+import com.mojang.logging.LogUtils;
-+import io.papermc.paper.util.CoordinateUtils;
+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.ChunkMap;
-+import net.minecraft.server.level.ChunkResult;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.TicketType;
+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkPyramid;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.status.ChunkStep;
-+import org.bukkit.Bukkit;
++import net.minecraft.world.level.chunk.status.ChunkStatus;
+import org.slf4j.Logger;
-+import java.util.ArrayList;
+import java.util.List;
-+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+public final class ChunkSystem {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
-+ private static final ChunkStep FULL_CHUNK_STEP = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
++ private static final net.minecraft.world.level.chunk.status.ChunkStep FULL_CHUNK_STEP = net.minecraft.world.level.chunk.status.ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
+
-+ public static int getDistance(final ChunkStatus status) {
++ private static int getDistance(final ChunkStatus status) {
+ return FULL_CHUNK_STEP.getAccumulatedRadiusOf(status);
+ }
+
@@ -1802,21 +3121,21 @@ index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783
+ });
+ }
+
-+ static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo);
++ static final net.minecraft.server.level.TicketType<Long> CHUNK_LOAD = net.minecraft.server.level.TicketType.create("chunk_load", Long::compareTo);
+
+ private static long chunkLoadCounter = 0L;
+ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+ final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
-+ if (!Bukkit.isPrimaryThread()) {
++ if (!org.bukkit.Bukkit.isPrimaryThread()) {
+ scheduleChunkTask(level, chunkX, chunkZ, () -> {
+ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ }, priority);
+ return;
+ }
+
-+ final int minLevel = 33 + ChunkSystem.getDistance(toStatus);
++ final int minLevel = 33 + getDistance(toStatus);
+ final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
-+ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
++ final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
+
+ if (addTicket) {
+ level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
@@ -1830,10 +3149,10 @@ index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783
+ }
+ } catch (final Throwable thr) {
+ LOGGER.error("Exception handling chunk load callback", thr);
-+ SneakyThrow.sneaky(thr);
++ com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
+ } finally {
+ if (addTicket) {
-+ level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
++ level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
+ level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+ }
+ }
@@ -1846,14 +3165,14 @@ index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783
+ return;
+ }
+
-+ final CompletableFuture<ChunkResult<ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
++ final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
+
+ if (loadFuture.isDone()) {
+ loadCallback.accept(loadFuture.join().orElse(null));
+ return;
+ }
+
-+ loadFuture.whenCompleteAsync((final ChunkResult<ChunkAccess> result, final Throwable thr) -> {
++ loadFuture.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess> result, final Throwable thr) -> {
+ if (thr != null) {
+ loadCallback.accept(null);
+ return;
@@ -1872,7 +3191,7 @@ index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783
+ throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
+ }
+
-+ if (!Bukkit.isPrimaryThread()) {
++ if (!org.bukkit.Bukkit.isPrimaryThread()) {
+ scheduleChunkTask(level, chunkX, chunkZ, () -> {
+ scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ }, priority);
@@ -1882,7 +3201,7 @@ index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783
+ final int minLevel = 33 - (toStatus.ordinal() - 1);
+ final int radius = toStatus.ordinal() - 1;
+ final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
-+ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
++ final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
+
+ if (addTicket) {
+ level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
@@ -1896,10 +3215,10 @@ index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783
+ }
+ } catch (final Throwable thr) {
+ LOGGER.error("Exception handling chunk load callback", thr);
-+ SneakyThrow.sneaky(thr);
++ com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
+ } finally {
+ if (addTicket) {
-+ level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
++ level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
+ level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+ }
+ }
@@ -1912,7 +3231,7 @@ index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783
+ return;
+ }
+
-+ final CompletableFuture<ChunkResult<LevelChunk>> tickingState;
++ final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk>> tickingState;
+ switch (toStatus) {
+ case FULL: {
+ tickingState = holder.getFullChunkFuture();
@@ -1936,7 +3255,7 @@ index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783
+ return;
+ }
+
-+ tickingState.whenCompleteAsync((final ChunkResult<LevelChunk> result, final Throwable thr) -> {
++ tickingState.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk> result, final Throwable thr) -> {
+ if (thr != null) {
+ loadCallback.accept(null);
+ return;
@@ -1948,11 +3267,11 @@ index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783
+ }
+
+ public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
-+ return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
++ return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
+ }
+
+ public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
-+ return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
++ return new java.util.ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
+ }
+
+ public static int getVisibleChunkHolderCount(final ServerLevel level) {
@@ -1967,8 +3286,8 @@ index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783
+ return getUpdatingChunkHolderCount(level) != 0;
+ }
+
-+ public static void onEntityPreAdd(final ServerLevel level, final Entity entity) {
-+
++ public static boolean screenEntity(final ServerLevel level, final Entity entity) {
++ return true;
+ }
+
+ public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
@@ -1988,19 +3307,19 @@ index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783
+ }
+
+ public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
-+ chunk.level.getChunkSource().tickingChunks.add(chunk);
++
+ }
+
+ public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
-+ chunk.level.getChunkSource().tickingChunks.remove(chunk);
++
+ }
+
+ public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
-+ chunk.level.getChunkSource().entityTickingChunks.add(chunk);
++
+ }
+
+ public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
-+ chunk.level.getChunkSource().entityTickingChunks.remove(chunk);
++
+ }
+
+ public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
@@ -2014,7 +3333,7 @@ index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783
+ public static int getLoadViewDistance(final ServerPlayer player) {
+ final ServerLevel level = player.serverLevel();
+ if (level == null) {
-+ return Bukkit.getViewDistance();
++ return org.bukkit.Bukkit.getViewDistance();
+ }
+ return level.chunkSource.chunkMap.getPlayerViewDistance(player);
+ }
@@ -2022,51 +3341,30 @@ index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783
+ public static int getTickViewDistance(final ServerPlayer player) {
+ final ServerLevel level = player.serverLevel();
+ if (level == null) {
-+ return Bukkit.getSimulationDistance();
++ return org.bukkit.Bukkit.getSimulationDistance();
+ }
+ return level.chunkSource.chunkMap.distanceManager.simulationDistance;
+ }
+
-+ private ChunkSystem() {
-+ throw new RuntimeException();
-+ }
-+}
-diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..be668387f65a633c6ac497fca632a4767a1bf3a2
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/CachedLists.java
-@@ -0,0 +1,8 @@
-+package io.papermc.paper.util;
-+
-+public final class CachedLists {
-+
-+ public static void reset() {
-+
-+ }
++ private ChunkSystem() {}
+}
-diff --git a/src/main/java/io/papermc/paper/util/CoordinateUtils.java b/src/main/java/io/papermc/paper/util/CoordinateUtils.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java
new file mode 100644
-index 0000000000000000000000000000000000000000..413e4b6da027876dbbe8eb78f2568a440f431547
+index 0000000000000000000000000000000000000000..31b92bd48828cbea25b44a9f0f96886347aa1ae6
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/CoordinateUtils.java
-@@ -0,0 +1,128 @@
-+package io.papermc.paper.util;
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java
+@@ -0,0 +1,129 @@
++package ca.spottedleaf.moonrise.common.util;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.SectionPos;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.phys.Vec3;
+
+public final class CoordinateUtils {
+
-+ // dx, dz are relative to the target chunk
-+ // dx, dz in [-radius, radius]
-+ public static int getNeighbourMappedIndex(final int dx, final int dz, final int radius) {
-+ return (dx + radius) + (2 * radius + 1)*(dz + radius);
-+ }
-+
+ // the chunk keys are compatible with vanilla
+
+ public static long getChunkKey(final BlockPos pos) {
@@ -2157,282 +3455,497 @@ index 0000000000000000000000000000000000000000..413e4b6da027876dbbe8eb78f2568a44
+ return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS));
+ }
+
-+ // the block coordinates are not necessarily compatible with vanilla's
++ public static int getBlockX(final Vec3 pos) {
++ return Mth.floor(pos.x);
++ }
++
++ public static int getBlockY(final Vec3 pos) {
++ return Mth.floor(pos.y);
++ }
+
-+ public static int getBlockCoordinate(final double blockCoordinate) {
-+ return Mth.floor(blockCoordinate);
++ public static int getBlockZ(final Vec3 pos) {
++ return Mth.floor(pos.z);
+ }
+
-+ public static long getBlockKey(final int x, final int y, final int z) {
-+ return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54);
++ public static int getChunkX(final Vec3 pos) {
++ return Mth.floor(pos.x) >> 4;
+ }
+
-+ public static long getBlockKey(final BlockPos pos) {
-+ return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54);
++ public static int getChunkY(final Vec3 pos) {
++ return Mth.floor(pos.y) >> 4;
+ }
+
-+ public static long getBlockKey(final Entity entity) {
-+ return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54);
++ public static int getChunkZ(final Vec3 pos) {
++ return Mth.floor(pos.z) >> 4;
+ }
+
+ private CoordinateUtils() {
+ throw new RuntimeException();
+ }
+}
-diff --git a/src/main/java/io/papermc/paper/util/IntegerUtil.java b/src/main/java/io/papermc/paper/util/IntegerUtil.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java
new file mode 100644
-index 0000000000000000000000000000000000000000..16785bd5c0524f6bad0691ca7ecd4514608d2eab
+index 0000000000000000000000000000000000000000..0531f25aaad162386a029d33e68d7c8336b9d5d1
--- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/IntegerUtil.java
-@@ -0,0 +1,242 @@
-+package io.papermc.paper.util;
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java
+@@ -0,0 +1,109 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import java.util.Objects;
++
++public final class FlatBitsetUtil {
++
++ private static final int LOG2_LONG = 6;
++ private static final long ALL_SET = -1L;
++ private static final int BITS_PER_LONG = Long.SIZE;
++
++ // from inclusive
++ // to exclusive
++ public static int firstSet(final long[] bitset, final int from, final int to) {
++ if ((from | to | (to - from)) < 0) {
++ throw new IndexOutOfBoundsException();
++ }
++
++ int bitsetIdx = from >>> LOG2_LONG;
++ int bitIdx = from & ~(BITS_PER_LONG - 1);
++
++ long tmp = bitset[bitsetIdx] & (ALL_SET << from);
++ for (;;) {
++ if (tmp != 0L) {
++ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
++ return ret >= to ? -1 : ret;
++ }
++
++ bitIdx += BITS_PER_LONG;
++
++ if (bitIdx >= to) {
++ return -1;
++ }
++
++ tmp = bitset[++bitsetIdx];
++ }
++ }
++
++ // from inclusive
++ // to exclusive
++ public static int firstClear(final long[] bitset, final int from, final int to) {
++ if ((from | to | (to - from)) < 0) {
++ throw new IndexOutOfBoundsException();
++ }
++ // like firstSet, but invert the bitset
++
++ int bitsetIdx = from >>> LOG2_LONG;
++ int bitIdx = from & ~(BITS_PER_LONG - 1);
++
++ long tmp = (~bitset[bitsetIdx]) & (ALL_SET << from);
++ for (;;) {
++ if (tmp != 0L) {
++ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
++ return ret >= to ? -1 : ret;
++ }
++
++ bitIdx += BITS_PER_LONG;
++
++ if (bitIdx >= to) {
++ return -1;
++ }
++
++ tmp = ~bitset[++bitsetIdx];
++ }
++ }
++
++ // from inclusive
++ // to exclusive
++ public static void clearRange(final long[] bitset, final int from, int to) {
++ if ((from | to | (to - from)) < 0) {
++ throw new IndexOutOfBoundsException();
++ }
++
++ if (from == to) {
++ return;
++ }
++
++ --to;
++
++ final int fromBitsetIdx = from >>> LOG2_LONG;
++ final int toBitsetIdx = to >>> LOG2_LONG;
++
++ final long keepFirst = ~(ALL_SET << from);
++ final long keepLast = ~(ALL_SET >>> ((BITS_PER_LONG - 1) ^ to));
++
++ Objects.checkFromToIndex(fromBitsetIdx, toBitsetIdx, bitset.length);
++
++ if (fromBitsetIdx == toBitsetIdx) {
++ // special case: need to keep both first and last
++ bitset[fromBitsetIdx] &= (keepFirst | keepLast);
++ } else {
++ bitset[fromBitsetIdx] &= keepFirst;
++
++ for (int i = fromBitsetIdx + 1; i < toBitsetIdx; ++i) {
++ bitset[i] = 0L;
++ }
++
++ bitset[toBitsetIdx] &= keepLast;
++ }
++ }
++
++ // from inclusive
++ // to exclusive
++ public static boolean isRangeSet(final long[] bitset, final int from, final int to) {
++ return firstClear(bitset, from, to) == -1;
++ }
++
++
++ private FlatBitsetUtil() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..91efda726b87a8a8f28dee84e31b6a7063752ebd
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java
+@@ -0,0 +1,34 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import com.google.gson.JsonElement;
++import com.google.gson.internal.Streams;
++import com.google.gson.stream.JsonWriter;
++import java.io.File;
++import java.io.FileOutputStream;
++import java.io.IOException;
++import java.io.PrintStream;
++import java.io.StringWriter;
++import java.nio.charset.StandardCharsets;
++
++public final class JsonUtil {
++
++ public static void writeJson(final JsonElement element, final File file) throws IOException {
++ final StringWriter stringWriter = new StringWriter();
++ final JsonWriter jsonWriter = new JsonWriter(stringWriter);
++ jsonWriter.setIndent(" ");
++ jsonWriter.setLenient(false);
++ Streams.write(element, jsonWriter);
++
++ final String jsonString = stringWriter.toString();
++
++ final File parent = file.getParentFile();
++ if (parent != null) {
++ parent.mkdirs();
++ }
++ file.createNewFile();
++ try (final PrintStream out = new PrintStream(new FileOutputStream(file), false, StandardCharsets.UTF_8)) {
++ out.print(jsonString);
++ }
++ }
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ac6f284ee4469d16c5655328b2488d7612832353
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
+@@ -0,0 +1,10 @@
++package ca.spottedleaf.moonrise.common.util;
++
++public final class MixinWorkarounds {
++
++ // mixins tries to find the owner of the clone() method, which doesn't exist and NPEs
++ public static long[] clone(final long[] values) {
++ return values.clone();
++ }
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3abe0bd2a820352b85306d554bf14a4cf6123091
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
+@@ -0,0 +1,46 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import java.io.File;
+
-+public final class IntegerUtil {
++public final class MoonriseCommon {
+
-+ public static final int HIGH_BIT_U32 = Integer.MIN_VALUE;
-+ public static final long HIGH_BIT_U64 = Long.MIN_VALUE;
++ private static final Logger LOGGER = LoggerFactory.getLogger(MoonriseCommon.class);
++
++ // Paper start
++ public static PrioritisedThreadPool WORKER_POOL;
++ public static int WORKER_THREADS;
++ public static void init(io.papermc.paper.configuration.GlobalConfiguration.ChunkSystem chunkSystem) {
++ // Paper end
++ int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2;
++ if (defaultWorkerThreads <= 4) {
++ defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2;
++ } else {
++ defaultWorkerThreads = defaultWorkerThreads / 2;
++ }
++ defaultWorkerThreads = Integer.getInteger("Paper.WorkerThreadCount", Integer.valueOf(defaultWorkerThreads)); // Paper
+
-+ public static int ceilLog2(final int value) {
-+ return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
++ int workerThreads = chunkSystem.workerThreads; // Paper
++
++ if (workerThreads <= 0) {
++ workerThreads = defaultWorkerThreads;
++ }
++
++ WORKER_POOL = new PrioritisedThreadPool(
++ "Paper Worker Pool", workerThreads, // Paper
++ (final Thread thread, final Integer id) -> {
++ thread.setName("Paper Common Worker #" + id.intValue()); // Paper
++ thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
++ @Override
++ public void uncaughtException(final Thread thread, final Throwable throwable) {
++ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
++ }
++ });
++ }, (long)(20.0e6)); // 20ms
++ WORKER_THREADS = workerThreads;
+ }
+
-+ public static long ceilLog2(final long value) {
-+ return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
++ private MoonriseCommon() {}
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..1cf32d7d1bbc8a0a3f7cb9024c793f6744199f64
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
+@@ -0,0 +1,9 @@
++package ca.spottedleaf.moonrise.common.util;
++
++public final class MoonriseConstants {
++
++ public static final int MAX_VIEW_DISTANCE = 32;
++
++ private MoonriseConstants() {}
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7deb341b7e2b4592ae3f88733d6cacf6e58764e4
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
+@@ -0,0 +1,139 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import net.minecraft.core.BlockPos;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.phys.AABB;
++import net.minecraft.world.phys.Vec3;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++
++import java.util.concurrent.atomic.AtomicInteger;
++
++public class TickThread extends Thread {
++
++ private static final Logger LOGGER = LoggerFactory.getLogger(TickThread.class);
++
++ /**
++ * @deprecated
++ */
++ @Deprecated
++ public static void ensureTickThread(final String reason) {
++ if (!isTickThread()) {
++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
+ }
+
-+ public static int floorLog2(final int value) {
-+ // xor is optimized subtract for 2^n -1
-+ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
-+ return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
++ public static void ensureTickThread(final ServerLevel world, final BlockPos pos, final String reason) {
++ if (!isTickThreadFor(world, pos)) {
++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
+ }
+
-+ public static int floorLog2(final long value) {
-+ // xor is optimized subtract for 2^n -1
-+ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
-+ return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
++ public static void ensureTickThread(final ServerLevel world, final ChunkPos pos, final String reason) {
++ if (!isTickThreadFor(world, pos)) {
++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
+ }
+
-+ public static int roundCeilLog2(final int value) {
-+ // optimized variant of 1 << (32 - leading(val - 1))
-+ // given
-+ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
-+ // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
-+ // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
-+ // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1))
-+ // HIGH_BIT_32 >>> (-1 + leading(val - 1))
-+ return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1);
++ public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) {
++ if (!isTickThreadFor(world, chunkX, chunkZ)) {
++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
+ }
+
-+ public static long roundCeilLog2(final long value) {
-+ // see logic documented above
-+ return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1);
++ public static void ensureTickThread(final Entity entity, final String reason) {
++ if (!isTickThreadFor(entity)) {
++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
+ }
+
-+ public static int roundFloorLog2(final int value) {
-+ // optimized variant of 1 << (31 - leading(val))
-+ // given
-+ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
-+ // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val)))
-+ // HIGH_BIT_32 >> (31 - (31 - leading(val)))
-+ // HIGH_BIT_32 >> (31 - 31 + leading(val))
-+ return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value);
++ public static void ensureTickThread(final ServerLevel world, final AABB aabb, final String reason) {
++ if (!isTickThreadFor(world, aabb)) {
++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
+ }
+
-+ public static long roundFloorLog2(final long value) {
-+ // see logic documented above
-+ return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value);
++ public static void ensureTickThread(final ServerLevel world, final double blockX, final double blockZ, final String reason) {
++ if (!isTickThreadFor(world, blockX, blockZ)) {
++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
++ throw new IllegalStateException(reason);
++ }
+ }
+
-+ public static boolean isPowerOfTwo(final int n) {
-+ // 2^n has one bit
-+ // note: this rets true for 0 still
-+ return IntegerUtil.getTrailingBit(n) == n;
++ public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */
++
++ private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
++
++ public TickThread(final String name) {
++ this(null, name);
+ }
+
-+ public static boolean isPowerOfTwo(final long n) {
-+ // 2^n has one bit
-+ // note: this rets true for 0 still
-+ return IntegerUtil.getTrailingBit(n) == n;
++ public TickThread(final Runnable run, final String name) {
++ this(run, name, ID_GENERATOR.incrementAndGet());
+ }
+
-+ public static int getTrailingBit(final int n) {
-+ return -n & n;
++ private TickThread(final Runnable run, final String name, final int id) {
++ super(run, name);
++ this.id = id;
+ }
+
-+ public static long getTrailingBit(final long n) {
-+ return -n & n;
++ public static TickThread getCurrentTickThread() {
++ return (TickThread)Thread.currentThread();
+ }
+
-+ public static int trailingZeros(final int n) {
-+ return Integer.numberOfTrailingZeros(n);
++ public static boolean isTickThread() {
++ return org.bukkit.Bukkit.isPrimaryThread(); // Paper
+ }
+
-+ public static int trailingZeros(final long n) {
-+ return Long.numberOfTrailingZeros(n);
++ public static boolean isShutdownThread() {
++ return false;
+ }
+
-+ // from hacker's delight (signed division magic value)
-+ public static int getDivisorMultiple(final long numbers) {
-+ return (int)(numbers >>> 32);
++ public static boolean isTickThreadFor(final ServerLevel world, final BlockPos pos) {
++ return isTickThread();
+ }
+
-+ // from hacker's delight (signed division magic value)
-+ public static int getDivisorShift(final long numbers) {
-+ return (int)numbers;
++ public static boolean isTickThreadFor(final ServerLevel world, final ChunkPos pos) {
++ return isTickThread();
+ }
+
-+ // copied from hacker's delight (signed division magic value)
-+ // http://www.hackersdelight.org/hdcodetxt/magic.c.txt
-+ public static long getDivisorNumbers(final int d) {
-+ final int ad = branchlessAbs(d);
++ public static boolean isTickThreadFor(final ServerLevel world, final Vec3 pos) {
++ return isTickThread();
++ }
+
-+ if (ad < 2) {
-+ throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d);
-+ }
++ public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) {
++ return isTickThread();
++ }
+
-+ final int two31 = 0x80000000;
-+ final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour
++ public static boolean isTickThreadFor(final ServerLevel world, final AABB aabb) {
++ return isTickThread();
++ }
+
-+ /*
-+ Signed usage:
-+ int number;
-+ long magic = getDivisorNumbers(div);
-+ long mul = magic >>> 32;
-+ int sign = number >> 31;
-+ int result = (int)(((long)number * mul) >>> magic) - sign;
-+ */
-+ /*
-+ Unsigned usage:
-+ int number;
-+ long magic = getDivisorNumbers(div);
-+ long mul = magic >>> 32;
-+ int result = (int)(((long)number * mul) >>> magic);
-+ */
++ public static boolean isTickThreadFor(final ServerLevel world, final double blockX, final double blockZ) {
++ return isTickThread();
++ }
+
-+ int p = 31;
-+
-+ // all these variables are UNSIGNED!
-+ int t = two31 + (d >>> 31);
-+ int anc = t - 1 - (int)((t & mask)%ad);
-+ int q1 = (int)((two31 & mask)/(anc & mask));
-+ int r1 = two31 - q1*anc;
-+ int q2 = (int)((two31 & mask)/(ad & mask));
-+ int r2 = two31 - q2*ad;
-+ int delta;
-+
-+ do {
-+ p = p + 1;
-+ q1 = 2*q1; // Update q1 = 2**p/|nc|.
-+ r1 = 2*r1; // Update r1 = rem(2**p, |nc|).
-+ if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here)
-+ q1 = q1 + 1;
-+ r1 = r1 - anc;
-+ }
-+ q2 = 2*q2; // Update q2 = 2**p/|d|.
-+ r2 = 2*r2; // Update r2 = rem(2**p, |d|).
-+ if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here)
-+ q2 = q2 + 1;
-+ r2 = r2 - ad;
-+ }
-+ delta = ad - r2;
-+ } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0));
++ public static boolean isTickThreadFor(final ServerLevel world, final Vec3 position, final Vec3 deltaMovement, final int buffer) {
++ return isTickThread();
++ }
+
-+ int magicNum = q2 + 1;
-+ if (d < 0) {
-+ magicNum = -magicNum;
-+ }
-+ int shift = p;
-+ return ((long)magicNum << 32) | shift;
++ public static boolean isTickThreadFor(final ServerLevel world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) {
++ return isTickThread();
+ }
+
-+ public static int branchlessAbs(final int val) {
-+ // -n = -1 ^ n + 1
-+ final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0
-+ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
++ public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) {
++ return isTickThread();
+ }
+
-+ public static long branchlessAbs(final long val) {
-+ // -n = -1 ^ n + 1
-+ final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0
-+ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
++ public static boolean isTickThreadFor(final Entity entity) {
++ return isTickThread();
+ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..af9623240ff2d389aa7090623f507720e7dbab7d
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
+@@ -0,0 +1,54 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.LevelHeightAccessor;
+
-+ //https://github.com/skeeto/hash-prospector for hash functions
++public final class WorldUtil {
++
++ // min, max are inclusive
+
-+ //score = ~590.47984224483832
-+ public static int hash0(int x) {
-+ x *= 0x36935555;
-+ x ^= x >>> 16;
-+ return x;
++ public static int getMaxSection(final LevelHeightAccessor world) {
++ return world.getMaxSection() - 1; // getMaxSection() is exclusive
+ }
+
-+ //score = ~310.01596637036749
-+ public static int hash1(int x) {
-+ x ^= x >>> 15;
-+ x *= 0x356aaaad;
-+ x ^= x >>> 17;
-+ return x;
++ public static int getMinSection(final LevelHeightAccessor world) {
++ return world.getMinSection();
+ }
+
-+ public static int hash2(int x) {
-+ x ^= x >>> 16;
-+ x *= 0x7feb352d;
-+ x ^= x >>> 15;
-+ x *= 0x846ca68b;
-+ x ^= x >>> 16;
-+ return x;
++ public static int getMaxLightSection(final LevelHeightAccessor world) {
++ return getMaxSection(world) + 1;
+ }
+
-+ public static int hash3(int x) {
-+ x ^= x >>> 17;
-+ x *= 0xed5ad4bb;
-+ x ^= x >>> 11;
-+ x *= 0xac4c1b51;
-+ x ^= x >>> 15;
-+ x *= 0x31848bab;
-+ x ^= x >>> 14;
-+ return x;
++ public static int getMinLightSection(final LevelHeightAccessor world) {
++ return getMinSection(world) - 1;
+ }
+
-+ //score = ~365.79959673201887
-+ public static long hash1(long x) {
-+ x ^= x >>> 27;
-+ x *= 0xb24924b71d2d354bL;
-+ x ^= x >>> 28;
-+ return x;
++
++
++ public static int getTotalSections(final LevelHeightAccessor world) {
++ return getMaxSection(world) - getMinSection(world) + 1;
+ }
+
-+ //h2 hash
-+ public static long hash2(long x) {
-+ x ^= x >>> 32;
-+ x *= 0xd6e8feb86659fd93L;
-+ x ^= x >>> 32;
-+ x *= 0xd6e8feb86659fd93L;
-+ x ^= x >>> 32;
-+ return x;
++ public static int getTotalLightSections(final LevelHeightAccessor world) {
++ return getMaxLightSection(world) - getMinLightSection(world) + 1;
+ }
+
-+ public static long hash3(long x) {
-+ x ^= x >>> 45;
-+ x *= 0xc161abe5704b6c79L;
-+ x ^= x >>> 41;
-+ x *= 0xe3e5389aedbc90f7L;
-+ x ^= x >>> 56;
-+ x *= 0x1f9aba75a52db073L;
-+ x ^= x >>> 53;
-+ return x;
++ public static int getMinBlockY(final LevelHeightAccessor world) {
++ return getMinSection(world) << 4;
+ }
+
-+ private IntegerUtil() {
++ public static int getMaxBlockY(final LevelHeightAccessor world) {
++ return (getMaxSection(world) << 4) | 15;
++ }
++
++ public static String getWorldName(final Level world) {
++ if (world == null) {
++ return "null world";
++ }
++ return world.getWorld().getName(); // Paper
++ }
++
++ private WorldUtil() {
+ throw new RuntimeException();
+ }
+}
+diff --git a/src/main/java/com/mojang/logging/LogUtils.java b/src/main/java/com/mojang/logging/LogUtils.java
+index 46cab7a8c7b87ab01b26074b04f5a02b3907cfc4..49019b4a9bc4e634d54a9b0acaf9229a5c896f85 100644
+--- a/src/main/java/com/mojang/logging/LogUtils.java
++++ b/src/main/java/com/mojang/logging/LogUtils.java
+@@ -61,4 +61,9 @@ public class LogUtils {
+ public static Logger getLogger() {
+ return LoggerFactory.getLogger(STACK_WALKER.getCallerClass());
+ }
++ // Paper start
++ public static Logger getClassLogger() {
++ return LoggerFactory.getLogger(STACK_WALKER.getCallerClass().getSimpleName());
++ }
++ // Paper end
+ }
diff --git a/src/main/java/io/papermc/paper/util/IntervalledCounter.java b/src/main/java/io/papermc/paper/util/IntervalledCounter.java
new file mode 100644
-index 0000000000000000000000000000000000000000..c90acc3bde887b9c8f8d49fcc3195657c721bc14
+index 0000000000000000000000000000000000000000..197224e31175252d8438a8df585bbb65f2288d7f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/IntervalledCounter.java
-@@ -0,0 +1,128 @@
+@@ -0,0 +1,129 @@
+package io.papermc.paper.util;
+
+public final class IntervalledCounter {
@@ -2536,6 +4049,7 @@ index 0000000000000000000000000000000000000000..c90acc3bde887b9c8f8d49fcc3195657
+ } else {
+ // ordered from [head, length)
+ // then followed by [0, tail)
++
+ System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head);
+ System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail);
+
@@ -3147,845 +4661,6 @@ index 0000000000000000000000000000000000000000..f7114d5b8f2f93f62883e24da29afaf9
+ return foundFrame.orElse(null);
+ }
+}
-diff --git a/src/main/java/io/papermc/paper/util/WorldUtil.java b/src/main/java/io/papermc/paper/util/WorldUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..67bb91fcfb532a919954cd9d7733d09a6c3fec35
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/WorldUtil.java
-@@ -0,0 +1,46 @@
-+package io.papermc.paper.util;
-+
-+import net.minecraft.world.level.LevelHeightAccessor;
-+
-+public final class WorldUtil {
-+
-+ // min, max are inclusive
-+
-+ public static int getMaxSection(final LevelHeightAccessor world) {
-+ return world.getMaxSection() - 1; // getMaxSection() is exclusive
-+ }
-+
-+ public static int getMinSection(final LevelHeightAccessor world) {
-+ return world.getMinSection();
-+ }
-+
-+ public static int getMaxLightSection(final LevelHeightAccessor world) {
-+ return getMaxSection(world) + 1;
-+ }
-+
-+ public static int getMinLightSection(final LevelHeightAccessor world) {
-+ return getMinSection(world) - 1;
-+ }
-+
-+
-+
-+ public static int getTotalSections(final LevelHeightAccessor world) {
-+ return getMaxSection(world) - getMinSection(world) + 1;
-+ }
-+
-+ public static int getTotalLightSections(final LevelHeightAccessor world) {
-+ return getMaxLightSection(world) - getMinLightSection(world) + 1;
-+ }
-+
-+ public static int getMinBlockY(final LevelHeightAccessor world) {
-+ return getMinSection(world) << 4;
-+ }
-+
-+ public static int getMaxBlockY(final LevelHeightAccessor world) {
-+ return (getMaxSection(world) << 4) | 15;
-+ }
-+
-+ private WorldUtil() {
-+ throw new RuntimeException();
-+ }
-+}
-diff --git a/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java b/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0fd814f1d65c111266a2b20f86561839a4cef755
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java
-@@ -0,0 +1,334 @@
-+package io.papermc.paper.util.maplist;
-+
-+import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
-+import org.bukkit.Bukkit;
-+import java.util.Arrays;
-+import java.util.NoSuchElementException;
-+
-+public final class IteratorSafeOrderedReferenceSet<E> {
-+
-+ public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0;
-+
-+ protected final Reference2IntLinkedOpenHashMap<E> indexMap;
-+ protected int firstInvalidIndex = -1;
-+
-+ /* list impl */
-+ protected E[] listElements;
-+ protected int listSize;
-+
-+ protected final double maxFragFactor;
-+
-+ protected int iteratorCount;
-+
-+ private final boolean threadRestricted;
-+
-+ public IteratorSafeOrderedReferenceSet() {
-+ this(16, 0.75f, 16, 0.2);
-+ }
-+
-+ public IteratorSafeOrderedReferenceSet(final boolean threadRestricted) {
-+ this(16, 0.75f, 16, 0.2, threadRestricted);
-+ }
-+
-+ public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity,
-+ final double maxFragFactor) {
-+ this(setCapacity, setLoadFactor, arrayCapacity, maxFragFactor, false);
-+ }
-+ public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity,
-+ final double maxFragFactor, final boolean threadRestricted) {
-+ this.indexMap = new Reference2IntLinkedOpenHashMap<>(setCapacity, setLoadFactor);
-+ this.indexMap.defaultReturnValue(-1);
-+ this.maxFragFactor = maxFragFactor;
-+ this.listElements = (E[])new Object[arrayCapacity];
-+ this.threadRestricted = threadRestricted;
-+ }
-+
-+ /*
-+ public void check() {
-+ int iterated = 0;
-+ ReferenceOpenHashSet<E> check = new ReferenceOpenHashSet<>();
-+ if (this.listElements != null) {
-+ for (int i = 0; i < this.listSize; ++i) {
-+ Object obj = this.listElements[i];
-+ if (obj != null) {
-+ iterated++;
-+ if (!check.add((E)obj)) {
-+ throw new IllegalStateException("contains duplicate");
-+ }
-+ if (!this.contains((E)obj)) {
-+ throw new IllegalStateException("desync");
-+ }
-+ }
-+ }
-+ }
-+
-+ if (iterated != this.size()) {
-+ throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size());
-+ }
-+
-+ check.clear();
-+ iterated = 0;
-+ for (final java.util.Iterator<E> iterator = this.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
-+ final E element = iterator.next();
-+ iterated++;
-+ if (!check.add(element)) {
-+ throw new IllegalStateException("contains duplicate (iterator is wrong)");
-+ }
-+ if (!this.contains(element)) {
-+ throw new IllegalStateException("desync (iterator is wrong)");
-+ }
-+ }
-+
-+ if (iterated != this.size()) {
-+ throw new IllegalStateException("Size is mismatched! (iterator is wrong) Got " + iterated + ", expected " + this.size());
-+ }
-+ }
-+ */
-+
-+ protected final boolean allowSafeIteration() {
-+ return !this.threadRestricted || Bukkit.isPrimaryThread();
-+ }
-+
-+ protected final double getFragFactor() {
-+ return 1.0 - ((double)this.indexMap.size() / (double)this.listSize);
-+ }
-+
-+ public int createRawIterator() {
-+ if (this.allowSafeIteration()) {
-+ ++this.iteratorCount;
-+ }
-+ if (this.indexMap.isEmpty()) {
-+ return -1;
-+ } else {
-+ return this.firstInvalidIndex == 0 ? this.indexMap.getInt(this.indexMap.firstKey()) : 0;
-+ }
-+ }
-+
-+ public int advanceRawIterator(final int index) {
-+ final E[] elements = this.listElements;
-+ int ret = index + 1;
-+ for (int len = this.listSize; ret < len; ++ret) {
-+ if (elements[ret] != null) {
-+ return ret;
-+ }
-+ }
-+
-+ return -1;
-+ }
-+
-+ public void finishRawIterator() {
-+ if (this.allowSafeIteration() && --this.iteratorCount == 0) {
-+ if (this.getFragFactor() >= this.maxFragFactor) {
-+ this.defrag();
-+ }
-+ }
-+ }
-+
-+ public boolean remove(final E element) {
-+ final int index = this.indexMap.removeInt(element);
-+ if (index >= 0) {
-+ if (this.firstInvalidIndex < 0 || index < this.firstInvalidIndex) {
-+ this.firstInvalidIndex = index;
-+ }
-+ if (this.listElements[index] != element) {
-+ throw new IllegalStateException();
-+ }
-+ this.listElements[index] = null;
-+ if (this.allowSafeIteration() && this.iteratorCount == 0 && this.getFragFactor() >= this.maxFragFactor) {
-+ this.defrag();
-+ }
-+ //this.check();
-+ return true;
-+ }
-+ return false;
-+ }
-+
-+ public boolean contains(final E element) {
-+ return this.indexMap.containsKey(element);
-+ }
-+
-+ public boolean add(final E element) {
-+ final int listSize = this.listSize;
-+
-+ final int previous = this.indexMap.putIfAbsent(element, listSize);
-+ if (previous != -1) {
-+ return false;
-+ }
-+
-+ if (listSize >= this.listElements.length) {
-+ this.listElements = Arrays.copyOf(this.listElements, listSize * 2);
-+ }
-+ this.listElements[listSize] = element;
-+ this.listSize = listSize + 1;
-+
-+ //this.check();
-+ return true;
-+ }
-+
-+ protected void defrag() {
-+ if (this.firstInvalidIndex < 0) {
-+ return; // nothing to do
-+ }
-+
-+ if (this.indexMap.isEmpty()) {
-+ Arrays.fill(this.listElements, 0, this.listSize, null);
-+ this.listSize = 0;
-+ this.firstInvalidIndex = -1;
-+ //this.check();
-+ return;
-+ }
-+
-+ final E[] backingArray = this.listElements;
-+
-+ int lastValidIndex;
-+ java.util.Iterator<Reference2IntMap.Entry<E>> iterator;
-+
-+ if (this.firstInvalidIndex == 0) {
-+ iterator = this.indexMap.reference2IntEntrySet().fastIterator();
-+ lastValidIndex = 0;
-+ } else {
-+ lastValidIndex = this.firstInvalidIndex;
-+ final E key = backingArray[lastValidIndex - 1];
-+ iterator = this.indexMap.reference2IntEntrySet().fastIterator(new Reference2IntMap.Entry<E>() {
-+ @Override
-+ public int getIntValue() {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public int setValue(int i) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public E getKey() {
-+ return key;
-+ }
-+ });
-+ }
-+
-+ while (iterator.hasNext()) {
-+ final Reference2IntMap.Entry<E> entry = iterator.next();
-+
-+ final int newIndex = lastValidIndex++;
-+ backingArray[newIndex] = entry.getKey();
-+ entry.setValue(newIndex);
-+ }
-+
-+ // cleanup end
-+ Arrays.fill(backingArray, lastValidIndex, this.listSize, null);
-+ this.listSize = lastValidIndex;
-+ this.firstInvalidIndex = -1;
-+ //this.check();
-+ }
-+
-+ public E rawGet(final int index) {
-+ return this.listElements[index];
-+ }
-+
-+ public int size() {
-+ // always returns the correct amount - listSize can be different
-+ return this.indexMap.size();
-+ }
-+
-+ public IteratorSafeOrderedReferenceSet.Iterator<E> iterator() {
-+ return this.iterator(0);
-+ }
-+
-+ public IteratorSafeOrderedReferenceSet.Iterator<E> iterator(final int flags) {
-+ if (this.allowSafeIteration()) {
-+ ++this.iteratorCount;
-+ }
-+ return new BaseIterator<>(this, true, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
-+ }
-+
-+ public java.util.Iterator<E> unsafeIterator() {
-+ return this.unsafeIterator(0);
-+ }
-+ public java.util.Iterator<E> unsafeIterator(final int flags) {
-+ return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
-+ }
-+
-+ public static interface Iterator<E> extends java.util.Iterator<E> {
-+
-+ public void finishedIterating();
-+
-+ }
-+
-+ protected static final class BaseIterator<E> implements IteratorSafeOrderedReferenceSet.Iterator<E> {
-+
-+ protected final IteratorSafeOrderedReferenceSet<E> set;
-+ protected final boolean canFinish;
-+ protected final int maxIndex;
-+ protected int nextIndex;
-+ protected E pendingValue;
-+ protected boolean finished;
-+ protected E lastReturned;
-+
-+ protected BaseIterator(final IteratorSafeOrderedReferenceSet<E> set, final boolean canFinish, final int maxIndex) {
-+ this.set = set;
-+ this.canFinish = canFinish;
-+ this.maxIndex = maxIndex;
-+ }
-+
-+ @Override
-+ public boolean hasNext() {
-+ if (this.finished) {
-+ return false;
-+ }
-+ if (this.pendingValue != null) {
-+ return true;
-+ }
-+
-+ final E[] elements = this.set.listElements;
-+ int index, len;
-+ for (index = this.nextIndex, len = Math.min(this.maxIndex, this.set.listSize); index < len; ++index) {
-+ final E element = elements[index];
-+ if (element != null) {
-+ this.pendingValue = element;
-+ this.nextIndex = index + 1;
-+ return true;
-+ }
-+ }
-+
-+ this.nextIndex = index;
-+ return false;
-+ }
-+
-+ @Override
-+ public E next() {
-+ if (!this.hasNext()) {
-+ throw new NoSuchElementException();
-+ }
-+ final E ret = this.pendingValue;
-+
-+ this.pendingValue = null;
-+ this.lastReturned = ret;
-+
-+ return ret;
-+ }
-+
-+ @Override
-+ public void remove() {
-+ final E lastReturned = this.lastReturned;
-+ if (lastReturned == null) {
-+ throw new IllegalStateException();
-+ }
-+ this.lastReturned = null;
-+ this.set.remove(lastReturned);
-+ }
-+
-+ @Override
-+ public void finishedIterating() {
-+ if (this.finished || !this.canFinish) {
-+ throw new IllegalStateException();
-+ }
-+ this.lastReturned = null;
-+ this.finished = true;
-+ if (this.set.allowSafeIteration()) {
-+ this.set.finishRawIterator();
-+ }
-+ }
-+ }
-+}
-diff --git a/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..c3ce8a42dddd76b7189ad5685b23f9d9f8ccadb3
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java
-@@ -0,0 +1,203 @@
-+package io.papermc.paper.util.player;
-+
-+import com.destroystokyo.paper.util.maplist.ReferenceList;
-+import io.papermc.paper.chunk.system.ChunkSystem;
-+import io.papermc.paper.util.CoordinateUtils;
-+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.world.level.ChunkPos;
-+
-+public final class NearbyPlayers {
-+
-+ public static enum NearbyMapType {
-+ GENERAL,
-+ GENERAL_SMALL,
-+ GENERAL_REALLY_SMALL,
-+ TICK_VIEW_DISTANCE,
-+ VIEW_DISTANCE;
-+ }
-+
-+ private static final NearbyMapType[] MOB_TYPES = NearbyMapType.values();
-+ public static final int TOTAL_MAP_TYPES = MOB_TYPES.length;
-+
-+ private static final int GENERAL_AREA_VIEW_DISTANCE = 33;
-+ private static final int GENERAL_SMALL_VIEW_DISTANCE = 10;
-+ private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3;
-+
-+ public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4);
-+ public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4);
-+ public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4);
-+
-+ private final ServerLevel world;
-+ private final Reference2ReferenceOpenHashMap<ServerPlayer, TrackedPlayer[]> players = new Reference2ReferenceOpenHashMap<>();
-+ private final Long2ReferenceOpenHashMap<TrackedChunk> byChunk = new Long2ReferenceOpenHashMap<>();
-+
-+ public NearbyPlayers(final ServerLevel world) {
-+ this.world = world;
-+ }
-+
-+ public void addPlayer(final ServerPlayer player) {
-+ final TrackedPlayer[] newTrackers = new TrackedPlayer[TOTAL_MAP_TYPES];
-+ if (this.players.putIfAbsent(player, newTrackers) != null) {
-+ throw new IllegalStateException("Already have player " + player);
-+ }
-+
-+ final ChunkPos chunk = player.chunkPosition();
-+
-+ for (int i = 0; i < TOTAL_MAP_TYPES; ++i) {
-+ // use 0 for default, will be updated by tickPlayer
-+ (newTrackers[i] = new TrackedPlayer(player, MOB_TYPES[i])).add(chunk.x, chunk.z, 0);
-+ }
-+
-+ // update view distances
-+ this.tickPlayer(player);
-+ }
-+
-+ public void removePlayer(final ServerPlayer player) {
-+ final TrackedPlayer[] players = this.players.remove(player);
-+ if (players == null) {
-+ return; // May be called during teleportation before the player is actually placed
-+ }
-+
-+ for (final TrackedPlayer tracker : players) {
-+ tracker.remove();
-+ }
-+ }
-+
-+ public void tickPlayer(final ServerPlayer player) {
-+ final TrackedPlayer[] players = this.players.get(player);
-+ if (players == null) {
-+ throw new IllegalStateException("Don't have player " + player);
-+ }
-+
-+ final ChunkPos chunk = player.chunkPosition();
-+
-+ players[NearbyMapType.GENERAL.ordinal()].update(chunk.x, chunk.z, GENERAL_AREA_VIEW_DISTANCE);
-+ players[NearbyMapType.GENERAL_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_SMALL_VIEW_DISTANCE);
-+ players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE);
-+ players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player));
-+ players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getLoadViewDistance(player));
-+ }
-+
-+ public TrackedChunk getChunk(final ChunkPos pos) {
-+ return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
-+ }
-+
-+ public TrackedChunk getChunk(final BlockPos pos) {
-+ return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
-+ }
-+
-+ public ReferenceList<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) {
-+ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos));
-+
-+ return chunk == null ? null : chunk.players[type.ordinal()];
-+ }
-+
-+ public ReferenceList<ServerPlayer> getPlayers(final ChunkPos pos, final NearbyMapType type) {
-+ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos));
-+
-+ return chunk == null ? null : chunk.players[type.ordinal()];
-+ }
-+
-+ public ReferenceList<ServerPlayer> getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) {
-+ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+
-+ return chunk == null ? null : chunk.players[type.ordinal()];
-+ }
-+
-+ public ReferenceList<ServerPlayer> getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) {
-+ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4));
-+
-+ return chunk == null ? null : chunk.players[type.ordinal()];
-+ }
-+
-+ public static final class TrackedChunk {
-+
-+ public final ReferenceList<ServerPlayer>[] players = new ReferenceList[TOTAL_MAP_TYPES];
-+ private int nonEmptyLists;
-+ private int updateCount;
-+
-+ public boolean isEmpty() {
-+ return this.nonEmptyLists == 0;
-+ }
-+
-+ public int getUpdateCount() {
-+ return this.updateCount;
-+ }
-+
-+ public ReferenceList<ServerPlayer> getPlayers(final NearbyMapType type) {
-+ return this.players[type.ordinal()];
-+ }
-+
-+ public void addPlayer(final ServerPlayer player, final NearbyMapType type) {
-+ ++this.updateCount;
-+ final int idx = type.ordinal();
-+ final ReferenceList<ServerPlayer> list = this.players[idx];
-+ if (list == null) {
-+ ++this.nonEmptyLists;
-+ (this.players[idx] = new ReferenceList<>()).add(player);
-+ return;
-+ }
-+
-+ if (!list.add(player)) {
-+ throw new IllegalStateException("Already contains player " + player);
-+ }
-+ }
-+
-+ public void removePlayer(final ServerPlayer player, final NearbyMapType type) {
-+ ++this.updateCount;
-+ final int idx = type.ordinal();
-+ final ReferenceList<ServerPlayer> list = this.players[idx];
-+ if (list == null) {
-+ throw new IllegalStateException("Does not contain player " + player);
-+ }
-+
-+ if (!list.remove(player)) {
-+ throw new IllegalStateException("Does not contain player " + player);
-+ }
-+
-+ if (list.size() == 0) {
-+ this.players[idx] = null;
-+ --this.nonEmptyLists;
-+ }
-+ }
-+ }
-+
-+ private final class TrackedPlayer extends SingleUserAreaMap<ServerPlayer> {
-+
-+ final NearbyMapType type;
-+
-+ public TrackedPlayer(final ServerPlayer player, final NearbyMapType type) {
-+ super(player);
-+ this.type = type;
-+ }
-+
-+ @Override
-+ protected void addCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) {
-+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+ NearbyPlayers.this.byChunk.computeIfAbsent(chunkKey, (final long keyInMap) -> {
-+ return new TrackedChunk();
-+ }).addPlayer(parameter, this.type);
-+ }
-+
-+ @Override
-+ protected void removeCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) {
-+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+ final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey);
-+ if (chunk == null) {
-+ throw new IllegalStateException("Chunk should exist at " + new ChunkPos(chunkKey));
-+ }
-+
-+ chunk.removePlayer(parameter, this.type);
-+
-+ if (chunk.isEmpty()) {
-+ NearbyPlayers.this.byChunk.remove(chunkKey);
-+ }
-+ }
-+ }
-+}
-diff --git a/src/main/java/io/papermc/paper/util/player/SingleUserAreaMap.java b/src/main/java/io/papermc/paper/util/player/SingleUserAreaMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..d603887f4d0464f4463172fd79bcd5298d54983e
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/player/SingleUserAreaMap.java
-@@ -0,0 +1,232 @@
-+package io.papermc.paper.util.player;
-+
-+import io.papermc.paper.util.IntegerUtil;
-+
-+public abstract class SingleUserAreaMap<T> {
-+
-+ private static final int NOT_SET = Integer.MIN_VALUE;
-+
-+ private final T parameter;
-+ private int lastChunkX = NOT_SET;
-+ private int lastChunkZ = NOT_SET;
-+ private int distance = NOT_SET;
-+
-+ public SingleUserAreaMap(final T parameter) {
-+ this.parameter = parameter;
-+ }
-+
-+ /* math sign function except 0 returns 1 */
-+ protected static int sign(int val) {
-+ return 1 | (val >> (Integer.SIZE - 1));
-+ }
-+
-+ protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ);
-+
-+ protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ);
-+
-+ private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) {
-+ final int maxX = chunkX + distance;
-+ final int maxZ = chunkZ + distance;
-+
-+ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
-+ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
-+ this.addCallback(parameter, cx, cz);
-+ }
-+ }
-+ }
-+
-+ private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) {
-+ final int maxX = chunkX + distance;
-+ final int maxZ = chunkZ + distance;
-+
-+ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
-+ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
-+ this.removeCallback(parameter, cx, cz);
-+ }
-+ }
-+ }
-+
-+ public final boolean add(final int chunkX, final int chunkZ, final int distance) {
-+ if (distance < 0) {
-+ throw new IllegalArgumentException(Integer.toString(distance));
-+ }
-+ if (this.lastChunkX != NOT_SET) {
-+ return false;
-+ }
-+ this.lastChunkX = chunkX;
-+ this.lastChunkZ = chunkZ;
-+ this.distance = distance;
-+
-+ this.addToNew(this.parameter, chunkX, chunkZ, distance);
-+
-+ return true;
-+ }
-+
-+ public final boolean update(final int toX, final int toZ, final int newViewDistance) {
-+ if (newViewDistance < 0) {
-+ throw new IllegalArgumentException(Integer.toString(newViewDistance));
-+ }
-+ final int fromX = this.lastChunkX;
-+ final int fromZ = this.lastChunkZ;
-+ final int oldViewDistance = this.distance;
-+ if (fromX == NOT_SET) {
-+ return false;
-+ }
-+
-+ this.lastChunkX = toX;
-+ this.lastChunkZ = toZ;
-+ this.distance = newViewDistance;
-+
-+ final T parameter = this.parameter;
-+
-+
-+ final int dx = toX - fromX;
-+ final int dz = toZ - fromZ;
-+
-+ final int totalX = IntegerUtil.branchlessAbs(fromX - toX);
-+ final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
-+
-+ if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
-+ // teleported?
-+ this.removeFromOld(parameter, fromX, fromZ, oldViewDistance);
-+ this.addToNew(parameter, toX, toZ, newViewDistance);
-+ return true;
-+ }
-+
-+ if (oldViewDistance != newViewDistance) {
-+ // remove loop
-+
-+ final int oldMinX = fromX - oldViewDistance;
-+ final int oldMinZ = fromZ - oldViewDistance;
-+ final int oldMaxX = fromX + oldViewDistance;
-+ final int oldMaxZ = fromZ + oldViewDistance;
-+ for (int currX = oldMinX; currX <= oldMaxX; ++currX) {
-+ for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) {
-+
-+ // only remove if we're outside the new view distance...
-+ if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) {
-+ this.removeCallback(parameter, currX, currZ);
-+ }
-+ }
-+ }
-+
-+ // add loop
-+
-+ final int newMinX = toX - newViewDistance;
-+ final int newMinZ = toZ - newViewDistance;
-+ final int newMaxX = toX + newViewDistance;
-+ final int newMaxZ = toZ + newViewDistance;
-+ for (int currX = newMinX; currX <= newMaxX; ++currX) {
-+ for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) {
-+
-+ // only add if we're outside the old view distance...
-+ if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) {
-+ this.addCallback(parameter, currX, currZ);
-+ }
-+ }
-+ }
-+
-+ return true;
-+ }
-+
-+ // x axis is width
-+ // z axis is height
-+ // right refers to the x axis of where we moved
-+ // top refers to the z axis of where we moved
-+
-+ // same view distance
-+
-+ // used for relative positioning
-+ final int up = sign(dz); // 1 if dz >= 0, -1 otherwise
-+ final int right = sign(dx); // 1 if dx >= 0, -1 otherwise
-+
-+ // The area excluded by overlapping the two view distance squares creates four rectangles:
-+ // Two on the left, and two on the right. The ones on the left we consider the "removed" section
-+ // and on the right the "added" section.
-+ // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually
-+ // exclusive to the regions they surround.
-+
-+ // 4 points of the rectangle
-+ int maxX; // exclusive
-+ int minX; // inclusive
-+ int maxZ; // exclusive
-+ int minZ; // inclusive
-+
-+ if (dx != 0) {
-+ // handle right addition
-+
-+ maxX = toX + (oldViewDistance * right) + right; // exclusive
-+ minX = fromX + (oldViewDistance * right) + right; // inclusive
-+ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
-+ minZ = toZ - (oldViewDistance * up); // inclusive
-+
-+ for (int currX = minX; currX != maxX; currX += right) {
-+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+ this.addCallback(parameter, currX, currZ);
-+ }
-+ }
-+ }
-+
-+ if (dz != 0) {
-+ // handle up addition
-+
-+ maxX = toX + (oldViewDistance * right) + right; // exclusive
-+ minX = toX - (oldViewDistance * right); // inclusive
-+ maxZ = toZ + (oldViewDistance * up) + up; // exclusive
-+ minZ = fromZ + (oldViewDistance * up) + up; // inclusive
-+
-+ for (int currX = minX; currX != maxX; currX += right) {
-+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+ this.addCallback(parameter, currX, currZ);
-+ }
-+ }
-+ }
-+
-+ if (dx != 0) {
-+ // handle left removal
-+
-+ maxX = toX - (oldViewDistance * right); // exclusive
-+ minX = fromX - (oldViewDistance * right); // inclusive
-+ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
-+ minZ = toZ - (oldViewDistance * up); // inclusive
-+
-+ for (int currX = minX; currX != maxX; currX += right) {
-+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+ this.removeCallback(parameter, currX, currZ);
-+ }
-+ }
-+ }
-+
-+ if (dz != 0) {
-+ // handle down removal
-+
-+ maxX = fromX + (oldViewDistance * right) + right; // exclusive
-+ minX = fromX - (oldViewDistance * right); // inclusive
-+ maxZ = toZ - (oldViewDistance * up); // exclusive
-+ minZ = fromZ - (oldViewDistance * up); // inclusive
-+
-+ for (int currX = minX; currX != maxX; currX += right) {
-+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
-+ this.removeCallback(parameter, currX, currZ);
-+ }
-+ }
-+ }
-+
-+ return true;
-+ }
-+
-+ public final boolean remove() {
-+ final int chunkX = this.lastChunkX;
-+ final int chunkZ = this.lastChunkZ;
-+ final int distance = this.distance;
-+ if (chunkX == NOT_SET) {
-+ return false;
-+ }
-+
-+ this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET;
-+
-+ this.removeFromOld(this.parameter, chunkX, chunkZ, distance);
-+
-+ return true;
-+ }
-+}
diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java
index 5135cd504ec5864a4603c004e748947a7d88d2b4..396f368a7e21a7c7b1630b4e20cdbc452c4b0f84 100644
--- a/src/main/java/net/minecraft/Util.java
@@ -4124,7 +4799,7 @@ index 3e5a85a7ad6149b04622c254fbc2e174896a4128..3f662692ed4846e026a9d48595e7b3b2
+
}
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 40adb6117b9e0d5f70103113202a07715e403e2a..9eb987f9d86396d6b7e9d4f3834bea3326640ac7 100644
+index 40adb6117b9e0d5f70103113202a07715e403e2a..cef1761cdaf3e456695f2de61f4295fb99361914 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -310,6 +310,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -4153,17 +4828,16 @@ index 40adb6117b9e0d5f70103113202a07715e403e2a..9eb987f9d86396d6b7e9d4f3834bea33
if ( tickCount++ % MinecraftServer.SAMPLE_INTERVAL == 0 )
{
long curTime = Util.getMillis();
-@@ -1337,7 +1342,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+@@ -1337,7 +1342,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
MinecraftServer.LOGGER.debug("Autosave finished");
SpigotTimings.worldSaveTimer.stopTiming(); // Spigot
}
-
-+ io.papermc.paper.util.CachedLists.reset(); // Paper
this.profiler.push("tallying");
long j = Util.getNanos() - i;
int k = this.tickCount % 100;
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-index f40a2f348c45a29168ca3d4eef07b5b628060bee..c643bb0daa5cd264fd6ebab7acf0a2bdd7fe7029 100644
+index f40a2f348c45a29168ca3d4eef07b5b628060bee..d0866b2c2f729b6c251eaade3758e94de4d05d6d 100644
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -37,9 +37,9 @@ public class ChunkHolder extends GenerationChunkHolder {
@@ -4288,7 +4962,7 @@ index f40a2f348c45a29168ca3d4eef07b5b628060bee..c643bb0daa5cd264fd6ebab7acf0a2bd
+ chunkResult.ifSuccess(chunk -> {
+ if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
+ ChunkHolder.this.isFullChunkReady = true;
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(chunk, this);
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkBorder(chunk, this);
+ }
+ });
+ });
@@ -4299,7 +4973,7 @@ index f40a2f348c45a29168ca3d4eef07b5b628060bee..c643bb0daa5cd264fd6ebab7acf0a2bd
if (flag && !flag1) {
+ // Paper start
+ if (this.isFullChunkReady) {
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
+ }
+ // Paper end
this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
@@ -4314,7 +4988,7 @@ index f40a2f348c45a29168ca3d4eef07b5b628060bee..c643bb0daa5cd264fd6ebab7acf0a2bd
+ chunkResult.ifSuccess(chunk -> {
+ // note: Here is a very good place to add callbacks to logic waiting on this.
+ ChunkHolder.this.isTickingReady = true;
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(chunk, this);
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkTicking(chunk, this);
+ });
+ });
+ // Paper end
@@ -4325,7 +4999,7 @@ index f40a2f348c45a29168ca3d4eef07b5b628060bee..c643bb0daa5cd264fd6ebab7acf0a2bd
- this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
+ // Paper start
+ if (this.isTickingReady) {
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
+ }
+ // Paper end
+ this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage
@@ -4340,7 +5014,7 @@ index f40a2f348c45a29168ca3d4eef07b5b628060bee..c643bb0daa5cd264fd6ebab7acf0a2bd
+ this.entityTickingChunkFuture.thenAccept(chunkResult -> {
+ chunkResult.ifSuccess(chunk -> {
+ ChunkHolder.this.isEntityTickingReady = true;
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(chunk, this);
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkEntityTicking(chunk, this);
+ });
+ });
+ // Paper end
@@ -4351,7 +5025,7 @@ index f40a2f348c45a29168ca3d4eef07b5b628060bee..c643bb0daa5cd264fd6ebab7acf0a2bd
- this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
+ // Paper start
+ if (this.isEntityTickingReady) {
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
+ }
+ // Paper end
+ this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
@@ -4378,62 +5052,27 @@ index f40a2f348c45a29168ca3d4eef07b5b628060bee..c643bb0daa5cd264fd6ebab7acf0a2bd
+ // Paper end
}
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 5b920beb39dad8d392b4e5e12a89880720e41942..d3caadb3e884d7d0468daf5eff9abd6629ac4b49 100644
+index 5b920beb39dad8d392b4e5e12a89880720e41942..6751e403595170b22abf100a27f97251edcb2125 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -170,6 +170,37 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -170,6 +170,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
};
// CraftBukkit end
-+ // Paper start - distance maps
-+ private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets<ServerPlayer> pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>();
-+
-+ void addPlayerToDistanceMaps(ServerPlayer player) {
-+ int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX());
-+ int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ());
-+ // Note: players need to be explicitly added to distance maps before they can be updated
-+ this.nearbyPlayers.addPlayer(player);
-+ }
-+
-+ void removePlayerFromDistanceMaps(ServerPlayer player) {
-+ int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX());
-+ int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ());
-+ // Note: players need to be explicitly added to distance maps before they can be updated
-+ this.nearbyPlayers.removePlayer(player);
-+ }
-+
-+ void updateMaps(ServerPlayer player) {
-+ int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX());
-+ int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ());
-+ // Note: players need to be explicitly added to distance maps before they can be updated
-+ this.nearbyPlayers.tickPlayer(player);
-+ }
-+ // Paper end
+ // Paper start
+ public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
-+ return this.pendingUnloads.get(io.papermc.paper.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ return this.pendingUnloads.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
-+ public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers;
+ // Paper end
+
public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
this.visibleChunkMap = this.updatingChunkMap.clone();
-@@ -221,8 +252,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world.getServer(), world);
- this.setServerViewDistance(viewDistance);
+@@ -223,6 +229,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, this.mainThreadMailbox);
-+ // Paper start
-+ this.nearbyPlayers = new io.papermc.paper.util.player.NearbyPlayers(this.level);
-+ // Paper end
-+ }
-+
-+ // Paper start
-+ // always use accessor, so folia can override
-+ public final io.papermc.paper.util.player.NearbyPlayers getNearbyPlayers() {
-+ return this.nearbyPlayers;
}
++ // Paper start
+ public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
+ return -1;
+ }
@@ -4442,24 +5081,24 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..d3caadb3e884d7d0468daf5eff9abd66
protected ChunkGenerator generator() {
return this.worldGenContext.generator();
}
-@@ -378,9 +423,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -378,9 +390,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
};
stringbuilder.append("Updating:").append(System.lineSeparator());
- this.updatingChunkMap.values().forEach(consumer);
-+ io.papermc.paper.chunk.system.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
stringbuilder.append("Visible:").append(System.lineSeparator());
- this.visibleChunkMap.values().forEach(consumer);
-+ io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(consumer); // Paper
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).forEach(consumer); // Paper
CrashReport crashreport = CrashReport.forThrowable(exception, "Chunk loading");
CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk loading");
-@@ -422,8 +467,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -422,8 +434,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
holder.setTicketLevel(level);
} else {
holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this);
+ // Paper start
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderCreate(this.level, holder);
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderCreate(this.level, holder);
+ // Paper end
}
@@ -4469,34 +5108,34 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..d3caadb3e884d7d0468daf5eff9abd66
this.updatingChunkMap.put(pos, holder);
this.modified = true;
}
-@@ -445,7 +496,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -445,7 +463,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
protected void saveAllChunks(boolean flush) {
if (flush) {
- List<ChunkHolder> list = this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList();
-+ List<ChunkHolder> list = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper
++ List<ChunkHolder> list = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper
MutableBoolean mutableboolean = new MutableBoolean();
do {
-@@ -468,7 +519,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -468,7 +486,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
});
this.flushWorker();
} else {
- this.visibleChunkMap.values().forEach(this::saveChunkIfNeeded);
-+ io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded);
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded);
}
}
-@@ -487,7 +538,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -487,7 +505,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public boolean hasWork() {
- return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets();
-+ return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || io.papermc.paper.chunk.system.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper
++ return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || ca.spottedleaf.moonrise.common.util.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper
}
private void processUnloads(BooleanSupplier shouldKeepTicking) {
-@@ -504,6 +555,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -504,6 +522,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
this.updatingChunkMap.remove(j);
@@ -4504,16 +5143,16 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..d3caadb3e884d7d0468daf5eff9abd66
this.pendingUnloads.put(j, playerchunk);
this.modified = true;
++i;
-@@ -523,7 +575,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -523,7 +542,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
int l = 0;
- ObjectIterator<ChunkHolder> objectiterator = this.visibleChunkMap.values().iterator();
-+ Iterator<ChunkHolder> objectiterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
++ Iterator<ChunkHolder> objectiterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) {
if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) {
-@@ -541,7 +593,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -541,7 +560,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
} else {
ChunkAccess ichunkaccess = holder.getLatestChunk();
@@ -4521,23 +5160,23 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..d3caadb3e884d7d0468daf5eff9abd66
+ // Paper start
+ boolean removed;
+ if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) {
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder);
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, holder);
+ // Paper end
LevelChunk chunk;
if (ichunkaccess instanceof LevelChunk) {
-@@ -559,7 +615,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -559,7 +582,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.lightEngine.tryScheduleUpdate();
this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null);
this.chunkSaveCooldowns.remove(ichunkaccess.getPos().toLong());
- }
+ } else if (removed) { // Paper start
-+ io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder);
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, holder);
+ } // Paper end
}
};
-@@ -896,7 +954,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -896,7 +921,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
}
@@ -4546,7 +5185,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..d3caadb3e884d7d0468daf5eff9abd66
int j = Mth.clamp(watchDistance, 2, 32);
if (j != this.serverViewDistance) {
-@@ -913,7 +971,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -913,7 +938,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
@@ -4555,28 +5194,28 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..d3caadb3e884d7d0468daf5eff9abd66
return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance);
}
-@@ -942,7 +1000,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -942,7 +967,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public int size() {
- return this.visibleChunkMap.size();
-+ return io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper
++ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper
}
public DistanceManager getDistanceManager() {
-@@ -950,19 +1008,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -950,19 +975,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
protected Iterable<ChunkHolder> getChunks() {
- return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
-+ return Iterables.unmodifiableIterable(io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level)); // Paper
++ return Iterables.unmodifiableIterable(ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level)); // Paper
}
void dumpChunks(Writer writer) throws IOException {
CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer);
TickingTracker tickingtracker = this.distanceManager.tickingTracker();
- ObjectBidirectionalIterator objectbidirectionaliterator = this.visibleChunkMap.long2ObjectEntrySet().iterator();
-+ Iterator<ChunkHolder> objectbidirectionaliterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
++ Iterator<ChunkHolder> objectbidirectionaliterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
while (objectbidirectionaliterator.hasNext()) {
- Entry<ChunkHolder> entry = (Entry) objectbidirectionaliterator.next();
@@ -4589,31 +5228,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..d3caadb3e884d7d0468daf5eff9abd66
Optional<ChunkAccess> optional = Optional.ofNullable(playerchunk.getLatestChunk());
Optional<LevelChunk> optional1 = optional.flatMap((ichunkaccess) -> {
return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty();
-@@ -1083,6 +1141,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-
- player.setChunkTrackingView(ChunkTrackingView.EMPTY);
- this.updateChunkTracking(player);
-+ this.addPlayerToDistanceMaps(player); // Paper - distance maps
- } else {
- SectionPos sectionposition = player.getLastSectionPos();
-
-@@ -1091,6 +1150,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- this.distanceManager.removePlayer(sectionposition, player);
- }
-
-+ this.removePlayerFromDistanceMaps(player); // Paper - distance maps
- this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY);
- }
-
-@@ -1142,6 +1202,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- this.updateChunkTracking(player);
- }
-
-+ this.updateMaps(player); // Paper - distance maps
- }
-
- private void updateChunkTracking(ServerPlayer player) {
-@@ -1385,10 +1446,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1385,10 +1410,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
});
}
@@ -4659,7 +5274,7 @@ index b6cc33943fe7e4667944f3e6f868b3033ea9ca18..27065ffc5473c518acee3a3096b83fac
while (objectiterator.hasNext()) {
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-index d39268911ed7c4d60ee6a82178be23245aae58c4..e9f53f57c363a32106880ea9aad0ccf5a7342509 100644
+index d39268911ed7c4d60ee6a82178be23245aae58c4..cf94dd9ddcc1eabcf3fd336e70720f4ed3e52175 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -48,6 +48,7 @@ import net.minecraft.world.level.storage.LevelStorageSource;
@@ -4670,20 +5285,18 @@ index d39268911ed7c4d60ee6a82178be23245aae58c4..e9f53f57c363a32106880ea9aad0ccf5
private static final List<ChunkStatus> CHUNK_STATUSES = ChunkStatus.getStatusList();
private final DistanceManager distanceManager;
final ServerLevel level;
-@@ -66,6 +67,12 @@ public class ServerChunkCache extends ChunkSource {
+@@ -66,6 +67,10 @@ public class ServerChunkCache extends ChunkSource {
@Nullable
@VisibleForDebug
private NaturalSpawner.SpawnState lastSpawnState;
+ // Paper start
-+ public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> tickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
-+ public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
+ private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<net.minecraft.world.level.chunk.LevelChunk> fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>();
+ long chunkFutureAwaitCounter;
+ // Paper end
public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) {
this.level = world;
-@@ -91,6 +98,54 @@ public class ServerChunkCache extends ChunkSource {
+@@ -91,6 +96,54 @@ public class ServerChunkCache extends ChunkSource {
return chunk.getFullChunkNow() != null;
}
// CraftBukkit end
@@ -4738,7 +5351,7 @@ index d39268911ed7c4d60ee6a82178be23245aae58c4..e9f53f57c363a32106880ea9aad0ccf5
@Override
public ThreadedLevelLightEngine getLightEngine() {
-@@ -286,7 +341,7 @@ public class ServerChunkCache extends ChunkSource {
+@@ -286,7 +339,7 @@ public class ServerChunkCache extends ChunkSource {
return this.mainThreadProcessor.pollTask();
}
@@ -4747,7 +5360,7 @@ index d39268911ed7c4d60ee6a82178be23245aae58c4..e9f53f57c363a32106880ea9aad0ccf5
boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
boolean flag1 = this.chunkMap.promoteChunkMap();
-@@ -299,6 +354,12 @@ public class ServerChunkCache extends ChunkSource {
+@@ -299,6 +352,12 @@ public class ServerChunkCache extends ChunkSource {
}
}
@@ -4761,7 +5374,7 @@ index d39268911ed7c4d60ee6a82178be23245aae58c4..e9f53f57c363a32106880ea9aad0ccf5
ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index 9d11fcb3df12182ae00ce73f7e30091fd199a341..eea8bafd98e3a8d82b3216488537ab898cc4ae7a 100644
+index 9d11fcb3df12182ae00ce73f7e30091fd199a341..4c39d9e0466240b5cd459ee649a22fe3a72bf9f0 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -236,6 +236,98 @@ public class ServerLevel extends Level implements WorldGenLevel {
@@ -4852,7 +5465,7 @@ index 9d11fcb3df12182ae00ce73f7e30091fd199a341..eea8bafd98e3a8d82b3216488537ab89
+
+ for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
+ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
-+ io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
+ this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer
+ );
+ }
@@ -4864,27 +5477,17 @@ index 9d11fcb3df12182ae00ce73f7e30091fd199a341..eea8bafd98e3a8d82b3216488537ab89
public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
// IRegistryCustom.Dimension iregistrycustom_dimension = minecraftserver.registryAccess(); // CraftBukkit - decompile error
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-index defe0b30964613cbae5195485aafff21d73ff18e..684dfbcdd4d000ac918c0f77a60c9abf9b30d3a4 100644
+index defe0b30964613cbae5195485aafff21d73ff18e..8d535d96252068fd2a1608600ce29d5d16690fec 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-@@ -280,6 +280,8 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+@@ -280,6 +280,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
public boolean sentListPacket = false;
public String kickLeaveMessage = null; // SPIGOT-3034: Forward leave message to PlayerQuitEvent
// CraftBukkit end
+ public boolean isRealPlayer; // Paper
-+ public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> cachedSingleHashSet; // Paper
public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);
-@@ -349,6 +351,8 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
- this.updateOptions(clientOptions);
- this.object = null;
-
-+ this.cachedSingleHashSet = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper
-+
- // CraftBukkit start
- this.displayName = this.getScoreboardName();
- this.bukkitPickUpLoot = true;
diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
index 045b754b5b70bbd1e7732ad2142dfadd6cc2305c..f56e5c0f53f9b52a9247b9be9265b949494fc924 100644
--- a/src/main/java/net/minecraft/server/level/TicketType.java
@@ -5517,7 +6120,7 @@ index ae16b014abd52ee10d523fb003cce166b846b222..7f302405a88766c2112539d24d3dd2e5
public BlockState getBlockState(BlockPos pos) {
int i = pos.getY();
diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
-index 34933c5324126f9afdc5cba9dea997ace8f01806..219062cff8a05c765b092f1525043d9d9a1153ae 100644
+index 34933c5324126f9afdc5cba9dea997ace8f01806..1cfc906317f07a44f06a4adf021c44e34a2f1d07 100644
--- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
+++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
@@ -91,6 +91,18 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A
@@ -5529,8 +6132,8 @@ index 34933c5324126f9afdc5cba9dea997ace8f01806..219062cff8a05c765b092f1525043d9d
+ // I don't want to know why this is a generic type.
+ Entity entityCasted = (Entity)entity;
+ boolean wasRemoved = entityCasted.isRemoved();
-+ io.papermc.paper.chunk.system.ChunkSystem.onEntityPreAdd((net.minecraft.server.level.ServerLevel) entityCasted.level(), entityCasted);
-+ if (!wasRemoved && entityCasted.isRemoved()) {
++ boolean screened = ca.spottedleaf.moonrise.common.util.ChunkSystem.screenEntity((net.minecraft.server.level.ServerLevel)entityCasted.level(), entityCasted);
++ if ((!wasRemoved && entityCasted.isRemoved()) || !screened) {
+ // removed by callback
+ return false;
+ }
@@ -5554,7 +6157,7 @@ index fe0f57dbeecc4b5a0c81863f33e41d11eb60943a..9babfd8e6c847ea26863be6243f17fc2
+ }
}
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-index 508419378c88ba8688edbd5142d9d8ba52396507..a59eebb89d11788b999d1e5cb4fd2f4e55e023ab 100644
+index 508419378c88ba8688edbd5142d9d8ba52396507..69c62699e3412f2730e3db65f196099d77698980 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -252,8 +252,8 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@@ -5563,7 +6166,7 @@ index 508419378c88ba8688edbd5142d9d8ba52396507..a59eebb89d11788b999d1e5cb4fd2f4e
public Chunk[] getLoadedChunks() {
- Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = this.world.getChunkSource().chunkMap.visibleChunkMap;
- return chunks.values().stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(CraftChunk::new).toArray(Chunk[]::new);
-+ List<ChunkHolder> chunks = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.world); // Paper
++ List<ChunkHolder> chunks = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.world); // Paper
+ return chunks.stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(CraftChunk::new).toArray(Chunk[]::new);
}
@@ -5599,7 +6202,7 @@ index 508419378c88ba8688edbd5142d9d8ba52396507..a59eebb89d11788b999d1e5cb4fd2f4e
+
+ java.util.concurrent.CompletableFuture<Chunk> ret = new java.util.concurrent.CompletableFuture<>();
+
-+ io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> {
++ ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> {
+ net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
+ net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c;
+ ret.complete(chunk == null ? null : new CraftChunk(chunk));
@@ -5634,7 +6237,7 @@ index 508419378c88ba8688edbd5142d9d8ba52396507..a59eebb89d11788b999d1e5cb4fd2f4e
+ // Paper end
}
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
-index e130d0aa64d0caaa7760d8de4b1f989523f9de20..45108559bb2e4ae9d33aed5d92b72cbe5c17f553 100644
+index e130d0aa64d0caaa7760d8de4b1f989523f9de20..9ca244b69995552df63fb5d4e3d6961b585bcc47 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -2420,4 +2420,34 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@@ -5644,7 +6247,7 @@ index e130d0aa64d0caaa7760d8de4b1f989523f9de20..45108559bb2e4ae9d33aed5d92b72cbe
+
+ @Override
+ public int getViewDistance() {
-+ return io.papermc.paper.chunk.system.ChunkSystem.getLoadViewDistance(this.getHandle());
++ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getLoadViewDistance(this.getHandle()) - 1;
+ }
+
+ @Override
@@ -5654,7 +6257,7 @@ index e130d0aa64d0caaa7760d8de4b1f989523f9de20..45108559bb2e4ae9d33aed5d92b72cbe
+
+ @Override
+ public int getSimulationDistance() {
-+ return io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(this.getHandle());
++ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getTickViewDistance(this.getHandle());
+ }
+
+ @Override
@@ -5664,7 +6267,7 @@ index e130d0aa64d0caaa7760d8de4b1f989523f9de20..45108559bb2e4ae9d33aed5d92b72cbe
+
+ @Override
+ public int getSendViewDistance() {
-+ return io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(this.getHandle());
++ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getSendViewDistance(this.getHandle());
+ }
+
+ @Override