diff options
Diffstat (limited to 'patches/server/0009-MC-Utils.patch')
-rw-r--r-- | patches/server/0009-MC-Utils.patch | 4943 |
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 <[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 |