PaperMC/patches/server/0009-MC-Utils.patch
Bjarne Koll c5a10665b8
Remove wall-time / unused skip tick protection (#11412)
Spigot still maintains some partial implementation of "tick skipping", a
practice in which the MinecraftServer.currentTick field is updated not
by an increment of one per actual tick, but instead set to
System.currentTimeMillis() / 50. This behaviour means that the tracked
tick may "skip" a tick value in case a previous tick took more than the
expected 50ms.

To compensate for this in important paths, spigot/craftbukkit
implements "wall-time". Instead of incrementing/decrementing ticks on
block entities/entities by one for each call to their tick() method,
they instead increment/decrement important values, like
an ItemEntity's age or pickupDelay, by the difference of
`currentTick - lastTick`, where `lastTick` is the value of
`currentTick` during the last tick() call.

These "fixes" however do not play nicely with minecraft's simulation
distance as entities/block entities implementing the above behaviour
would "catch up" their values when moving from a non-ticking chunk to a
ticking one as their `lastTick` value remains stuck on the last tick in
a ticking chunk and hence lead to a large "catch up" once ticked again.

Paper completely removes the "tick skipping" behaviour (See patch
"Further-improve-server-tick-loop"), making the above precautions
completely unnecessary, which also rids paper of the previous described
incompatibility with non-ticking chunks.
2024-09-19 16:36:07 +02:00

5785 lines
234 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Mon, 28 Mar 2016 20:55:47 -0400
Subject: [PATCH] MC Utils
== AT ==
public net.minecraft.server.level.ServerChunkCache mainThread
public net.minecraft.server.level.ServerLevel chunkSource
public org.bukkit.craftbukkit.inventory.CraftItemStack handle
public net.minecraft.server.level.ChunkMap getVisibleChunkIfPresent(J)Lnet/minecraft/server/level/ChunkHolder;
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/ca/spottedleaf/moonrise/common/list/EntityList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
new file mode 100644
index 0000000000000000000000000000000000000000..ba68998f6ef57b24c72fd833bd7de440de9501cc
--- /dev/null
+++ 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;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+// list with O(1) remove & contains
+
+/**
+ * @author Spottedleaf
+ */
+public final class EntityList implements Iterable<Entity> {
+
+ protected final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f);
+ {
+ this.entityToIndex.defaultReturnValue(Integer.MIN_VALUE);
+ }
+
+ protected static final Entity[] EMPTY_LIST = new Entity[0];
+
+ protected Entity[] entities = EMPTY_LIST;
+ protected int count;
+
+ public int size() {
+ return this.count;
+ }
+
+ public boolean contains(final Entity entity) {
+ return this.entityToIndex.containsKey(entity.getId());
+ }
+
+ public boolean remove(final Entity entity) {
+ final int index = this.entityToIndex.remove(entity.getId());
+ if (index == Integer.MIN_VALUE) {
+ return false;
+ }
+
+ // move the entity at the end to this index
+ final int endIndex = --this.count;
+ final Entity end = this.entities[endIndex];
+ if (index != endIndex) {
+ // not empty after this call
+ this.entityToIndex.put(end.getId(), index); // update index
+ }
+ this.entities[index] = end;
+ this.entities[endIndex] = null;
+
+ return true;
+ }
+
+ public boolean add(final Entity entity) {
+ final int count = this.count;
+ final int currIndex = this.entityToIndex.putIfAbsent(entity.getId(), count);
+
+ if (currIndex != Integer.MIN_VALUE) {
+ return false; // already in this list
+ }
+
+ Entity[] list = this.entities;
+
+ if (list.length == count) {
+ // resize required
+ list = this.entities = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
+ }
+
+ list[count] = entity;
+ this.count = count + 1;
+
+ return true;
+ }
+
+ public Entity getChecked(final int index) {
+ if (index < 0 || index >= this.count) {
+ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
+ }
+ return this.entities[index];
+ }
+
+ public Entity getUnchecked(final int index) {
+ return this.entities[index];
+ }
+
+ public Entity[] getRawData() {
+ return this.entities;
+ }
+
+ public void clear() {
+ this.entityToIndex.clear();
+ Arrays.fill(this.entities, 0, this.count, null);
+ this.count = 0;
+ }
+
+ @Override
+ public Iterator<Entity> iterator() {
+ return new Iterator<Entity>() {
+
+ Entity lastRet;
+ int current;
+
+ @Override
+ public boolean hasNext() {
+ return this.current < EntityList.this.count;
+ }
+
+ @Override
+ public Entity next() {
+ if (this.current >= EntityList.this.count) {
+ throw new NoSuchElementException();
+ }
+ return this.lastRet = EntityList.this.entities[this.current++];
+ }
+
+ @Override
+ public void remove() {
+ final Entity lastRet = this.lastRet;
+
+ if (lastRet == null) {
+ throw new IllegalStateException();
+ }
+ this.lastRet = null;
+
+ EntityList.this.remove(lastRet);
+ --this.current;
+ }
+ };
+ }
+}
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..fcfbca333234c09f7c056bbfcd9ac8860b20a8db
--- /dev/null
+++ 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;
+import java.util.Arrays;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.GlobalPalette;
+
+public final class IBlockDataList {
+
+ 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);
+ {
+ this.map.defaultReturnValue(Long.MAX_VALUE);
+ }
+
+ private static final long[] EMPTY_LIST = new long[0];
+
+ private long[] byIndex = EMPTY_LIST;
+ private int size;
+
+ public static int getLocationKey(final int x, final int y, final int z) {
+ return (x & 15) | (((z & 15) << 4)) | ((y & 255) << (4 + 4));
+ }
+
+ public static BlockState getBlockDataFromRaw(final long raw) {
+ return GLOBAL_PALETTE.valueFor((int)(raw >>> 32));
+ }
+
+ public static int getIndexFromRaw(final long raw) {
+ return (int)(raw & 0xFFFF);
+ }
+
+ public static int getLocationFromRaw(final long raw) {
+ return (int)((raw >>> 16) & 0xFFFF);
+ }
+
+ public static long getRawFromValues(final int index, final int location, final BlockState data) {
+ return (long)index | ((long)location << 16) | (((long)GLOBAL_PALETTE.idFor(data)) << 32);
+ }
+
+ public static long setIndexRawValues(final long value, final int index) {
+ return value & ~(0xFFFF) | (index);
+ }
+
+ public long add(final int x, final int y, final int z, final BlockState data) {
+ return this.add(getLocationKey(x, y, z), data);
+ }
+
+ public long add(final int location, final BlockState data) {
+ final long curr = this.map.get((short)location);
+
+ if (curr == Long.MAX_VALUE) {
+ final int index = this.size++;
+ final long raw = getRawFromValues(index, location, data);
+ this.map.put((short)location, raw);
+
+ if (index >= this.byIndex.length) {
+ this.byIndex = Arrays.copyOf(this.byIndex, (int)Math.max(4L, this.byIndex.length * 2L));
+ }
+
+ this.byIndex[index] = raw;
+ return raw;
+ } else {
+ final int index = getIndexFromRaw(curr);
+ final long raw = this.byIndex[index] = getRawFromValues(index, location, data);
+
+ this.map.put((short)location, raw);
+
+ return raw;
+ }
+ }
+
+ public long remove(final int x, final int y, final int z) {
+ return this.remove(getLocationKey(x, y, z));
+ }
+
+ public long remove(final int location) {
+ final long ret = this.map.remove((short)location);
+ final int index = getIndexFromRaw(ret);
+ if (ret == Long.MAX_VALUE) {
+ return ret;
+ }
+
+ // move the entry at the end to this index
+ final int endIndex = --this.size;
+ final long end = this.byIndex[endIndex];
+ if (index != endIndex) {
+ // not empty after this call
+ this.map.put((short)getLocationFromRaw(end), setIndexRawValues(end, index));
+ }
+ this.byIndex[index] = end;
+ this.byIndex[endIndex] = 0L;
+
+ return ret;
+ }
+
+ public int size() {
+ return this.size;
+ }
+
+ public long getRaw(final int index) {
+ return this.byIndex[index];
+ }
+
+ public int getLocation(final int index) {
+ return getLocationFromRaw(this.getRaw(index));
+ }
+
+ public BlockState getData(final int index) {
+ return getBlockDataFromRaw(this.getRaw(index));
+ }
+
+ public void clear() {
+ this.size = 0;
+ this.map.clear();
+ }
+
+ public LongIterator getRawIterator() {
+ return this.map.values().iterator();
+ }
+}
\ 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..c21e00812f1aaa1279834a0562d360d6b89e146c
--- /dev/null
+++ 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;
+
+public final class ReferenceList<E> implements Iterable<E> {
+
+ 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);
+ }
+
+ private ReferenceList(final E[] references, final int count, final Reference2IntOpenHashMap<E> referenceToIndex) {
+ this.references = references;
+ this.count = count;
+ this.referenceToIndex = referenceToIndex;
+ }
+
+ public ReferenceList<E> copy() {
+ return new ReferenceList<>(this.references.clone(), this.count, this.referenceToIndex.clone());
+ }
+
+ public int size() {
+ return this.count;
+ }
+
+ public boolean contains(final E obj) {
+ return this.referenceToIndex.containsKey(obj);
+ }
+
+ public boolean remove(final E obj) {
+ final int index = this.referenceToIndex.removeInt(obj);
+ if (index == Integer.MIN_VALUE) {
+ return false;
+ }
+
+ // move the object at the end to this index
+ final int endIndex = --this.count;
+ final E end = (E)this.references[endIndex];
+ if (index != endIndex) {
+ // not empty after this call
+ this.referenceToIndex.put(end, index); // update index
+ }
+ this.references[index] = end;
+ this.references[endIndex] = null;
+
+ return true;
+ }
+
+ public boolean add(final E obj) {
+ final int count = this.count;
+ final int currIndex = this.referenceToIndex.putIfAbsent(obj, count);
+
+ if (currIndex != Integer.MIN_VALUE) {
+ return false; // already in this list
+ }
+
+ E[] list = this.references;
+
+ if (list.length == count) {
+ // resize required
+ list = this.references = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
+ }
+
+ list[count] = obj;
+ this.count = count + 1;
+
+ return true;
+ }
+
+ public E getChecked(final int index) {
+ if (index < 0 || index >= this.count) {
+ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
+ }
+ return this.references[index];
+ }
+
+ public E getUnchecked(final int 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);
+ this.count = 0;
+ }
+
+ @Override
+ public Iterator<E> iterator() {
+ return new Iterator<>() {
+ private E lastRet;
+ private int current;
+
+ @Override
+ public boolean hasNext() {
+ return this.current < ReferenceList.this.count;
+ }
+
+ @Override
+ public E next() {
+ if (this.current >= ReferenceList.this.count) {
+ throw new NoSuchElementException();
+ }
+ return this.lastRet = ReferenceList.this.references[this.current++];
+ }
+
+ @Override
+ public void remove() {
+ final E lastRet = this.lastRet;
+
+ if (lastRet == null) {
+ throw new IllegalStateException();
+ }
+ this.lastRet = null;
+
+ ReferenceList.this.remove(lastRet);
+ --this.current;
+ }
+ };
+ }
+}
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..db92261a6cb3758391108361096417c61bc82cdc
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/SortedList.java
@@ -0,0 +1,117 @@
+package ca.spottedleaf.moonrise.common.list;
+
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.Comparator;
+
+public final class SortedList<E> {
+
+ private static final Object[] EMPTY_LIST = new Object[0];
+
+ private Comparator<? super E> comparator;
+ private E[] elements;
+ private int count;
+
+ public SortedList(final Comparator<? super E> comparator) {
+ this((E[])EMPTY_LIST, comparator);
+ }
+
+ public SortedList(final E[] elements, final Comparator<? super E> comparator) {
+ this.elements = elements;
+ this.comparator = comparator;
+ }
+
+ // 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;
+
+ final E middleVal = elements[middle];
+
+ final int cmp = comparator.compare(element, middleVal);
+
+ if (cmp < 0) {
+ end = middle - 1;
+ } else {
+ start = middle + 1;
+ }
+ }
+
+ return start;
+ }
+
+ public int size() {
+ return this.count;
+ }
+
+ public boolean isEmpty() {
+ return this.count == 0;
+ }
+
+ 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;
+
+ final int idx = insertIdx(elements, element, comparator, 0, count - 1);
+
+ 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 {
+ 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;
+ }
+ }
+ }
+
+ public E get(final int idx) {
+ if (idx < 0 || idx >= this.count) {
+ throw new IndexOutOfBoundsException(idx);
+ }
+ return this.elements[idx];
+ }
+
+
+ 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 int last = this.count - 1;
+ this.count = last;
+
+ final E ret = elements[idx];
+
+ 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;
+
+import it.unimi.dsi.fastutil.ints.Int2IntFunction;
+
+import java.util.Arrays;
+
+public class Int2IntArraySortedMap {
+
+ protected int[] key;
+ protected int[] val;
+ protected int size;
+
+ public Int2IntArraySortedMap() {
+ this.key = new int[8];
+ this.val = new int[8];
+ }
+
+ 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;
+
+ return 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;
+
+ this.key[insert] = key;
+
+ return this.val[insert] = producer.apply(key);
+ }
+
+ 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];
+ }
+
+ 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;
+
+import java.util.Arrays;
+import java.util.function.IntFunction;
+
+public class Int2ObjectArraySortedMap<V> {
+
+ protected int[] key;
+ protected V[] val;
+ protected int size;
+
+ public Int2ObjectArraySortedMap() {
+ this.key = new int[8];
+ this.val = (V[])new Object[8];
+ }
+
+ 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;
+ }
+ 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;
+ }
+
+ 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);
+
+ this.key[insert] = key;
+
+ return this.val[insert] = producer.apply(key);
+ }
+
+ 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];
+ }
+
+ 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;
+
+import it.unimi.dsi.fastutil.longs.Long2IntFunction;
+
+import java.util.Arrays;
+
+public class Long2IntArraySortedMap {
+
+ protected long[] key;
+ protected int[] val;
+ protected int size;
+
+ public Long2IntArraySortedMap() {
+ this.key = new long[8];
+ this.val = new int[8];
+ }
+
+ 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;
+ }
+ 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;
+ }
+
+ 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);
+ }
+
+ 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];
+ }
+
+ 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;
+
+import java.util.Arrays;
+import java.util.function.LongFunction;
+
+public class Long2ObjectArraySortedMap<V> {
+
+ protected long[] key;
+ protected V[] val;
+ protected int size;
+
+ 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;
+
+ this.key[insert] = key;
+ this.val[insert] = value;
+
+ return null;
+ }
+
+ 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;
+
+ this.key[insert] = key;
+
+ return this.val[insert] = producer.apply(key);
+ }
+
+ 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];
+ }
+
+ 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;
+
+import it.unimi.dsi.fastutil.longs.Long2BooleanFunction;
+import it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap;
+
+public final class SynchronisedLong2BooleanMap {
+ private final Long2BooleanLinkedOpenHashMap map = new Long2BooleanLinkedOpenHashMap();
+ private final int limit;
+
+ public SynchronisedLong2BooleanMap(final int limit) {
+ this.limit = limit;
+ }
+
+ // must hold lock on map
+ private void purgeEntries() {
+ while (this.map.size() > this.limit) {
+ this.map.removeLastBoolean();
+ }
+ }
+
+ public boolean remove(final long key) {
+ synchronized (this.map) {
+ return this.map.remove(key);
+ }
+ }
+
+ // note:
+ public boolean getOrCompute(final long key, final Long2BooleanFunction ifAbsent) {
+ synchronized (this.map) {
+ if (this.map.containsKey(key)) {
+ return this.map.getAndMoveToFirst(key);
+ }
+ }
+
+ final boolean put = ifAbsent.get(key);
+
+ 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;
+
+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
+import java.util.function.BiFunction;
+
+public final class SynchronisedLong2ObjectMap<V> {
+ private final Long2ObjectLinkedOpenHashMap<V> map = new Long2ObjectLinkedOpenHashMap<>();
+ private final int limit;
+
+ 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();
+ }
+ }
+
+ public V get(final long key) {
+ synchronized (this.map) {
+ return this.map.getAndMoveToFirst(key);
+ }
+ }
+
+ public V put(final long key, final V value) {
+ synchronized (this.map) {
+ final V ret = this.map.putAndMoveToFirst(key, value);
+ this.purgeEntries();
+ return ret;
+ }
+ }
+
+ 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;
+
+public final class AllocatingRateLimiter {
+
+ // max difference granularity in ns
+ private final long maxGranularity;
+
+ 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;
+ }
+
+ public void reset(final long time) {
+ this.allocation = 0.0;
+ this.lastAllocationUpdate = time;
+ this.takeCarry = 0.0;
+ this.lastTakeUpdate = time;
+ }
+
+ // 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));
+ }
+
+ 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/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java
new file mode 100644
index 0000000000000000000000000000000000000000..460e27ab0506c83a28934800ee74ee886d4b025e
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/Delayed26WayDistancePropagator3D.java
@@ -0,0 +1,297 @@
+package ca.spottedleaf.moonrise.common.misc;
+
+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;
+
+public final class Delayed26WayDistancePropagator3D {
+
+ // 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 LevelChangeCallback changeCallback;
+
+ public Delayed26WayDistancePropagator3D() {
+ this(null);
+ }
+
+ public Delayed26WayDistancePropagator3D(final LevelChangeCallback changeCallback) {
+ this.changeCallback = changeCallback;
+ }
+
+ public int getLevel(final long pos) {
+ return this.levels.get(pos);
+ }
+
+ public int getLevel(final int x, final int y, final int z) {
+ return this.levels.get(CoordinateUtils.getChunkSectionKey(x, y, z));
+ }
+
+ public void setSource(final int x, final int y, final int z, final int level) {
+ this.setSource(CoordinateUtils.getChunkSectionKey(x, y, z), level);
+ }
+
+ 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);
+ }
+
+ 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);
+ }
+ }
+
+ // 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;
+
+ 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);
+
+ this.levelIncreaseWorkQueueBitset |= (1L << level);
+ }
+
+ 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);
+
+ this.levelIncreaseWorkQueueBitset |= (1L << index);
+ }
+
+ 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;
+ }
+
+ 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.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;
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ 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;
+
+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;
+ }
+ }
+ }
+ }
+
+ if (expect != got) {
+ throw new IllegalStateException("Expected " + expect + " at pos (" + x + "," + z + ") but got " + got);
+ }
+ }
+
+ static class Ticket {
+
+ 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();
+ }
+
+ // 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 (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
+
+ 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
+
+ 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);
+ }
+ }
+ }
+ }
+ */
+
+ // 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 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 LevelChangeCallback changeCallback;
+
+ public Delayed8WayDistancePropagator2D() {
+ this(null);
+ }
+
+ public Delayed8WayDistancePropagator2D(final LevelChangeCallback changeCallback) {
+ this.changeCallback = changeCallback;
+ }
+
+ public int getLevel(final long pos) {
+ return this.levels.get(pos);
+ }
+
+ public int getLevel(final int x, final int z) {
+ return this.levels.get(CoordinateUtils.getChunkKey(x, z));
+ }
+
+ public void setSource(final int x, final int z, final int level) {
+ this.setSource(CoordinateUtils.getChunkKey(x, z), level);
+ }
+
+ 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);
+ }
+
+ public void removeSource(final int x, final int z) {
+ this.removeSource(CoordinateUtils.getChunkKey(x, z));
+ }
+
+ public void removeSource(final long coordinate) {
+ if (this.sources.remove(coordinate) != 0) {
+ this.updatedSources.add(coordinate);
+ }
+ }
+
+ // 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();
+ }
+ }
+ 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;
+
+ protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) {
+ final WorkQueue queue = this.levelIncreaseWorkQueues[level];
+ queue.queuedCoordinates.enqueue(coordinate);
+ queue.queuedLevels.enqueue(level);
+
+ this.levelIncreaseWorkQueueBitset |= (1L << level);
+ }
+
+ 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);
+ }
+
+ protected final void addToRemoveWorkQueue(final long coordinate, final byte level) {
+ final 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;
+ }
+
+ 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.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;
+ }
+
+ 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 (level == 1) {
+ // can't propagate 0 to neighbours
+ continue;
+ }
+
+ // 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);
+ }
+ }
+ }
+ }
+ }
+
+ 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;
+ }
+
+ // 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);
+ }
+ }
+ }
+ }
+
+ // propagate sources we clobbered in the process
+ this.propagateIncreases();
+ }
+
+ protected static final class LevelMap extends Long2ByteOpenHashMap {
+ public LevelMap() {
+ super();
+ }
+
+ public LevelMap(final int expected, final float loadFactor) {
+ super(expected, loadFactor);
+ }
+
+ // 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;
+ }
+ }
+
+ return -(pos + 1);
+ }
+ }
+ }
+
+ // copied from superclass
+ private void insert(final int pos, final long k, final byte v) {
+ if (pos == this.n) {
+ this.containsNullKey = true;
+ }
+
+ this.key[pos] = k;
+ this.value[pos] = v;
+ if (this.size++ >= this.maxFill) {
+ this.rehash(HashCommon.arraySize(this.size + 1, this.f));
+ }
+ }
+
+ // 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;
+ }
+ }
+
+ // 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);
+ }
+ }
+
+ // 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);
+ }
+ }
+
+ // 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;
+ }
+ }
+
+ return this.defRetValue;
+ }
+ }
+ }
+ }
+
+ 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;
+ }
+ }
+
+ protected static final class NoResizeByteArrayFIFODeque extends ByteArrayFIFOQueue {
+
+ /**
+ * 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;
+ }
+
+ 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;
+
+public final class NearbyPlayers {
+
+ public static enum NearbyMapType {
+ GENERAL,
+ GENERAL_SMALL,
+ GENERAL_REALLY_SMALL,
+ TICK_VIEW_DISTANCE,
+ VIEW_DISTANCE,
+ SPAWN_RANGE, // Moonrise - chunk tick iteration
+ }
+
+ private static final NearbyMapType[] MAP_TYPES = NearbyMapType.values();
+ public static final int TOTAL_MAP_TYPES = MAP_TYPES.length;
+
+ 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);
+ }
+
+ 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);
+ }
+
+ // 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));
+ players[NearbyMapType.SPAWN_RANGE.ordinal()].update(chunk.x, chunk.z, ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Moonrise - chunk tick iteration
+ }
+
+ 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 {
+
+ 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 long 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<>(EMPTY_PLAYERS_ARRAY)).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> {
+
+ private 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/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;
+
+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;
+
+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));
+ }
+
+ 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;
+ }
+
+ 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);
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ return counter.update(toX, toZ, distance);
+ }
+
+ private final class PositionCounter extends SingleUserAreaMap<T> {
+
+ public PositionCounter(final T parameter) {
+ super(parameter);
+ }
+
+ @Override
+ protected void addCallback(final T parameter, final int toX, final int toZ) {
+ PositionCountingAreaMap.this.positions.addTo(IntPairUtil.key(toX, toZ), 1);
+ }
+
+ @Override
+ 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/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..94689e0342cf95dbedec955d67c95fa07a219678
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
@@ -0,0 +1,248 @@
+package ca.spottedleaf.moonrise.common.misc;
+
+import ca.spottedleaf.concurrentutil.util.IntegerUtil;
+
+public abstract class SingleUserAreaMap<T> {
+
+ public 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;
+ }
+
+ public final T getParameter() {
+ return this.parameter;
+ }
+
+ public final int getLastChunkX() {
+ return this.lastChunkX;
+ }
+
+ public final int getLastChunkZ() {
+ return this.lastChunkZ;
+ }
+
+ 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;
+
+ 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/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java
new file mode 100644
index 0000000000000000000000000000000000000000..4123edddc556c47f3f8d83523c125fd2e46b30e2
--- /dev/null
+++ 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;
+
+public final class OptimizedSmallEnumSet<E extends Enum<E>> {
+
+ private final Class<E> enumClass;
+ private long backingSet;
+
+ public OptimizedSmallEnumSet(final Class<E> clazz) {
+ if (clazz == null) {
+ throw new IllegalArgumentException("Null class");
+ }
+ if (!clazz.isEnum()) {
+ throw new IllegalArgumentException("Class must be enum, not " + clazz.getCanonicalName());
+ }
+ this.enumClass = clazz;
+ }
+
+ public boolean addUnchecked(final E element) {
+ final int ordinal = element.ordinal();
+ final long key = 1L << ordinal;
+
+ final long prev = this.backingSet;
+ this.backingSet = prev | key;
+
+ return (prev & key) == 0;
+ }
+
+ public boolean removeUnchecked(final E element) {
+ final int ordinal = element.ordinal();
+ final long key = 1L << ordinal;
+
+ final long prev = this.backingSet;
+ this.backingSet = prev & ~key;
+
+ return (prev & key) != 0;
+ }
+
+ public void clear() {
+ this.backingSet = 0L;
+ }
+
+ public int size() {
+ return Long.bitCount(this.backingSet);
+ }
+
+ public void addAllUnchecked(final Collection<E> enums) {
+ for (final E element : enums) {
+ if (element == null) {
+ throw new NullPointerException("Null element");
+ }
+ this.backingSet |= (1L << element.ordinal());
+ }
+ }
+
+ public long getBackingSet() {
+ return this.backingSet;
+ }
+
+ public boolean hasCommonElements(final OptimizedSmallEnumSet<E> other) {
+ return (other.backingSet & this.backingSet) != 0;
+ }
+
+ public boolean hasElement(final E element) {
+ return (this.backingSet & (1L << element.ordinal())) != 0;
+ }
+}
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..da323a1105347d5cf4b946df10ded78a953236f2
--- /dev/null
+++ 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.mojang.logging.LogUtils;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import org.slf4j.Logger;
+import java.util.List;
+import java.util.function.Consumer;
+
+public final class ChunkSystem {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+ 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);
+
+ private static int getDistance(final ChunkStatus status) {
+ return FULL_CHUNK_STEP.getAccumulatedRadiusOf(status);
+ }
+
+ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
+ scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
+ }
+
+ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) {
+ level.chunkSource.mainThreadProcessor.execute(run);
+ }
+
+ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
+ final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority,
+ final Consumer<ChunkAccess> onComplete) {
+ if (gen) {
+ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ return;
+ }
+ scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
+ if (chunk == null) {
+ if (onComplete != null) {
+ onComplete.accept(null);
+ }
+ } else {
+ if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
+ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ } else {
+ if (onComplete != null) {
+ onComplete.accept(null);
+ }
+ }
+ }
+ });
+ }
+
+ 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 (!org.bukkit.Bukkit.isPrimaryThread()) {
+ scheduleChunkTask(level, chunkX, chunkZ, () -> {
+ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ }, priority);
+ return;
+ }
+
+ final int minLevel = 33 + getDistance(toStatus);
+ final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
+ 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);
+ }
+ level.chunkSource.runDistanceManagerUpdates();
+
+ final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
+ try {
+ if (onComplete != null) {
+ onComplete.accept(chunk);
+ }
+ } catch (final Throwable thr) {
+ LOGGER.error("Exception handling chunk load callback", thr);
+ com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
+ } finally {
+ if (addTicket) {
+ level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
+ level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+ }
+ }
+ };
+
+ final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (holder == null || holder.getTicketLevel() > minLevel) {
+ loadCallback.accept(null);
+ return;
+ }
+
+ 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 net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess> result, final Throwable thr) -> {
+ if (thr != null) {
+ loadCallback.accept(null);
+ return;
+ }
+ loadCallback.accept(result.orElse(null));
+ }, (final Runnable r) -> {
+ scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
+ });
+ }
+
+ public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
+ final FullChunkStatus toStatus, final boolean addTicket,
+ final PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) {
+ // This method goes unused until the chunk system rewrite
+ if (toStatus == FullChunkStatus.INACCESSIBLE) {
+ throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
+ }
+
+ if (!org.bukkit.Bukkit.isPrimaryThread()) {
+ scheduleChunkTask(level, chunkX, chunkZ, () -> {
+ scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ }, priority);
+ return;
+ }
+
+ final int minLevel = 33 - (toStatus.ordinal() - 1);
+ final int radius = toStatus.ordinal() - 1;
+ final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
+ 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);
+ }
+ level.chunkSource.runDistanceManagerUpdates();
+
+ final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> {
+ try {
+ if (onComplete != null) {
+ onComplete.accept(chunk);
+ }
+ } catch (final Throwable thr) {
+ LOGGER.error("Exception handling chunk load callback", thr);
+ com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
+ } finally {
+ if (addTicket) {
+ level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
+ level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+ }
+ }
+ };
+
+ final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (holder == null || holder.getTicketLevel() > minLevel) {
+ loadCallback.accept(null);
+ return;
+ }
+
+ final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk>> tickingState;
+ switch (toStatus) {
+ case FULL: {
+ tickingState = holder.getFullChunkFuture();
+ break;
+ }
+ case BLOCK_TICKING: {
+ tickingState = holder.getTickingChunkFuture();
+ break;
+ }
+ case ENTITY_TICKING: {
+ tickingState = holder.getEntityTickingChunkFuture();
+ break;
+ }
+ default: {
+ throw new IllegalStateException("Cannot reach here");
+ }
+ }
+
+ if (tickingState.isDone()) {
+ loadCallback.accept(tickingState.join().orElse(null));
+ return;
+ }
+
+ 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;
+ }
+ loadCallback.accept(result.orElse(null));
+ }, (final Runnable r) -> {
+ scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
+ });
+ }
+
+ public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
+ return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
+ }
+
+ public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
+ return new java.util.ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
+ }
+
+ public static int getVisibleChunkHolderCount(final ServerLevel level) {
+ return level.chunkSource.chunkMap.visibleChunkMap.size();
+ }
+
+ public static int getUpdatingChunkHolderCount(final ServerLevel level) {
+ return level.chunkSource.chunkMap.updatingChunkMap.size();
+ }
+
+ public static boolean hasAnyChunkHolders(final ServerLevel level) {
+ return getUpdatingChunkHolderCount(level) != 0;
+ }
+
+ public static boolean screenEntity(final ServerLevel level, final Entity entity) {
+ return true;
+ }
+
+ public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
+
+ }
+
+ public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
+
+ }
+
+ public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
+
+ }
+
+ public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
+
+ }
+
+ public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
+
+ }
+
+ public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
+
+ }
+
+ public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
+
+ }
+
+ public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
+
+ }
+
+ public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
+ return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ);
+ }
+
+ public static int getSendViewDistance(final ServerPlayer player) {
+ return getLoadViewDistance(player);
+ }
+
+ public static int getLoadViewDistance(final ServerPlayer player) {
+ final ServerLevel level = player.serverLevel();
+ if (level == null) {
+ return org.bukkit.Bukkit.getViewDistance();
+ }
+ return level.chunkSource.chunkMap.getPlayerViewDistance(player);
+ }
+
+ public static int getTickViewDistance(final ServerPlayer player) {
+ final ServerLevel level = player.serverLevel();
+ if (level == null) {
+ return org.bukkit.Bukkit.getSimulationDistance();
+ }
+ return level.chunkSource.chunkMap.distanceManager.simulationDistance;
+ }
+
+ private ChunkSystem() {}
+}
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..31b92bd48828cbea25b44a9f0f96886347aa1ae6
--- /dev/null
+++ 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 {
+
+ // the chunk keys are compatible with vanilla
+
+ public static long getChunkKey(final BlockPos pos) {
+ return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL);
+ }
+
+ public static long getChunkKey(final Entity entity) {
+ return ((Mth.lfloor(entity.getZ()) >> 4) << 32) | ((Mth.lfloor(entity.getX()) >> 4) & 0xFFFFFFFFL);
+ }
+
+ public static long getChunkKey(final ChunkPos pos) {
+ return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL);
+ }
+
+ public static long getChunkKey(final SectionPos pos) {
+ return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL);
+ }
+
+ public static long getChunkKey(final int x, final int z) {
+ return ((long)z << 32) | (x & 0xFFFFFFFFL);
+ }
+
+ public static int getChunkX(final long chunkKey) {
+ return (int)chunkKey;
+ }
+
+ public static int getChunkZ(final long chunkKey) {
+ return (int)(chunkKey >>> 32);
+ }
+
+ public static int getChunkCoordinate(final double blockCoordinate) {
+ return Mth.floor(blockCoordinate) >> 4;
+ }
+
+ // the section keys are compatible with vanilla's
+
+ static final int SECTION_X_BITS = 22;
+ static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1;
+ static final int SECTION_Y_BITS = 20;
+ static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1;
+ static final int SECTION_Z_BITS = 22;
+ static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1;
+ // format is y,z,x (in order of LSB to MSB)
+ static final int SECTION_Y_SHIFT = 0;
+ static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS;
+ static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS;
+ static final int SECTION_TO_BLOCK_SHIFT = 4;
+
+ public static long getChunkSectionKey(final int x, final int y, final int z) {
+ return ((x & SECTION_X_MASK) << SECTION_X_SHIFT)
+ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
+ | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
+ }
+
+ public static long getChunkSectionKey(final SectionPos pos) {
+ return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT)
+ | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT)
+ | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT);
+ }
+
+ public static long getChunkSectionKey(final ChunkPos pos, final int y) {
+ return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT)
+ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
+ | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
+ }
+
+ public static long getChunkSectionKey(final BlockPos pos) {
+ return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
+ ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
+ (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
+ }
+
+ public static long getChunkSectionKey(final Entity entity) {
+ return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
+ ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
+ ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
+ }
+
+ public static int getChunkSectionX(final long key) {
+ return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS));
+ }
+
+ public static int getChunkSectionY(final long key) {
+ return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS));
+ }
+
+ public static int getChunkSectionZ(final long key) {
+ return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS));
+ }
+
+ 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 getBlockZ(final Vec3 pos) {
+ return Mth.floor(pos.z);
+ }
+
+ public static int getChunkX(final Vec3 pos) {
+ return Mth.floor(pos.x) >> 4;
+ }
+
+ public static int getChunkY(final Vec3 pos) {
+ return Mth.floor(pos.y) >> 4;
+ }
+
+ public static int getChunkZ(final Vec3 pos) {
+ return Mth.floor(pos.z) >> 4;
+ }
+
+ private CoordinateUtils() {
+ throw new RuntimeException();
+ }
+}
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..0531f25aaad162386a029d33e68d7c8336b9d5d1
--- /dev/null
+++ 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 MoonriseCommon {
+
+ 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
+
+ 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;
+ }
+
+ 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..11b7f15755dde766140c29bedca456c80d53293f
--- /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.world.entity.Entity;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+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 void ensureTickThread(final Level 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 void ensureTickThread(final Level 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 void ensureTickThread(final Level 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 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 void ensureTickThread(final Level 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 void ensureTickThread(final Level 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 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 TickThread(final Runnable run, final String name) {
+ this(run, name, ID_GENERATOR.incrementAndGet());
+ }
+
+ private TickThread(final Runnable run, final String name, final int id) {
+ super(run, name);
+ this.id = id;
+ }
+
+ public static TickThread getCurrentTickThread() {
+ return (TickThread)Thread.currentThread();
+ }
+
+ public static boolean isTickThread() {
+ return Thread.currentThread() instanceof TickThread;
+ }
+
+ public static boolean isShutdownThread() {
+ return false;
+ }
+
+ public static boolean isTickThreadFor(final Level world, final BlockPos pos) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final ChunkPos pos) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final Vec3 pos) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final AABB aabb) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final double blockX, final double blockZ) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final Vec3 position, final Vec3 deltaMovement, final int buffer) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ, final int radius) {
+ return isTickThread();
+ }
+
+ 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;
+
+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;
+ }
+
+ 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..197224e31175252d8438a8df585bbb65f2288d7f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/IntervalledCounter.java
@@ -0,0 +1,129 @@
+package io.papermc.paper.util;
+
+public final class IntervalledCounter {
+
+ private static final int INITIAL_SIZE = 8;
+
+ protected long[] times;
+ protected long[] counts;
+ protected final long interval;
+ protected long minTime;
+ protected long sum;
+ protected int head; // inclusive
+ protected int tail; // exclusive
+
+ public IntervalledCounter(final long interval) {
+ this.times = new long[INITIAL_SIZE];
+ this.counts = new long[INITIAL_SIZE];
+ this.interval = interval;
+ }
+
+ public void updateCurrentTime() {
+ this.updateCurrentTime(System.nanoTime());
+ }
+
+ public void updateCurrentTime(final long currentTime) {
+ long sum = this.sum;
+ int head = this.head;
+ final int tail = this.tail;
+ final long minTime = currentTime - this.interval;
+
+ final int arrayLen = this.times.length;
+
+ // guard against overflow by using subtraction
+ while (head != tail && this.times[head] - minTime < 0) {
+ sum -= this.counts[head];
+ // there are two ways we can do this:
+ // 1. free the count when adding
+ // 2. free it now
+ // option #2
+ this.counts[head] = 0;
+ if (++head >= arrayLen) {
+ head = 0;
+ }
+ }
+
+ this.sum = sum;
+ this.head = head;
+ this.minTime = minTime;
+ }
+
+ public void addTime(final long currTime) {
+ this.addTime(currTime, 1L);
+ }
+
+ public void addTime(final long currTime, final long count) {
+ // guard against overflow by using subtraction
+ if (currTime - this.minTime < 0) {
+ return;
+ }
+ int nextTail = (this.tail + 1) % this.times.length;
+ if (nextTail == this.head) {
+ this.resize();
+ nextTail = (this.tail + 1) % this.times.length;
+ }
+
+ this.times[this.tail] = currTime;
+ this.counts[this.tail] += count;
+ this.sum += count;
+ this.tail = nextTail;
+ }
+
+ public void updateAndAdd(final long count) {
+ final long currTime = System.nanoTime();
+ this.updateCurrentTime(currTime);
+ this.addTime(currTime, count);
+ }
+
+ public void updateAndAdd(final long count, final long currTime) {
+ this.updateCurrentTime(currTime);
+ this.addTime(currTime, count);
+ }
+
+ private void resize() {
+ final long[] oldElements = this.times;
+ final long[] oldCounts = this.counts;
+ final long[] newElements = new long[this.times.length * 2];
+ final long[] newCounts = new long[this.times.length * 2];
+ this.times = newElements;
+ this.counts = newCounts;
+
+ final int head = this.head;
+ final int tail = this.tail;
+ final int size = tail >= head ? (tail - head) : (tail + (oldElements.length - head));
+ this.head = 0;
+ this.tail = size;
+
+ if (tail >= head) {
+ // sequentially ordered from [head, tail)
+ System.arraycopy(oldElements, head, newElements, 0, size);
+ System.arraycopy(oldCounts, head, newCounts, 0, size);
+ } 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);
+
+ System.arraycopy(oldCounts, head, newCounts, 0, oldCounts.length - head);
+ System.arraycopy(oldCounts, 0, newCounts, oldCounts.length - head, tail);
+ }
+ }
+
+ // returns in units per second
+ public double getRate() {
+ return (double)this.sum / ((double)this.interval * 1.0E-9);
+ }
+
+ public long getInterval() {
+ return this.interval;
+ }
+
+ public long getSum() {
+ return this.sum;
+ }
+
+ public int totalDataPoints() {
+ return this.tail >= this.head ? (this.tail - this.head) : (this.tail + (this.counts.length - this.head));
+ }
+}
diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..0449d4619e3a0752dea0981fb149542e23076c52
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/MCUtil.java
@@ -0,0 +1,176 @@
+package io.papermc.paper.util;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import io.papermc.paper.math.BlockPosition;
+import io.papermc.paper.math.FinePosition;
+import io.papermc.paper.math.Position;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Vec3i;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.phys.Vec3;
+import org.bukkit.Location;
+import org.bukkit.craftbukkit.util.Waitable;
+
+public final class MCUtil {
+ public static final java.util.concurrent.Executor MAIN_EXECUTOR = (run) -> {
+ if (!isMainThread()) {
+ MinecraftServer.getServer().execute(run);
+ } else {
+ run.run();
+ }
+ };
+ public static final ThreadPoolExecutor asyncExecutor = new ThreadPoolExecutor(
+ 2, 2, 60L, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(),
+ new ThreadFactoryBuilder()
+ .setNameFormat("Paper Async Task Handler Thread - %1$d")
+ .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(MinecraftServer.LOGGER))
+ .build()
+ );
+
+ private MCUtil() {
+ }
+
+ public static List<ChunkPos> getSpiralOutChunks(BlockPos blockposition, int radius) {
+ List<ChunkPos> list = com.google.common.collect.Lists.newArrayList();
+
+ list.add(new ChunkPos(blockposition.getX() >> 4, blockposition.getZ() >> 4));
+ for (int r = 1; r <= radius; r++) {
+ int x = -r;
+ int z = r;
+
+ // Iterates the edge of half of the box; then negates for other half.
+ while (x <= r && z > -r) {
+ list.add(new ChunkPos((blockposition.getX() + (x << 4)) >> 4, (blockposition.getZ() + (z << 4)) >> 4));
+ list.add(new ChunkPos((blockposition.getX() - (x << 4)) >> 4, (blockposition.getZ() - (z << 4)) >> 4));
+
+ if (x < r) {
+ x++;
+ } else {
+ z--;
+ }
+ }
+ }
+ return list;
+ }
+
+ public static <T> CompletableFuture<T> ensureMain(CompletableFuture<T> future) {
+ return future.thenApplyAsync(r -> r, MAIN_EXECUTOR);
+ }
+
+ public static <T> void thenOnMain(CompletableFuture<T> future, Consumer<T> consumer) {
+ future.thenAcceptAsync(consumer, MAIN_EXECUTOR);
+ }
+
+ public static <T> void thenOnMain(CompletableFuture<T> future, BiConsumer<T, Throwable> consumer) {
+ future.whenCompleteAsync(consumer, MAIN_EXECUTOR);
+ }
+
+ public static boolean isMainThread() {
+ return MinecraftServer.getServer().isSameThread();
+ }
+
+ public static void ensureMain(Runnable run) {
+ ensureMain(null, run);
+ }
+
+ /**
+ * Ensures the target code is running on the main thread.
+ */
+ public static void ensureMain(String reason, Runnable run) {
+ if (!isMainThread()) {
+ if (reason != null) {
+ MinecraftServer.LOGGER.warn("Asynchronous " + reason + "!", new IllegalStateException());
+ }
+ MinecraftServer.getServer().processQueue.add(run);
+ return;
+ }
+ run.run();
+ }
+
+ public static <T> T ensureMain(Supplier<T> run) {
+ return ensureMain(null, run);
+ }
+
+ /**
+ * Ensures the target code is running on the main thread.
+ */
+ public static <T> T ensureMain(String reason, Supplier<T> run) {
+ if (!isMainThread()) {
+ if (reason != null) {
+ MinecraftServer.LOGGER.warn("Asynchronous " + reason + "! Blocking thread until it returns ", new IllegalStateException());
+ }
+ Waitable<T> wait = new Waitable<>() {
+ @Override
+ protected T evaluate() {
+ return run.get();
+ }
+ };
+ MinecraftServer.getServer().processQueue.add(wait);
+ try {
+ return wait.get();
+ } catch (InterruptedException | ExecutionException e) {
+ MinecraftServer.LOGGER.warn("Encountered exception", e);
+ }
+ return null;
+ }
+ return run.get();
+ }
+
+ public static double distance(double x1, double y1, double z1, double x2, double y2, double z2) {
+ return Math.sqrt(distanceSq(x1, y1, z1, x2, y2, z2));
+ }
+
+ public static double distanceSq(double x1, double y1, double z1, double x2, double y2, double z2) {
+ return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2);
+ }
+
+ public static Location toLocation(Level world, double x, double y, double z) {
+ return new Location(world.getWorld(), x, y, z);
+ }
+
+ public static Location toLocation(Level world, BlockPos pos) {
+ return new Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ());
+ }
+
+ public static BlockPos toBlockPosition(Location loc) {
+ return new BlockPos(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
+ }
+
+ public static BlockPos toBlockPos(Position pos) {
+ return new BlockPos(pos.blockX(), pos.blockY(), pos.blockZ());
+ }
+
+ public static FinePosition toPosition(Vec3 vector) {
+ return Position.fine(vector.x, vector.y, vector.z);
+ }
+
+ public static BlockPosition toPosition(Vec3i vector) {
+ return Position.block(vector.getX(), vector.getY(), vector.getZ());
+ }
+
+ public static Vec3 toVec3(Position position) {
+ return new Vec3(position.x(), position.y(), position.z());
+ }
+
+ public static boolean isEdgeOfChunk(BlockPos pos) {
+ final int modX = pos.getX() & 15;
+ final int modZ = pos.getZ() & 15;
+ return (modX == 0 || modX == 15 || modZ == 0 || modZ == 15);
+ }
+
+ public static void scheduleAsyncTask(Runnable run) {
+ asyncExecutor.execute(run);
+ }
+}
diff --git a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..f7114d5b8f2f93f62883e24da29afaf9f74ee1a6
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
@@ -0,0 +1,24 @@
+package io.papermc.paper.util;
+
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.plugin.java.PluginClassLoader;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Optional;
+
+public class StackWalkerUtil {
+
+ @Nullable
+ public static JavaPlugin getFirstPluginCaller() {
+ Optional<JavaPlugin> foundFrame = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
+ .walk(stream -> stream
+ .filter(frame -> frame.getDeclaringClass().getClassLoader() instanceof PluginClassLoader)
+ .map((frame) -> {
+ PluginClassLoader classLoader = (PluginClassLoader) frame.getDeclaringClass().getClassLoader();
+ return classLoader.getPlugin();
+ })
+ .findFirst());
+
+ return foundFrame.orElse(null);
+ }
+}
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
+++ b/src/main/java/net/minecraft/Util.java
@@ -133,7 +133,7 @@ public class Util {
}
public static long getNanos() {
- return timeSource.getAsLong();
+ return System.nanoTime(); // Paper
}
public static long getEpochMillis() {
diff --git a/src/main/java/net/minecraft/nbt/CompoundTag.java b/src/main/java/net/minecraft/nbt/CompoundTag.java
index 6b588a4e639da11edeb933ec2bc4afde8f0b47f1..d721ae6d9b54cbace5b7ade657e9739fc7c42d14 100644
--- a/src/main/java/net/minecraft/nbt/CompoundTag.java
+++ b/src/main/java/net/minecraft/nbt/CompoundTag.java
@@ -235,6 +235,10 @@ public class CompoundTag implements Tag {
this.tags.put(key, NbtUtils.createUUID(value));
}
+
+ /**
+ * You must use {@link #hasUUID(String)} before or else it <b>will</b> throw an NPE.
+ */
public UUID getUUID(String key) {
return NbtUtils.loadUUID(this.get(key));
}
diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
index 821de8e17031bf54edb61fc577d7c5a90aabfd2e..77985072928a1b892fb4f7dec1d0899324780082 100644
--- a/src/main/java/net/minecraft/network/Connection.java
+++ b/src/main/java/net/minecraft/network/Connection.java
@@ -121,6 +121,18 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
BandwidthDebugMonitor bandwidthDebugMonitor;
public String hostname = ""; // CraftBukkit - add field
+ // Paper start - add utility methods
+ public final net.minecraft.server.level.ServerPlayer getPlayer() {
+ if (this.packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl impl) {
+ return impl.player;
+ } else if (this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl impl) {
+ org.bukkit.craftbukkit.entity.CraftPlayer player = impl.getCraftPlayer();
+ return player == null ? null : player.getHandle();
+ }
+ return null;
+ }
+ // Paper end - add utility methods
+
public Connection(PacketFlow side) {
this.receiving = side;
}
diff --git a/src/main/java/net/minecraft/network/PacketEncoder.java b/src/main/java/net/minecraft/network/PacketEncoder.java
index e82263b4b48c4544a5ade6613ea284f4de2b4c80..a58f67973b4ed986065860263c7a42214640520d 100644
--- a/src/main/java/net/minecraft/network/PacketEncoder.java
+++ b/src/main/java/net/minecraft/network/PacketEncoder.java
@@ -31,7 +31,7 @@ public class PacketEncoder<T extends PacketListener> extends MessageToByteEncode
JvmProfiler.INSTANCE.onPacketSent(this.protocolInfo.id(), packetType, channelHandlerContext.channel().remoteAddress(), i);
} catch (Throwable var9) {
- LOGGER.error("Error sending packet {}", packetType, var9);
+ LOGGER.error("Error sending packet {} (skippable? {})", packetType, packet.isSkippable(), var9);
if (packet.isSkippable()) {
throw new SkipPacketException(var9);
}
diff --git a/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java b/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java
index e113cd9d93750cf59712b06db62591876b4efbac..1789c0c71d968b386060bd6dc2630e8a078c32e2 100644
--- a/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java
+++ b/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java
@@ -47,4 +47,14 @@ public record ClientboundCustomQueryPacket(int transactionId, CustomQueryPayload
public void handle(ClientLoginPacketListener listener) {
listener.handleCustomQuery(this);
}
+
+ // Paper start - MC Utils - default query payloads
+ public static record PlayerInfoChannelPayload(ResourceLocation id, FriendlyByteBuf buffer) implements CustomQueryPayload {
+
+ @Override
+ public void write(final FriendlyByteBuf buf) {
+ buf.writeBytes(this.buffer.copy());
+ }
+ }
+ // Paper end - MC Utils - default query payloads
}
diff --git a/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java b/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java
index 3e5a85a7ad6149b04622c254fbc2e174896a4128..3f662692ed4846e026a9d48595e7b3b22404a031 100644
--- a/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java
+++ b/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java
@@ -20,7 +20,17 @@ public record ServerboundCustomQueryAnswerPacket(int transactionId, @Nullable Cu
}
private static CustomQueryAnswerPayload readPayload(int queryId, FriendlyByteBuf buf) {
- return readUnknownPayload(buf);
+ // Paper start - MC Utils - default query payloads
+ FriendlyByteBuf buffer = buf.readNullable((buf2) -> {
+ int i = buf2.readableBytes();
+ if (i >= 0 && i <= MAX_PAYLOAD_SIZE) {
+ return new FriendlyByteBuf(buf2.readBytes(i));
+ } else {
+ throw new IllegalArgumentException("Payload may not be larger than " + MAX_PAYLOAD_SIZE + " bytes");
+ }
+ });
+ return buffer == null ? null : new net.minecraft.network.protocol.login.ServerboundCustomQueryAnswerPacket.QueryAnswerPayload(buffer);
+ // Paper end - MC Utils - default query payloads
}
private static CustomQueryAnswerPayload readUnknownPayload(FriendlyByteBuf buf) {
@@ -47,4 +57,21 @@ public record ServerboundCustomQueryAnswerPacket(int transactionId, @Nullable Cu
public void handle(ServerLoginPacketListener listener) {
listener.handleCustomQueryPacket(this);
}
+
+ // Paper start - MC Utils - default query payloads
+ public static final class QueryAnswerPayload implements CustomQueryAnswerPayload {
+
+ public final FriendlyByteBuf buffer;
+
+ public QueryAnswerPayload(final net.minecraft.network.FriendlyByteBuf buffer) {
+ this.buffer = buffer;
+ }
+
+ @Override
+ public void write(final net.minecraft.network.FriendlyByteBuf buf) {
+ buf.writeBytes(this.buffer.copy());
+ }
+ }
+ // Paper end - MC Utils - default query payloads
+
}
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 40adb6117b9e0d5f70103113202a07715e403e2a..c9cab509796599b13ca3422de1049aed78348704 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -973,6 +973,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
MinecraftServer.LOGGER.error("Failed to unlock level {}", this.storageSource.getLevelId(), ioexception1);
}
// Spigot start
+ io.papermc.paper.util.MCUtil.asyncExecutor.shutdown(); // Paper
+ try { io.papermc.paper.util.MCUtil.asyncExecutor.awaitTermination(30, java.util.concurrent.TimeUnit.SECONDS); // Paper
+ } catch (java.lang.InterruptedException ignored) {} // Paper
if (org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) {
MinecraftServer.LOGGER.info("Saving usercache.json");
this.getProfileCache().save();
@@ -1337,7 +1340,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
MinecraftServer.LOGGER.debug("Autosave finished");
SpigotTimings.worldSaveTimer.stopTiming(); // Spigot
}
-
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..9a009a688c02e990723917766c51e1c0e71e338d 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 {
public static final ChunkResult<LevelChunk> UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk");
private static final CompletableFuture<ChunkResult<LevelChunk>> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK);
private final LevelHeightAccessor levelHeightAccessor;
- private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture;
- private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture;
- private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture;
+ private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
+ private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
+ private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
public int oldTicketLevel;
private int ticketLevel;
private int queueLevel;
@@ -101,7 +101,7 @@ public class ChunkHolder extends GenerationChunkHolder {
}
@Nullable
- public LevelChunk getTickingChunk() {
+ public final LevelChunk getTickingChunk() { // Paper - final for inline
return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error
}
@@ -337,12 +337,28 @@ public class ChunkHolder extends GenerationChunkHolder {
this.wasAccessibleSinceLastSave |= flag1;
if (!flag && flag1) {
+ int expectCreateCount = ++this.fullChunkCreateCount; // Paper
this.fullChunkFuture = chunkLoadingManager.prepareAccessibleChunk(this);
this.scheduleFullChunkPromotion(chunkLoadingManager, this.fullChunkFuture, executor, FullChunkStatus.FULL);
+ // Paper start - cache ticking ready status
+ this.fullChunkFuture.thenAccept(chunkResult -> {
+ chunkResult.ifSuccess(chunk -> {
+ if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
+ ChunkHolder.this.isFullChunkReady = true;
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkBorder(chunk, this);
+ }
+ });
+ });
+ // Paper end - cache ticking ready status
this.addSaveDependency(this.fullChunkFuture);
}
if (flag && !flag1) {
+ // Paper start
+ if (this.isFullChunkReady) {
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
+ }
+ // Paper end
this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
}
@@ -353,11 +369,25 @@ public class ChunkHolder extends GenerationChunkHolder {
if (!flag2 && flag3) {
this.tickingChunkFuture = chunkLoadingManager.prepareTickingChunk(this);
this.scheduleFullChunkPromotion(chunkLoadingManager, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING);
+ // Paper start - cache ticking ready status
+ this.tickingChunkFuture.thenAccept(chunkResult -> {
+ chunkResult.ifSuccess(chunk -> {
+ // note: Here is a very good place to add callbacks to logic waiting on this.
+ ChunkHolder.this.isTickingReady = true;
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkTicking(chunk, this);
+ });
+ });
+ // Paper end
this.addSaveDependency(this.tickingChunkFuture);
}
if (flag2 && !flag3) {
- this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
+ // Paper start
+ if (this.isTickingReady) {
+ 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
this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
}
@@ -371,11 +401,24 @@ public class ChunkHolder extends GenerationChunkHolder {
this.entityTickingChunkFuture = chunkLoadingManager.prepareEntityTickingChunk(this);
this.scheduleFullChunkPromotion(chunkLoadingManager, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING);
+ // Paper start - cache ticking ready status
+ this.entityTickingChunkFuture.thenAccept(chunkResult -> {
+ chunkResult.ifSuccess(chunk -> {
+ ChunkHolder.this.isEntityTickingReady = true;
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkEntityTicking(chunk, this);
+ });
+ });
+ // Paper end
this.addSaveDependency(this.entityTickingChunkFuture);
}
if (flag4 && !flag5) {
- this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
+ // Paper start
+ if (this.isEntityTickingReady) {
+ 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
this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
}
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index 5b920beb39dad8d392b4e5e12a89880720e41942..449608e60f3900778247101581ff598f1637499b 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -170,6 +170,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
};
// CraftBukkit end
+ // Paper start
+ public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
+ return this.pendingUnloads.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+ // 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();
@@ -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
+ public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
+ return -1;
+ }
+ // Paper end
+
protected ChunkGenerator generator() {
return this.worldGenContext.generator();
}
@@ -378,9 +390,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
};
stringbuilder.append("Updating:").append(System.lineSeparator());
- this.updatingChunkMap.values().forEach(consumer);
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
stringbuilder.append("Visible:").append(System.lineSeparator());
- this.visibleChunkMap.values().forEach(consumer);
+ 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,6 +434,9 @@ 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
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderCreate(this.level, holder);
+ // Paper end
}
this.updatingChunkMap.put(pos, holder);
@@ -445,7 +460,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 = 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 +483,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
});
this.flushWorker();
} else {
- this.visibleChunkMap.values().forEach(this::saveChunkIfNeeded);
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded);
}
}
@@ -487,7 +502,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() || 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) {
@@ -523,7 +538,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
int l = 0;
- ObjectIterator<ChunkHolder> objectiterator = this.visibleChunkMap.values().iterator();
+ 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 +556,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
} else {
ChunkAccess ichunkaccess = holder.getLatestChunk();
- if (this.pendingUnloads.remove(pos, holder) && ichunkaccess != null) {
+ // Paper start
+ boolean removed;
+ if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) {
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, holder);
+ // Paper end
LevelChunk chunk;
if (ichunkaccess instanceof LevelChunk) {
@@ -559,7 +578,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
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, holder);
+ } // Paper end
}
};
@@ -896,7 +917,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
}
- protected void setServerViewDistance(int watchDistance) {
+ public void setServerViewDistance(int watchDistance) { // Paper - public
int j = Mth.clamp(watchDistance, 2, 32);
if (j != this.serverViewDistance) {
@@ -913,7 +934,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
- int getPlayerViewDistance(ServerPlayer player) {
+ public int getPlayerViewDistance(ServerPlayer player) { // Paper - public
return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance);
}
@@ -942,7 +963,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public int size() {
- return this.visibleChunkMap.size();
+ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper
}
public DistanceManager getDistanceManager() {
@@ -950,19 +971,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
protected Iterable<ChunkHolder> getChunks() {
- return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
+ 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 = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
while (objectbidirectionaliterator.hasNext()) {
- Entry<ChunkHolder> entry = (Entry) objectbidirectionaliterator.next();
- long i = entry.getLongKey();
+ ChunkHolder playerchunk = objectbidirectionaliterator.next(); // Paper
+ long i = playerchunk.pos.toLong(); // Paper
ChunkPos chunkcoordintpair = new ChunkPos(i);
- ChunkHolder playerchunk = (ChunkHolder) entry.getValue();
+ // Paper - move up
Optional<ChunkAccess> optional = Optional.ofNullable(playerchunk.getLatestChunk());
Optional<LevelChunk> optional1 = optional.flatMap((ichunkaccess) -> {
return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty();
@@ -1385,7 +1406,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
});
}
- private class ChunkDistanceManager extends DistanceManager {
+ public class ChunkDistanceManager extends DistanceManager { // Paper - public
protected ChunkDistanceManager(final Executor workerExecutor, final Executor mainThreadExecutor) {
super(workerExecutor, mainThreadExecutor);
diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
index b6cc33943fe7e4667944f3e6f868b3033ea9ca18..3d46412b307f08968bb9b96c0649e0405813462e 100644
--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
@@ -365,7 +365,7 @@ public abstract class DistanceManager {
}
public void removeTicketsOnClosing() {
- ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT);
+ ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve
ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
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..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;
public class ServerChunkCache extends ChunkSource {
+ public static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); // Paper
private static final List<ChunkStatus> CHUNK_STATUSES = ChunkStatus.getStatusList();
private final DistanceManager distanceManager;
final ServerLevel level;
@@ -66,6 +67,10 @@ public class ServerChunkCache extends ChunkSource {
@Nullable
@VisibleForDebug
private NaturalSpawner.SpawnState lastSpawnState;
+ // Paper start
+ 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 +96,54 @@ public class ServerChunkCache extends ChunkSource {
return chunk.getFullChunkNow() != null;
}
// CraftBukkit end
+ // Paper start
+ public void addLoadedChunk(LevelChunk chunk) {
+ this.fullChunks.put(chunk.coordinateKey, chunk);
+ }
+
+ public void removeLoadedChunk(LevelChunk chunk) {
+ this.fullChunks.remove(chunk.coordinateKey);
+ }
+
+ @Nullable
+ public ChunkAccess getChunkAtImmediately(int x, int z) {
+ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
+ if (holder == null) {
+ return null;
+ }
+
+ return holder.getLatestChunk();
+ }
+
+ public <T> void addTicketAtLevel(TicketType<T> ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) {
+ this.distanceManager.addTicket(ticketType, chunkPos, ticketLevel, identifier);
+ }
+
+ public <T> void removeTicketAtLevel(TicketType<T> ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) {
+ this.distanceManager.removeTicket(ticketType, chunkPos, ticketLevel, identifier);
+ }
+
+ // "real" get chunk if loaded
+ // Note: Partially copied from the getChunkAt method below
+ @Nullable
+ public LevelChunk getChunkAtIfCachedImmediately(int x, int z) {
+ long k = ChunkPos.asLong(x, z);
+
+ // Note: Bypass cache since we need to check ticket level, and to make this MT-Safe
+
+ ChunkHolder playerChunk = this.getVisibleChunkIfPresent(k);
+ if (playerChunk == null) {
+ return null;
+ }
+
+ return playerChunk.getFullChunkNowUnchecked();
+ }
+
+ @Nullable
+ public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) {
+ return this.fullChunks.get(ChunkPos.asLong(x, z));
+ }
+ // Paper end
@Override
public ThreadedLevelLightEngine getLightEngine() {
@@ -286,7 +339,7 @@ public class ServerChunkCache extends ChunkSource {
return this.mainThreadProcessor.pollTask();
}
- boolean runDistanceManagerUpdates() {
+ public boolean runDistanceManagerUpdates() { // Paper - public
boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
boolean flag1 = this.chunkMap.promoteChunkMap();
@@ -299,6 +352,12 @@ public class ServerChunkCache extends ChunkSource {
}
}
+ // Paper start
+ public boolean isPositionTicking(Entity entity) {
+ return this.isPositionTicking(ChunkPos.asLong(net.minecraft.util.Mth.floor(entity.getX()) >> 4, net.minecraft.util.Mth.floor(entity.getZ()) >> 4));
+ }
+ // Paper end
+
public boolean isPositionTicking(long pos) {
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..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 {
return this.convertable.dimensionType;
}
+ // Paper start
+ public final boolean areChunksLoadedForMove(AABB axisalignedbb) {
+ // copied code from collision methods, so that we can guarantee that they wont load chunks (we don't override
+ // ICollisionAccess methods for VoxelShapes)
+ // be more strict too, add a block (dumb plugins in move events?)
+ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
+ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
+
+ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
+ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
+
+ int minChunkX = minBlockX >> 4;
+ int maxChunkX = maxBlockX >> 4;
+
+ int minChunkZ = minBlockZ >> 4;
+ int maxChunkZ = maxBlockZ >> 4;
+
+ ServerChunkCache chunkProvider = this.getChunkSource();
+
+ for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
+ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
+ if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
+ java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+ if (Thread.currentThread() != this.thread) {
+ this.getChunkSource().mainThreadProcessor.execute(() -> {
+ this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad);
+ });
+ return;
+ }
+ List<net.minecraft.world.level.chunk.ChunkAccess> ret = new java.util.ArrayList<>();
+ it.unimi.dsi.fastutil.ints.IntArrayList ticketLevels = new it.unimi.dsi.fastutil.ints.IntArrayList();
+
+ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
+ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
+
+ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
+ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
+
+ int minChunkX = minBlockX >> 4;
+ int maxChunkX = maxBlockX >> 4;
+
+ int minChunkZ = minBlockZ >> 4;
+ int maxChunkZ = maxBlockZ >> 4;
+
+ ServerChunkCache chunkProvider = this.getChunkSource();
+
+ int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
+ int[] loadedChunks = new int[1];
+
+ Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++);
+
+ java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> {
+ if (chunk != null) {
+ int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel());
+ ret.add(chunk);
+ ticketLevels.add(ticketLevel);
+ chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier);
+ }
+ if (++loadedChunks[0] == requiredChunks) {
+ try {
+ onLoad.accept(java.util.Collections.unmodifiableList(ret));
+ } finally {
+ for (int i = 0, len = ret.size(); i < len; ++i) {
+ ChunkPos chunkPos = ret.get(i).getPos();
+ int ticketLevel = ticketLevels.getInt(i);
+
+ chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos);
+ chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier);
+ }
+ }
+ }
+ };
+
+ for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
+ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
+ this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer
+ );
+ }
+ }
+ }
+ // Paper end
+
// Add env and gen to constructor, IWorldDataServer -> WorldDataServer
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 ff1a8e62d2bb3de62e0c27b2335cb512ea91dedd..cb136a30287a17947ed018cdc48e6f91ed904072 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -281,6 +281,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 ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);
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
+++ b/src/main/java/net/minecraft/server/level/TicketType.java
@@ -7,6 +7,7 @@ import net.minecraft.util.Unit;
import net.minecraft.world.level.ChunkPos;
public class TicketType<T> {
+ public static final TicketType<Long> FUTURE_AWAIT = create("future_await", Long::compareTo); // Paper
private final String name;
private final Comparator<T> comparator;
diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
index 7d69da7e761ccfe736656e8c89dd1ae08956695f..421f146ea9c35b852251c0ddb29856c13e11aef3 100644
--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java
+++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
@@ -169,6 +169,26 @@ public class WorldGenRegion implements WorldGenLevel {
return k < this.generatingStep.directDependencies().size();
}
+ // Paper start - if loaded util
+ @Nullable
+ @Override
+ public ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
+ return this.getChunk(x, z, ChunkStatus.FULL, false);
+ }
+
+ @Override
+ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
+ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+ return chunk == null ? null : chunk.getBlockState(blockposition);
+ }
+
+ @Override
+ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+ return chunk == null ? null : chunk.getFluidState(blockposition);
+ }
+ // Paper end
+
@Override
public BlockState getBlockState(BlockPos pos) {
return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())).getBlockState(pos);
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
index 0914b2f9fef1f49df9f0ce7c85cdde94c2c39b6c..6abe921099ff00ecfaf0f423ef27d708420f6f48 100644
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
@@ -180,6 +180,7 @@ public abstract class PlayerList {
}
public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData) {
+ player.isRealPlayer = true; // Paper
GameProfile gameprofile = player.getGameProfile();
GameProfileCache usercache = this.server.getProfileCache();
// Optional optional; // CraftBukkit - decompile error
diff --git a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
index aede9b65e799a1f123f71f9390fb05acddda676b..ca94a1aaccdcc9f28b5f7936b871216a75ab762a 100644
--- a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
+++ b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
@@ -79,6 +79,13 @@ public abstract class BlockableEventLoop<R extends Runnable> implements Profiler
runnable.run();
}
}
+ // Paper start
+ public void scheduleOnMain(Runnable runnable) {
+ // postToMainThread does not work the same as older versions of mc
+ // This method is actually used to create a TickTask, which can then be posted onto main
+ this.tell(this.wrapRunnable(runnable));
+ }
+ // Paper end
@Override
public void tell(R runnable) {
diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
index 05a4056b242159b1c85aa6ebf43b69cf85c00021..06cbe7a7ea131a8bead857cbfbd27810a9093320 100644
--- a/src/main/java/net/minecraft/world/entity/Entity.java
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
@@ -339,6 +339,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
return this.level.hasChunk((int) Math.floor(this.getX()) >> 4, (int) Math.floor(this.getZ()) >> 4);
}
// CraftBukkit end
+ // Paper start
+ public final AABB getBoundingBoxAt(double x, double y, double z) {
+ return this.dimensions.makeBoundingBox(x, y, z);
+ }
+ // Paper end
public Entity(EntityType<?> type, Level world) {
this.id = Entity.ENTITY_COUNTER.incrementAndGet();
diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
index c1db114edd9e31273b76374cbd19710b01cada2b..26064174397dc95f9b117d901e22c55abebf3c39 100644
--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
@@ -282,6 +282,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
public boolean collides = true;
public Set<UUID> collidableExemptions = new HashSet<>();
public boolean bukkitPickUpLoot;
+ public org.bukkit.craftbukkit.entity.CraftLivingEntity getBukkitLivingEntity() { return (org.bukkit.craftbukkit.entity.CraftLivingEntity) super.getBukkitEntity(); } // Paper
@Override
public float getBukkitYaw() {
diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java
index 5fa5c6fa8c143e1aa1dacc7a6b63b11ad4b074ec..5025f7f5d66f2aedd57db137ae2dc69c723768b3 100644
--- a/src/main/java/net/minecraft/world/item/ItemStack.java
+++ b/src/main/java/net/minecraft/world/item/ItemStack.java
@@ -953,6 +953,25 @@ public final class ItemStack implements DataComponentHolder {
}
}
+ // Paper start - (this is just a good no conflict location)
+ public org.bukkit.inventory.ItemStack asBukkitMirror() {
+ return CraftItemStack.asCraftMirror(this);
+ }
+ public org.bukkit.inventory.ItemStack asBukkitCopy() {
+ return CraftItemStack.asCraftMirror(this.copy());
+ }
+ public static ItemStack fromBukkitCopy(org.bukkit.inventory.ItemStack itemstack) {
+ return CraftItemStack.asNMSCopy(itemstack);
+ }
+ private org.bukkit.craftbukkit.inventory.CraftItemStack bukkitStack;
+ public org.bukkit.inventory.ItemStack getBukkitStack() {
+ if (bukkitStack == null || bukkitStack.handle != this) {
+ bukkitStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this);
+ }
+ return bukkitStack;
+ }
+ // Paper end
+
public void applyComponents(DataComponentPatch changes) {
this.components.applyPatch(changes);
this.getItem().verifyComponentsAfterLoad(this);
@@ -1219,6 +1238,7 @@ public final class ItemStack implements DataComponentHolder {
// CraftBukkit start
@Deprecated
public void setItem(Item item) {
+ this.bukkitStack = null; // Paper
this.item = item;
}
// CraftBukkit end
diff --git a/src/main/java/net/minecraft/world/level/BlockGetter.java b/src/main/java/net/minecraft/world/level/BlockGetter.java
index 1c71d2c1b16bdba1e14a8230787e4cb4ad530163..d6d8bbc98fc71997cb52521d59ebb59d727d3c22 100644
--- a/src/main/java/net/minecraft/world/level/BlockGetter.java
+++ b/src/main/java/net/minecraft/world/level/BlockGetter.java
@@ -9,6 +9,7 @@ import javax.annotation.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.Mth;
+import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
@@ -30,6 +31,15 @@ public interface BlockGetter extends LevelHeightAccessor {
}
BlockState getBlockState(BlockPos pos);
+ // Paper start - if loaded util
+ @Nullable BlockState getBlockStateIfLoaded(BlockPos blockposition);
+
+ default @Nullable Block getBlockIfLoaded(BlockPos blockposition) {
+ BlockState type = this.getBlockStateIfLoaded(blockposition);
+ return type == null ? null : type.getBlock();
+ }
+ @Nullable FluidState getFluidIfLoaded(BlockPos blockposition);
+ // Paper end
FluidState getFluidState(BlockPos pos);
diff --git a/src/main/java/net/minecraft/world/level/ChunkPos.java b/src/main/java/net/minecraft/world/level/ChunkPos.java
index 171c9c4ab2d1a7988935e09b49286f30e36741e2..fa58eeec2b652f0fa251eedf11cfabde5fd3198b 100644
--- a/src/main/java/net/minecraft/world/level/ChunkPos.java
+++ b/src/main/java/net/minecraft/world/level/ChunkPos.java
@@ -20,6 +20,7 @@ public class ChunkPos {
public static final int REGION_MAX_INDEX = 31;
public final int x;
public final int z;
+ public final long longKey; // Paper
private static final int HASH_A = 1664525;
private static final int HASH_C = 1013904223;
private static final int HASH_Z_XOR = -559038737;
@@ -27,16 +28,19 @@ public class ChunkPos {
public ChunkPos(int x, int z) {
this.x = x;
this.z = z;
+ this.longKey = asLong(this.x, this.z); // Paper
}
public ChunkPos(BlockPos pos) {
this.x = SectionPos.blockToSectionCoord(pos.getX());
this.z = SectionPos.blockToSectionCoord(pos.getZ());
+ this.longKey = asLong(this.x, this.z); // Paper
}
public ChunkPos(long pos) {
this.x = (int)pos;
this.z = (int)(pos >> 32);
+ this.longKey = asLong(this.x, this.z); // Paper
}
public static ChunkPos minFromRegion(int x, int z) {
@@ -48,7 +52,7 @@ public class ChunkPos {
}
public long toLong() {
- return asLong(this.x, this.z);
+ return longKey; // Paper
}
public static long asLong(int chunkX, int chunkZ) {
diff --git a/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java b/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java
index 3c707d6674b2594b09503b959a31c1f4ad3981e6..db61b6b0158a9bcc0e1d735e34fe3671f8c89e21 100644
--- a/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java
+++ b/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java
@@ -17,6 +17,18 @@ public enum EmptyBlockGetter implements BlockGetter {
return null;
}
+ // Paper start - If loaded util
+ @Override
+ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+ return Fluids.EMPTY.defaultFluidState();
+ }
+
+ @Override
+ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
+ return Blocks.AIR.defaultBlockState();
+ }
+ // Paper end
+
@Override
public BlockState getBlockState(BlockPos pos) {
return Blocks.AIR.defaultBlockState();
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
index c061813d275fbc48d7629cc59d90dbb4c347516c..2bc1d0d3ea8a6e3327e9c11bd1f0666d210e9bbe 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
@@ -95,6 +95,7 @@ import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.SpigotTimings; // Spigot
import org.bukkit.craftbukkit.block.CapturedBlockState;
+import org.bukkit.craftbukkit.block.CraftBlockState;
import org.bukkit.craftbukkit.block.data.CraftBlockData;
import org.bukkit.craftbukkit.util.CraftSpawnCategory;
import org.bukkit.entity.SpawnCategory;
@@ -274,6 +275,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
return null;
}
+ // Paper start
+ public net.minecraft.world.phys.BlockHitResult.Type clipDirect(Vec3 start, Vec3 end, net.minecraft.world.phys.shapes.CollisionContext context) {
+ // To be patched over
+ return this.clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, context)).getType();
+ }
+ // Paper end
+
public boolean isInWorldBounds(BlockPos pos) {
return !this.isOutsideBuildHeight(pos) && Level.isInWorldBoundsHorizontal(pos);
}
@@ -290,18 +298,52 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
return y < -20000000 || y >= 20000000;
}
- public LevelChunk getChunkAt(BlockPos pos) {
+ public final LevelChunk getChunkAt(BlockPos pos) { // Paper - help inline
return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
}
@Override
- public LevelChunk getChunk(int chunkX, int chunkZ) {
- return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL);
+ public final LevelChunk getChunk(int chunkX, int chunkZ) { // Paper - final to help inline
+ return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); // Paper - avoid a method jump
}
+ // Paper start - if loaded
+ @Nullable
+ @Override
+ public final ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
+ return ((ServerLevel)this).chunkSource.getChunkAtIfLoadedImmediately(x, z);
+ }
+
+ @Override
@Nullable
+ public final BlockState getBlockStateIfLoaded(BlockPos pos) {
+ // CraftBukkit start - tree generation
+ if (this.captureTreeGeneration) {
+ CraftBlockState previous = this.capturedBlockStates.get(pos);
+ if (previous != null) {
+ return previous.getHandle();
+ }
+ }
+ // CraftBukkit end
+ if (this.isOutsideBuildHeight(pos)) {
+ return Blocks.VOID_AIR.defaultBlockState();
+ } else {
+ ChunkAccess chunk = this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);
+
+ return chunk == null ? null : chunk.getBlockState(pos);
+ }
+ }
+
+ @Override
+ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+
+ return chunk == null ? null : chunk.getFluidState(blockposition);
+ }
+
@Override
public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
+ // Paper end
ChunkAccess ichunkaccess = this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, create);
if (ichunkaccess == null && create) {
@@ -551,7 +593,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
if (this.isOutsideBuildHeight(pos)) {
return Blocks.VOID_AIR.defaultBlockState();
} else {
- LevelChunk chunk = this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
+ ChunkAccess chunk = this.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.FULL, true); // Paper - manually inline to reduce hops and avoid unnecessary null check to reduce total byte code size, this should never return null and if it does we will see it the next line but the real stack trace will matter in the chunk engine
return chunk.getBlockState(pos);
}
diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java
index 749e4ea1be56b393877b5fdd72dc3669dbf5a3dd..a0ae26d6197e1069ca09982b4f8b706c55ae8491 100644
--- a/src/main/java/net/minecraft/world/level/LevelReader.java
+++ b/src/main/java/net/minecraft/world/level/LevelReader.java
@@ -26,6 +26,9 @@ public interface LevelReader extends BlockAndTintGetter, CollisionGetter, Signal
@Nullable
ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create);
+ @Nullable ChunkAccess getChunkIfLoadedImmediately(int x, int z); // Paper - ifLoaded api (we need this since current impl blocks if the chunk is loading)
+ @Nullable default ChunkAccess getChunkIfLoadedImmediately(BlockPos pos) { return this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);}
+
@Deprecated
boolean hasChunk(int chunkX, int chunkZ);
diff --git a/src/main/java/net/minecraft/world/level/PathNavigationRegion.java b/src/main/java/net/minecraft/world/level/PathNavigationRegion.java
index 497792978bdf0e6a53d772304770e8df3e7416ea..c5454b92ca2565461c799d7340160f9fb72c1b0f 100644
--- a/src/main/java/net/minecraft/world/level/PathNavigationRegion.java
+++ b/src/main/java/net/minecraft/world/level/PathNavigationRegion.java
@@ -9,6 +9,7 @@ import net.minecraft.core.Holder;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.util.profiling.ProfilerFiller;
+import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.Biomes;
@@ -67,7 +68,7 @@ public class PathNavigationRegion implements BlockGetter, CollisionGetter {
private ChunkAccess getChunk(int chunkX, int chunkZ) {
int i = chunkX - this.centerX;
int j = chunkZ - this.centerZ;
- if (i >= 0 && i < this.chunks.length && j >= 0 && j < this.chunks[i].length) {
+ if (i >= 0 && i < this.chunks.length && j >= 0 && j < this.chunks[i].length) { // Paper - if this changes, update getChunkIfLoaded below
ChunkAccess chunkAccess = this.chunks[i][j];
return (ChunkAccess)(chunkAccess != null ? chunkAccess : new EmptyLevelChunk(this.level, new ChunkPos(chunkX, chunkZ), this.plains.get()));
} else {
@@ -75,6 +76,30 @@ public class PathNavigationRegion implements BlockGetter, CollisionGetter {
}
}
+ // Paper start - if loaded util
+ private @Nullable ChunkAccess getChunkIfLoaded(int x, int z) {
+ // Based on getChunk(int, int)
+ int xx = x - this.centerX;
+ int zz = z - this.centerZ;
+
+ if (xx >= 0 && xx < this.chunks.length && zz >= 0 && zz < this.chunks[xx].length) {
+ return this.chunks[xx][zz];
+ }
+ return null;
+ }
+ @Override
+ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+ ChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+ return chunk == null ? null : chunk.getFluidState(blockposition);
+ }
+
+ @Override
+ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
+ ChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+ return chunk == null ? null : chunk.getBlockState(blockposition);
+ }
+ // Paper end
+
@Override
public WorldBorder getWorldBorder() {
return this.level.getWorldBorder();
diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
index e0594a1c381487b43bfc55212044e1b3122cee66..59fcaca90b67c03e1a6799e58061dbae3b1f1ceb 100644
--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
@@ -841,12 +841,14 @@ public abstract class BlockBehaviour implements FeatureElement {
}
}
+ protected boolean shapeExceedsCube = true; // Paper - moved from actual method to here
public void initCache() {
this.fluidState = ((Block) this.owner).getFluidState(this.asState());
this.isRandomlyTicking = ((Block) this.owner).isRandomlyTicking(this.asState());
if (!this.getBlock().hasDynamicShape()) {
this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState());
}
+ this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here
this.legacySolid = this.calculateSolid();
}
@@ -893,8 +895,8 @@ public abstract class BlockBehaviour implements FeatureElement {
return this.getBlock().getOcclusionShape(this.asState(), world, pos);
}
- public boolean hasLargeCollisionShape() {
- return this.cache == null || this.cache.largeCollisionShape;
+ public final boolean hasLargeCollisionShape() { // Paper
+ return this.shapeExceedsCube; // Paper - moved into shape cache init
}
public boolean useShapeForLightOcclusion() {
diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
index 35c4bf87870c0dfa1f648547115238dacbb87426..db4d95ce98eb1490d5306d1f74b282d27264871a 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
@@ -65,7 +65,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
protected final ShortList[] postProcessing;
protected volatile boolean unsaved;
private volatile boolean isLightCorrect;
- protected final ChunkPos chunkPos;
+ protected final ChunkPos chunkPos; public final long coordinateKey; public final int locX; public final int locZ; // Paper - cache coordinate key
private long inhabitedTime;
/** @deprecated */
@Nullable
@@ -91,7 +91,8 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
// CraftBukkit end
public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry<Biome> biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) {
- this.chunkPos = pos;
+ this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups
+ this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key
this.upgradeData = upgradeData;
this.levelHeightAccessor = heightLimitView;
this.sections = new LevelChunkSection[heightLimitView.getSectionsCount()];
diff --git a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
index a52077f0d93c94b0ea644bc14b9b28e84fd1b154..dcc0acd259920463a4464213b9a5e793603852f9 100644
--- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
@@ -25,6 +25,12 @@ public class EmptyLevelChunk extends LevelChunk {
public BlockState getBlockState(BlockPos pos) {
return Blocks.VOID_AIR.defaultBlockState();
}
+ // Paper start
+ @Override
+ public BlockState getBlockState(final int x, final int y, final int z) {
+ return Blocks.VOID_AIR.defaultBlockState();
+ }
+ // Paper end
@Nullable
@Override
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
index 5b73fcfe278f57de249f3a96da58dc08eda7aff6..25380a44e5cc94f3924cfee6a03c3091fea04ae2 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
@@ -116,6 +116,10 @@ public class LevelChunk extends ChunkAccess {
public boolean needsDecoration;
// CraftBukkit end
+ // Paper start
+ boolean loadedTicketLevel;
+ // Paper end
+
public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) {
this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData());
Iterator iterator = protoChunk.getBlockEntities().values().iterator();
@@ -181,8 +185,25 @@ public class LevelChunk extends ChunkAccess {
}
}
+ // Paper start - Perf: Reduce instructions and provide final method
+ public BlockState getBlockState(final int x, final int y, final int z) {
+ return this.getBlockStateFinal(x, y, z);
+ }
+ public BlockState getBlockStateFinal(final int x, final int y, final int z) {
+ // Copied and modified from below
+ final int sectionIndex = this.getSectionIndex(y);
+ if (sectionIndex < 0 || sectionIndex >= this.sections.length
+ || this.sections[sectionIndex].nonEmptyBlockCount == 0) {
+ return Blocks.AIR.defaultBlockState();
+ }
+ return this.sections[sectionIndex].states.get((y & 15) << 8 | (z & 15) << 4 | x & 15);
+ }
@Override
public BlockState getBlockState(BlockPos pos) {
+ if (true) {
+ return this.getBlockStateFinal(pos.getX(), pos.getY(), pos.getZ());
+ }
+ // Paper end - Perf: Reduce instructions and provide final method
int i = pos.getX();
int j = pos.getY();
int k = pos.getZ();
@@ -224,6 +245,18 @@ public class LevelChunk extends ChunkAccess {
}
}
+ // Paper start - If loaded util
+ @Override
+ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+ return this.getFluidState(blockposition);
+ }
+
+ @Override
+ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
+ return this.getBlockState(blockposition);
+ }
+ // Paper end
+
@Override
public FluidState getFluidState(BlockPos pos) {
return this.getFluidState(pos.getX(), pos.getY(), pos.getZ());
@@ -554,7 +587,11 @@ public class LevelChunk extends ChunkAccess {
// CraftBukkit start
public void loadCallback() {
+ // Paper start
+ this.loadedTicketLevel = true;
+ // Paper end
org.bukkit.Server server = this.level.getCraftServer();
+ this.level.getChunkSource().addLoadedChunk(this); // Paper
if (server != null) {
/*
* If it's a new world, the first few chunks are generated inside
@@ -595,6 +632,10 @@ public class LevelChunk extends ChunkAccess {
server.getPluginManager().callEvent(unloadEvent);
// note: saving can be prevented, but not forced if no saving is actually required
this.mustNotSave = !unloadEvent.isSaveChunk();
+ this.level.getChunkSource().removeLoadedChunk(this); // Paper
+ // Paper start
+ this.loadedTicketLevel = false;
+ // Paper end
}
@Override
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
index ca4c8f74a1ab2a8b36e193a2c40c3bd76565d258..2c153af611399e884752f8256bee4fe32de5c572 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
@@ -19,7 +19,7 @@ public class LevelChunkSection {
public static final int SECTION_HEIGHT = 16;
public static final int SECTION_SIZE = 4096;
public static final int BIOME_CONTAINER_BITS = 2;
- private short nonEmptyBlockCount;
+ short nonEmptyBlockCount; // Paper - package private
private short tickingBlockCount;
private short tickingFluidCount;
public final PalettedContainer<BlockState> states;
diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
index ae16b014abd52ee10d523fb003cce166b846b222..7f302405a88766c2112539d24d3dd2e513f94985 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
@@ -83,6 +83,18 @@ public class ProtoChunk extends ChunkAccess {
return new ChunkAccess.TicksToSave(this.blockTicks, this.fluidTicks);
}
+ // Paper start - If loaded util
+ @Override
+ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+ return this.getFluidState(blockposition);
+ }
+
+ @Override
+ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
+ return this.getBlockState(blockposition);
+ }
+ // Paper end
+
@Override
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..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
}
private boolean addEntity(T entity, boolean existing) {
+ // Paper start - chunk system hooks
+ if (existing) {
+ // I don't want to know why this is a generic type.
+ Entity entityCasted = (Entity)entity;
+ boolean 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;
+ }
+ }
+ // Paper end - chunk system hooks
if (!this.addEntityUuid(entity)) {
return false;
} else {
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index 4c5b6f8d63e4c60a1dc81e68117fa049b956819a..958925a980f50cc85b10d36bcc343947cec8f285 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -2598,4 +2598,9 @@ public final class CraftServer implements Server {
return this.spigot;
}
// Spigot end
+
+ @Override
+ public double[] getTPS() {
+ return new double[]{0, 0, 0}; // TODO
+ }
}
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
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 {
@Override
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 = 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);
}
@Override
@@ -328,7 +328,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public boolean refreshChunk(int x, int z) {
- ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.visibleChunkMap.get(ChunkPos.asLong(x, z));
+ ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
if (playerChunk == null) return false;
playerChunk.getTickingChunkFuture().thenAccept(either -> {
@@ -2100,4 +2100,55 @@ public class CraftWorld extends CraftRegionAccessor implements World {
return this.spigot;
}
// Spigot end
+ // Paper start
+ public java.util.concurrent.CompletableFuture<Chunk> getChunkAtAsync(int x, int z, boolean gen, boolean urgent) {
+ if (Bukkit.isPrimaryThread()) {
+ net.minecraft.world.level.chunk.LevelChunk immediate = this.world.getChunkSource().getChunkAtIfLoadedImmediately(x, z);
+ if (immediate != null) {
+ return java.util.concurrent.CompletableFuture.completedFuture(new CraftChunk(immediate));
+ }
+ }
+
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority;
+ if (urgent) {
+ priority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER;
+ } else {
+ priority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL;
+ }
+
+ java.util.concurrent.CompletableFuture<Chunk> ret = new java.util.concurrent.CompletableFuture<>();
+
+ 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));
+ });
+ });
+
+ return ret;
+ }
+
+ @Override
+ public void setViewDistance(final int viewDistance) {
+ if (viewDistance < 2 || viewDistance > 32) {
+ throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]");
+ }
+ this.getHandle().chunkSource.chunkMap.setServerViewDistance(viewDistance);
+ }
+
+ @Override
+ public void setSimulationDistance(final int simulationDistance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public int getSendViewDistance() {
+ return this.getViewDistance();
+ }
+
+ @Override
+ public void setSendViewDistance(final int viewDistance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+ // 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 4cbf0d797fc2d57c822877c111698b2077399d32..9cd22b09b295ecd95d17d1dd5edb96f916eb6c09 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 {
return this.spigot;
}
// Spigot end
+
+ @Override
+ public int getViewDistance() {
+ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getLoadViewDistance(this.getHandle()) - 1;
+ }
+
+ @Override
+ public void setViewDistance(final int viewDistance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public int getSimulationDistance() {
+ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getTickViewDistance(this.getHandle());
+ }
+
+ @Override
+ public void setSimulationDistance(final int simulationDistance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public int getSendViewDistance() {
+ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getSendViewDistance(this.getHandle());
+ }
+
+ @Override
+ public void setSendViewDistance(final int viewDistance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
}
diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
index 952c6ebde7031dc060efe98992f82c02bf3534ea..17fa2d3db112762bcb8b941b69b8ddcc53f47224 100644
--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
@@ -23,6 +23,20 @@ import org.jetbrains.annotations.ApiStatus;
@DelegateDeserialization(ItemStack.class)
public final class CraftItemStack extends ItemStack {
+ // Paper start - MC Utils
+ public static net.minecraft.world.item.ItemStack unwrap(ItemStack bukkit) {
+ if (bukkit instanceof CraftItemStack craftItemStack) {
+ return craftItemStack.handle != null ? craftItemStack.handle : net.minecraft.world.item.ItemStack.EMPTY;
+ } else {
+ return asNMSCopy(bukkit);
+ }
+ }
+
+ public static net.minecraft.world.item.ItemStack getOrCloneOnMutation(ItemStack old, ItemStack newInstance) {
+ return old == newInstance ? unwrap(old) : asNMSCopy(newInstance);
+ }
+ // Paper end - MC Utils
+
public static net.minecraft.world.item.ItemStack asNMSCopy(ItemStack original) {
if (original instanceof CraftItemStack) {
CraftItemStack stack = (CraftItemStack) original;
diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
index 5fd6eb754c4edebed6798c65b06507a4e89ca48f..524b51a0ab808a0629c871ad813115abd4b49dbd 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
@@ -58,6 +58,7 @@ import net.minecraft.world.phys.shapes.VoxelShape;
import net.minecraft.world.ticks.LevelTickAccess;
import net.minecraft.world.ticks.TickPriority;
import org.bukkit.event.entity.CreatureSpawnEvent;
+import org.jetbrains.annotations.Nullable;
public abstract class DelegatedGeneratorAccess implements WorldGenLevel {
@@ -807,4 +808,24 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel {
public int getMoonPhase() {
return this.handle.getMoonPhase();
}
+
+ // Paper start
+ @Nullable
+ @Override
+ public BlockState getBlockStateIfLoaded(final BlockPos blockposition) {
+ return this.handle.getBlockStateIfLoaded(blockposition);
+ }
+
+ @Nullable
+ @Override
+ public FluidState getFluidIfLoaded(final BlockPos blockposition) {
+ return this.handle.getFluidIfLoaded(blockposition);
+ }
+
+ @Nullable
+ @Override
+ public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) {
+ return this.handle.getChunkIfLoadedImmediately(x, z);
+ }
+ // Paper end
}
diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
index e837d76e833d73d888bc1dad3515c2b82bc0e437..4705aed1dd98378c146bf9e346df1a17f719ad36 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
@@ -212,7 +212,23 @@ public class DummyGeneratorAccess implements WorldGenLevel {
public FluidState getFluidState(BlockPos pos) {
return Fluids.EMPTY.defaultFluidState(); // SPIGOT-6634
}
+ // Paper start - if loaded util
+ @javax.annotation.Nullable
+ @Override
+ public ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ @Override
+ public BlockState getBlockStateIfLoaded(BlockPos blockposition) {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+ @Override
+ public FluidState getFluidIfLoaded(BlockPos blockposition) {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+ // Paper end
@Override
public WorldBorder getWorldBorder() {
throw new UnsupportedOperationException("Not supported yet.");
diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java
index 0c7c97f27853843ec714e47f5b570f9d09bbba14..ff422d4d4f2b764370f0ee2af13034853c1d3fe1 100644
--- a/src/main/java/org/spigotmc/ActivationRange.java
+++ b/src/main/java/org/spigotmc/ActivationRange.java
@@ -34,6 +34,9 @@ public class ActivationRange
public enum ActivationType
{
+ WATER, // Paper
+ FLYING_MONSTER, // Paper
+ VILLAGER, // Paper
MONSTER,
ANIMAL,
RAIDER,