PaperMC/patches/server/0991-Chunk-System-Starlight-from-Moonrise.patch
Spottedleaf f7124df56b Fix recursive chunk loading in chunk unload event
Since the chunk may not even be at a loaded ticket level, the
getChunk call may invoke a sync load. To prevent this, we can
retrieve the full loaded chunk first which is guaranteed to be
non-null when unloading.
2024-07-11 07:16:42 -07:00

29789 lines
1.3 MiB

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Fri, 14 Jun 2024 11:57:26 -0700
Subject: [PATCH] Chunk System + Starlight from Moonrise
See https://github.com/Tuinity/Moonrise
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..93e8c8134da8ee1a9b777c708f992922a1a7de8b
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
@@ -0,0 +1,135 @@
+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 final Reference2IntOpenHashMap<E> referenceToIndex = new Reference2IntOpenHashMap<>(2, 0.8f);
+ {
+ this.referenceToIndex.defaultReturnValue(Integer.MIN_VALUE);
+ }
+
+ private static final Object[] EMPTY_LIST = new Object[0];
+
+ private E[] references;
+ private int count;
+
+ public ReferenceList() {
+ this((E[])EMPTY_LIST, 0);
+ }
+
+ public ReferenceList(final E[] array, final int count) {
+ this.references = array;
+ this.count = count;
+ }
+
+ 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/SingleUserAreaMap.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..61f70247486fd15ed3ffc5b606582dc6a2dd81d3
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
@@ -0,0 +1,232 @@
+package ca.spottedleaf.moonrise.common.misc;
+
+import ca.spottedleaf.concurrentutil.util.IntegerUtil;
+
+public abstract class SingleUserAreaMap<T> {
+
+ private static final int NOT_SET = Integer.MIN_VALUE;
+
+ private final T parameter;
+ private int lastChunkX = NOT_SET;
+ private int lastChunkZ = NOT_SET;
+ private int distance = NOT_SET;
+
+ public SingleUserAreaMap(final T parameter) {
+ this.parameter = parameter;
+ }
+
+ /* math sign function except 0 returns 1 */
+ protected static int sign(int val) {
+ return 1 | (val >> (Integer.SIZE - 1));
+ }
+
+ protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ);
+
+ protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ);
+
+ private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) {
+ final int maxX = chunkX + distance;
+ final int maxZ = chunkZ + distance;
+
+ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
+ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
+ this.addCallback(parameter, cx, cz);
+ }
+ }
+ }
+
+ private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) {
+ final int maxX = chunkX + distance;
+ final int maxZ = chunkZ + distance;
+
+ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
+ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
+ this.removeCallback(parameter, cx, cz);
+ }
+ }
+ }
+
+ public final boolean add(final int chunkX, final int chunkZ, final int distance) {
+ if (distance < 0) {
+ throw new IllegalArgumentException(Integer.toString(distance));
+ }
+ if (this.lastChunkX != NOT_SET) {
+ return false;
+ }
+ this.lastChunkX = chunkX;
+ this.lastChunkZ = chunkZ;
+ this.distance = distance;
+
+ this.addToNew(this.parameter, chunkX, chunkZ, distance);
+
+ return true;
+ }
+
+ public final boolean update(final int toX, final int toZ, final int newViewDistance) {
+ if (newViewDistance < 0) {
+ throw new IllegalArgumentException(Integer.toString(newViewDistance));
+ }
+ final int fromX = this.lastChunkX;
+ final int fromZ = this.lastChunkZ;
+ final int oldViewDistance = this.distance;
+ if (fromX == NOT_SET) {
+ return false;
+ }
+
+ this.lastChunkX = toX;
+ this.lastChunkZ = toZ;
+ this.distance = newViewDistance;
+
+ final T parameter = this.parameter;
+
+
+ final int dx = toX - fromX;
+ final int dz = toZ - fromZ;
+
+ final int totalX = IntegerUtil.branchlessAbs(fromX - toX);
+ final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
+
+ if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
+ // teleported
+ this.removeFromOld(parameter, fromX, fromZ, oldViewDistance);
+ this.addToNew(parameter, toX, toZ, newViewDistance);
+ return true;
+ }
+
+ if (oldViewDistance != newViewDistance) {
+ // remove loop
+
+ final int oldMinX = fromX - oldViewDistance;
+ final int oldMinZ = fromZ - oldViewDistance;
+ final int oldMaxX = fromX + oldViewDistance;
+ final int oldMaxZ = fromZ + oldViewDistance;
+ for (int currX = oldMinX; currX <= oldMaxX; ++currX) {
+ for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) {
+
+ // only remove if we're outside the new view distance...
+ if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) {
+ this.removeCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ // add loop
+
+ final int newMinX = toX - newViewDistance;
+ final int newMinZ = toZ - newViewDistance;
+ final int newMaxX = toX + newViewDistance;
+ final int newMaxZ = toZ + newViewDistance;
+ for (int currX = newMinX; currX <= newMaxX; ++currX) {
+ for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) {
+
+ // only add if we're outside the old view distance...
+ if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) {
+ this.addCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ // x axis is width
+ // z axis is height
+ // right refers to the x axis of where we moved
+ // top refers to the z axis of where we moved
+
+ // same view distance
+
+ // used for relative positioning
+ final int up = sign(dz); // 1 if dz >= 0, -1 otherwise
+ final int right = sign(dx); // 1 if dx >= 0, -1 otherwise
+
+ // The area excluded by overlapping the two view distance squares creates four rectangles:
+ // Two on the left, and two on the right. The ones on the left we consider the "removed" section
+ // and on the right the "added" section.
+ // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually
+ // exclusive to the regions they surround.
+
+ // 4 points of the rectangle
+ int maxX; // exclusive
+ int minX; // inclusive
+ int maxZ; // exclusive
+ int minZ; // inclusive
+
+ if (dx != 0) {
+ // handle right addition
+
+ maxX = toX + (oldViewDistance * right) + right; // exclusive
+ minX = fromX + (oldViewDistance * right) + right; // inclusive
+ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
+ minZ = toZ - (oldViewDistance * up); // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.addCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ if (dz != 0) {
+ // handle up addition
+
+ maxX = toX + (oldViewDistance * right) + right; // exclusive
+ minX = toX - (oldViewDistance * right); // inclusive
+ maxZ = toZ + (oldViewDistance * up) + up; // exclusive
+ minZ = fromZ + (oldViewDistance * up) + up; // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.addCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ if (dx != 0) {
+ // handle left removal
+
+ maxX = toX - (oldViewDistance * right); // exclusive
+ minX = fromX - (oldViewDistance * right); // inclusive
+ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
+ minZ = toZ - (oldViewDistance * up); // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.removeCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ if (dz != 0) {
+ // handle down removal
+
+ maxX = fromX + (oldViewDistance * right) + right; // exclusive
+ minX = fromX - (oldViewDistance * right); // inclusive
+ maxZ = toZ - (oldViewDistance * up); // exclusive
+ minZ = fromZ - (oldViewDistance * up); // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.removeCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public final boolean remove() {
+ final int chunkX = this.lastChunkX;
+ final int chunkZ = this.lastChunkZ;
+ final int distance = this.distance;
+ if (chunkX == NOT_SET) {
+ return false;
+ }
+
+ this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET;
+
+ this.removeFromOld(this.parameter, chunkX, chunkZ, distance);
+
+ return true;
+ }
+}
diff --git a/src/main/java/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/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..ef1c9e1e8636a14b5215c6c55d3032bacfd94cac
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
@@ -0,0 +1,45 @@
+package ca.spottedleaf.moonrise.common.util;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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));
+
+ int workerThreads = chunkSystem.workerThreads;
+
+ if (workerThreads <= 0) {
+ workerThreads = defaultWorkerThreads;
+ }
+
+ WORKER_POOL = new PrioritisedThreadPool(
+ "Paper Worker Pool", workerThreads,
+ (final Thread thread, final Integer id) -> {
+ thread.setName("Paper Common Worker #" + id.intValue());
+ 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/WorldUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..e95cc73ddf20050aa4a241b0a309240e2bf46abd
--- /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();
+ }
+
+ private WorldUtil() {
+ throw new RuntimeException();
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java
new file mode 100644
index 0000000000000000000000000000000000000000..c2ff037e180393de6576f12c32c665ef640d6f50
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java
@@ -0,0 +1,162 @@
+package ca.spottedleaf.moonrise.patches.chunk_system;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk;
+import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader;
+import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache;
+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();
+
+ 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) {
+ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkTask(chunkX, chunkZ, run, priority);
+ }
+
+ 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) {
+ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete);
+ }
+
+ // Paper - rewrite chunk system
+ 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) {
+ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ }
+
+ 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) {
+ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ }
+
+ public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
+ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
+ }
+
+ public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
+ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders();
+ }
+
+ public static int getVisibleChunkHolderCount(final ServerLevel level) {
+ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
+ }
+
+ public static int getUpdatingChunkHolderCount(final ServerLevel level) {
+ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size();
+ }
+
+ public static boolean hasAnyChunkHolders(final ServerLevel level) {
+ return getUpdatingChunkHolderCount(level) != 0;
+ }
+
+ public static void onEntityPreAdd(final ServerLevel level, final Entity entity) {
+ // TODO move hook
+ io.papermc.paper.chunk.system.ChunkSystem.onEntityPreAdd(level, entity);
+ }
+
+ public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
+ // TODO move hook
+ io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderCreate(level, holder);
+ }
+
+ public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
+ // TODO move hook
+ io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(level, holder);
+ }
+
+ public static void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) {
+ ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource())
+ .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, chunk);
+ }
+
+ public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
+ // TODO move hook
+ io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(chunk, holder);
+ chunk.loadCallback(); // Paper
+ }
+
+ public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
+ // TODO move hook
+ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(chunk, holder);
+ chunk.unloadCallback(); // Paper
+ }
+
+ public static void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
+ ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource())
+ .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, null);
+ }
+
+ public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
+ // TODO move hook
+ io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(chunk, holder);
+ if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) {
+ chunk.postProcessGeneration();
+ }
+ ((ServerLevel)chunk.getLevel()).startTickingChunk(chunk);
+ ((ServerLevel)chunk.getLevel()).getChunkSource().chunkMap.tickingGenerated.incrementAndGet();
+ }
+
+ public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
+ // TODO move hook
+ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(chunk, holder);
+ }
+
+ public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
+ // TODO move hook
+ io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(chunk, holder);
+ }
+
+ public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
+ // TODO move hook
+ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(chunk, holder);
+ }
+
+ public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
+ return null;
+ }
+
+ public static int getSendViewDistance(final ServerPlayer player) {
+ return RegionizedPlayerChunkLoader.getAPISendViewDistance(player);
+ }
+
+ public static int getLoadViewDistance(final ServerPlayer player) {
+ return RegionizedPlayerChunkLoader.getLoadViewDistance(player);
+ }
+
+ public static int getTickViewDistance(final ServerPlayer player) {
+ return RegionizedPlayerChunkLoader.getAPITickViewDistance(player);
+ }
+
+ public static void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) {
+ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().addPlayer(player);
+ }
+
+ public static void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) {
+ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().removePlayer(player);
+ }
+
+ public static void updateMaps(final ServerLevel world, final ServerPlayer player) {
+ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player);
+ }
+
+ private ChunkSystem() {}
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
new file mode 100644
index 0000000000000000000000000000000000000000..49160a30b8e19e5c5ada811fbcae2a05959524f3
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
@@ -0,0 +1,38 @@
+package ca.spottedleaf.moonrise.patches.chunk_system;
+
+import net.minecraft.SharedConstants;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.Tag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.util.datafix.DataFixTypes;
+
+public final class ChunkSystemConverters {
+
+ // See SectionStorage#getVersion
+ private static final int DEFAULT_POI_DATA_VERSION = 1945;
+
+ private static final int DEFAULT_ENTITY_CHUNK_DATA_VERSION = -1;
+
+ private static int getCurrentVersion() {
+ return SharedConstants.getCurrentVersion().getDataVersion().getVersion();
+ }
+
+ private static int getDataVersion(final CompoundTag data, final int dfl) {
+ return !data.contains(SharedConstants.DATA_VERSION_TAG, Tag.TAG_ANY_NUMERIC)
+ ? dfl : data.getInt(SharedConstants.DATA_VERSION_TAG);
+ }
+
+ public static CompoundTag convertPoiCompoundTag(final CompoundTag data, final ServerLevel world) {
+ final int dataVersion = getDataVersion(data, DEFAULT_POI_DATA_VERSION);
+
+ return DataFixTypes.POI_CHUNK.update(world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion());
+ }
+
+ public static CompoundTag convertEntityChunkCompoundTag(final CompoundTag data, final ServerLevel world) {
+ final int dataVersion = getDataVersion(data, DEFAULT_ENTITY_CHUNK_DATA_VERSION);
+
+ return DataFixTypes.ENTITY_CHUNK.update(world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion());
+ }
+
+ private ChunkSystemConverters() {}
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java
new file mode 100644
index 0000000000000000000000000000000000000000..67f6dd9a4855611cfe242c2e37e90f6d27d4c823
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java
@@ -0,0 +1,36 @@
+package ca.spottedleaf.moonrise.patches.chunk_system;
+
+import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.chunk.ChunkAccess;
+
+public final class ChunkSystemFeatures {
+
+ public static boolean supportsAsyncChunkSave() {
+ // uncertain how to properly pass AsyncSaveData to ChunkSerializer#write
+ // additionally, there may be mods hooking into the write() call which may not be thread-safe to call
+ return true;
+ }
+
+ public static AsyncChunkSaveData getAsyncSaveData(final ServerLevel world, final ChunkAccess chunk) {
+ return net.minecraft.world.level.chunk.storage.ChunkSerializer.getAsyncSaveData(world, chunk);
+ }
+
+ public static CompoundTag saveChunkAsync(final ServerLevel world, final ChunkAccess chunk, final AsyncChunkSaveData asyncSaveData) {
+ return net.minecraft.world.level.chunk.storage.ChunkSerializer.saveChunk(world, chunk, asyncSaveData);
+ }
+
+ public static boolean forceNoSave(final ChunkAccess chunk) {
+ // support for CB chunk mustNotSave
+ return chunk instanceof net.minecraft.world.level.chunk.LevelChunk levelChunk && levelChunk.mustNotSave;
+ }
+
+ public static boolean supportsAsyncChunkDeserialization() {
+ // as it stands, the current problem with supporting this in Moonrise is that we are unsure that any mods
+ // hooking into ChunkSerializer#read() are thread-safe to call
+ return true;
+ }
+
+ private ChunkSystemFeatures() {}
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java
new file mode 100644
index 0000000000000000000000000000000000000000..becd1c6d54ed6c912aee3a9178a970e2751d3694
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java
@@ -0,0 +1,11 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.async_save;
+
+import net.minecraft.nbt.ListTag;
+import net.minecraft.nbt.Tag;
+
+public record AsyncChunkSaveData(
+ Tag blockTickList, // non-null if we had to go to the server's tick list
+ Tag fluidTickList, // non-null if we had to go to the server's tick list
+ ListTag blockEntities,
+ long worldTime
+) {}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c279854bdf214538380fa354e4298ec4bd9ac4e
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
@@ -0,0 +1,39 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.entity;
+
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.monster.Shulker;
+import net.minecraft.world.entity.vehicle.AbstractMinecart;
+import net.minecraft.world.entity.vehicle.Boat;
+
+public interface ChunkSystemEntity {
+
+ public boolean moonrise$isHardColliding();
+
+ // for mods to override
+ public default boolean moonrise$isHardCollidingUncached() {
+ return this instanceof Boat || this instanceof AbstractMinecart || this instanceof Shulker || ((Entity)this).canBeCollidedWith();
+ }
+
+ public FullChunkStatus moonrise$getChunkStatus();
+
+ public void moonrise$setChunkStatus(final FullChunkStatus status);
+
+ public int moonrise$getSectionX();
+
+ public void moonrise$setSectionX(final int x);
+
+ public int moonrise$getSectionY();
+
+ public void moonrise$setSectionY(final int y);
+
+ public int moonrise$getSectionZ();
+
+ public void moonrise$setSectionZ(final int z);
+
+ public boolean moonrise$isUpdatingSectionStatus();
+
+ public void moonrise$setUpdatingSectionStatus(final boolean to);
+
+ public boolean moonrise$hasAnyPlayerPassengers();
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..73df26b27146bbad2106d57b22dd3c792ed3dd1d
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
@@ -0,0 +1,14 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.io;
+
+import net.minecraft.world.level.chunk.storage.RegionFile;
+import java.io.IOException;
+
+public interface ChunkSystemRegionFileStorage {
+
+ public boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ);
+
+ public RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ);
+
+ public RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException;
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..c833f78d083b8f661087471c35bc90f65af1b525
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java
@@ -0,0 +1,1239 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.io;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.executor.Cancellable;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedQueueExecutorThread;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue;
+import ca.spottedleaf.concurrentutil.function.BiLong1Function;
+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.storage.RegionFile;
+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.io.IOException;
+import java.lang.invoke.VarHandle;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Prioritised RegionFile I/O executor, responsible for all RegionFile access.
+ * <p>
+ * All functions provided are MT-Safe, however certain ordering constraints are recommended:
+ * <li>
+ * Chunk saves may not occur for unloaded chunks.
+ * </li>
+ * <li>
+ * Tasks must be scheduled on the chunk scheduler thread.
+ * </li>
+ * By following these constraints, no chunk data loss should occur with the exception of underlying I/O problems.
+ * </p>
+ */
+public final class RegionFileIOThread extends PrioritisedQueueExecutorThread {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(RegionFileIOThread.class);
+
+ /**
+ * The kinds of region files controlled by the region file thread. Add more when needed, and ensure
+ * getControllerFor is updated.
+ */
+ public static enum RegionFileType {
+ CHUNK_DATA,
+ POI_DATA,
+ ENTITY_DATA;
+ }
+
+ private static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values();
+
+ public static ChunkDataController getControllerFor(final ServerLevel world, final RegionFileType type) {
+ switch (type) {
+ case CHUNK_DATA:
+ return ((ChunkSystemServerLevel)world).moonrise$getChunkDataController();
+ case POI_DATA:
+ return ((ChunkSystemServerLevel)world).moonrise$getPoiChunkDataController();
+ case ENTITY_DATA:
+ return ((ChunkSystemServerLevel)world).moonrise$getEntityChunkDataController();
+ default:
+ throw new IllegalStateException("Unknown controller type " + type);
+ }
+ }
+
+ /**
+ * Collects regionfile data for a certain chunk.
+ */
+ public static final class RegionFileData {
+
+ private final boolean[] hasResult = new boolean[CACHED_REGIONFILE_TYPES.length];
+ private final CompoundTag[] data = new CompoundTag[CACHED_REGIONFILE_TYPES.length];
+ private final Throwable[] throwables = new Throwable[CACHED_REGIONFILE_TYPES.length];
+
+ /**
+ * Sets the result associated with the specified regionfile type. Note that
+ * results can only be set once per regionfile type.
+ *
+ * @param type The regionfile type.
+ * @param data The result to set.
+ */
+ public void setData(final RegionFileType type, final CompoundTag data) {
+ final int index = type.ordinal();
+
+ if (this.hasResult[index]) {
+ throw new IllegalArgumentException("Result already exists for type " + type);
+ }
+ this.hasResult[index] = true;
+ this.data[index] = data;
+ }
+
+ /**
+ * Sets the result associated with the specified regionfile type. Note that
+ * results can only be set once per regionfile type.
+ *
+ * @param type The regionfile type.
+ * @param throwable The result to set.
+ */
+ public void setThrowable(final RegionFileType type, final Throwable throwable) {
+ final int index = type.ordinal();
+
+ if (this.hasResult[index]) {
+ throw new IllegalArgumentException("Result already exists for type " + type);
+ }
+ this.hasResult[index] = true;
+ this.throwables[index] = throwable;
+ }
+
+ /**
+ * Returns whether there is a result for the specified regionfile type.
+ *
+ * @param type Specified regionfile type.
+ *
+ * @return Whether a result exists for {@code type}.
+ */
+ public boolean hasResult(final RegionFileType type) {
+ return this.hasResult[type.ordinal()];
+ }
+
+ /**
+ * Returns the data result for the regionfile type.
+ *
+ * @param type Specified regionfile type.
+ *
+ * @throws IllegalArgumentException If the result has not been set for {@code type}.
+ * @return The data result for the specified type. If the result is a {@code Throwable},
+ * then returns {@code null}.
+ */
+ public CompoundTag getData(final RegionFileType type) {
+ final int index = type.ordinal();
+
+ if (!this.hasResult[index]) {
+ throw new IllegalArgumentException("Result does not exist for type " + type);
+ }
+
+ return this.data[index];
+ }
+
+ /**
+ * Returns the throwable result for the regionfile type.
+ *
+ * @param type Specified regionfile type.
+ *
+ * @throws IllegalArgumentException If the result has not been set for {@code type}.
+ * @return The throwable result for the specified type. If the result is an {@code CompoundTag},
+ * then returns {@code null}.
+ */
+ public Throwable getThrowable(final RegionFileType type) {
+ final int index = type.ordinal();
+
+ if (!this.hasResult[index]) {
+ throw new IllegalArgumentException("Result does not exist for type " + type);
+ }
+
+ return this.throwables[index];
+ }
+ }
+
+ private static final Object INIT_LOCK = new Object();
+
+ static RegionFileIOThread[] threads;
+
+ /* needs to be consistent given a set of parameters */
+ static RegionFileIOThread selectThread(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
+ if (threads == null) {
+ throw new IllegalStateException("Threads not initialised");
+ }
+
+ final int regionX = chunkX >> 5;
+ final int regionZ = chunkZ >> 5;
+ final int typeOffset = type.ordinal();
+
+ return threads[(System.identityHashCode(world) + regionX + regionZ + typeOffset) % threads.length];
+ }
+
+ /**
+ * Shuts down the I/O executor(s). Watis for all tasks to complete if specified.
+ * Tasks queued during this call might not be accepted, and tasks queued after will not be accepted.
+ *
+ * @param wait Whether to wait until all tasks have completed.
+ */
+ public static void close(final boolean wait) {
+ for (int i = 0, len = threads.length; i < len; ++i) {
+ threads[i].close(false, true);
+ }
+ if (wait) {
+ RegionFileIOThread.flush();
+ }
+ }
+
+ public static long[] getExecutedTasks() {
+ final long[] ret = new long[threads.length];
+ for (int i = 0, len = threads.length; i < len; ++i) {
+ ret[i] = threads[i].getTotalTasksExecuted();
+ }
+
+ return ret;
+ }
+
+ public static long[] getTasksScheduled() {
+ final long[] ret = new long[threads.length];
+ for (int i = 0, len = threads.length; i < len; ++i) {
+ ret[i] = threads[i].getTotalTasksScheduled();
+ }
+ return ret;
+ }
+
+ public static void flush() {
+ for (int i = 0, len = threads.length; i < len; ++i) {
+ threads[i].waitUntilAllExecuted();
+ }
+ }
+
+ public static void flushRegionStorages(final ServerLevel world) throws IOException {
+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
+ getControllerFor(world, type).getCache().flush();
+ }
+ }
+
+ public static void partialFlush(final int totalTasksRemaining) {
+ long failures = 1L; // start out at 0.25ms
+
+ for (;;) {
+ final long[] executed = getExecutedTasks();
+ final long[] scheduled = getTasksScheduled();
+
+ long sum = 0;
+ for (int i = 0; i < executed.length; ++i) {
+ sum += scheduled[i] - executed[i];
+ }
+
+ if (sum <= totalTasksRemaining) {
+ break;
+ }
+
+ failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms
+ }
+ }
+
+ /**
+ * Inits the executor with the specified number of threads.
+ *
+ * @param threads Specified number of threads.
+ */
+ public static void init(final int threads) {
+ synchronized (INIT_LOCK) {
+ if (RegionFileIOThread.threads != null) {
+ throw new IllegalStateException("Already initialised threads");
+ }
+
+ RegionFileIOThread.threads = new RegionFileIOThread[threads];
+
+ for (int i = 0; i < threads; ++i) {
+ RegionFileIOThread.threads[i] = new RegionFileIOThread(i);
+ RegionFileIOThread.threads[i].start();
+ }
+ }
+ }
+
+ public static void deinit() {
+ if (true) { // Paper
+ // TODO does this cause issues with mods? how to implement
+ close(true);
+ synchronized (INIT_LOCK) {
+ RegionFileIOThread.threads = null;
+ }
+ } else { RegionFileIOThread.flush(); }
+ }
+
+ private RegionFileIOThread(final int threadNumber) {
+ super(new PrioritisedThreadedTaskQueue(), (int)(1.0e6)); // 1.0ms spinwait time
+ this.setName("RegionFile I/O Thread #" + threadNumber);
+ this.setPriority(Thread.NORM_PRIORITY - 2); // we keep priority close to normal because threads can wait on us
+ this.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> {
+ LOGGER.error("Uncaught exception thrown from I/O thread, report this! Thread: " + thread.getName(), thr);
+ });
+ }
+
+ /**
+ * Returns whether the current thread is a regionfile I/O executor.
+ * @return Whether the current thread is a regionfile I/O executor.
+ */
+ public static boolean isRegionFileThread() {
+ return Thread.currentThread() instanceof RegionFileIOThread;
+ }
+
+ /**
+ * Returns the priority associated with blocking I/O based on the current thread. The goal is to avoid
+ * dumb plugins from taking away priority from threads we consider crucial.
+ * @return The priroity to use with blocking I/O on the current thread.
+ */
+ public static Priority getIOBlockingPriorityForCurrentThread() {
+ if (io.papermc.paper.util.TickThread.isTickThread()) {
+ return Priority.BLOCKING;
+ }
+ return Priority.HIGHEST;
+ }
+
+ /**
+ * Returns the current {@code CompoundTag} pending for write for the specified chunk & regionfile type.
+ * Note that this does not copy the result, so do not modify the result returned.
+ *
+ * @param world Specified world.
+ * @param chunkX Specified chunk x.
+ * @param chunkZ Specified chunk z.
+ * @param type Specified regionfile type.
+ *
+ * @return The compound tag associated for the specified chunk. {@code null} if no write was pending, or if {@code null} is the write pending.
+ */
+ public static CompoundTag getPendingWrite(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
+ return thread.getPendingWriteInternal(world, chunkX, chunkZ, type);
+ }
+
+ CompoundTag getPendingWriteInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
+ final ChunkDataController taskController = getControllerFor(world, type);
+ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (task == null) {
+ return null;
+ }
+
+ final CompoundTag ret = task.inProgressWrite;
+
+ return ret == ChunkDataTask.NOTHING_TO_WRITE ? null : ret;
+ }
+
+ /**
+ * Returns the priority for the specified regionfile type for the specified chunk.
+ * @param world Specified world.
+ * @param chunkX Specified chunk x.
+ * @param chunkZ Specified chunk z.
+ * @param type Specified regionfile type.
+ * @return The priority for the chunk
+ */
+ public static Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
+ return thread.getPriorityInternal(world, chunkX, chunkZ, type);
+ }
+
+ Priority getPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
+ final ChunkDataController taskController = getControllerFor(world, type);
+ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (task == null) {
+ return Priority.COMPLETING;
+ }
+
+ return task.prioritisedTask.getPriority();
+ }
+
+ /**
+ * Sets the priority for all regionfile types for the specified chunk. Note that great care should
+ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different
+ * priorities.
+ *
+ * @param world Specified world.
+ * @param chunkX Specified chunk x.
+ * @param chunkZ Specified chunk z.
+ * @param priority New priority.
+ *
+ * @see #raisePriority(ServerLevel, int, int, Priority)
+ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
+ * @see #lowerPriority(ServerLevel, int, int, Priority)
+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
+ */
+ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Priority priority) {
+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
+ RegionFileIOThread.setPriority(world, chunkX, chunkZ, type, priority);
+ }
+ }
+
+ /**
+ * Sets the priority for the specified regionfile type for the specified chunk. Note that great care should
+ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different
+ * priorities.
+ *
+ * @param world Specified world.
+ * @param chunkX Specified chunk x.
+ * @param chunkZ Specified chunk z.
+ * @param type Specified regionfile type.
+ * @param priority New priority.
+ *
+ * @see #raisePriority(ServerLevel, int, int, Priority)
+ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
+ * @see #lowerPriority(ServerLevel, int, int, Priority)
+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
+ */
+ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
+ final Priority priority) {
+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
+ thread.setPriorityInternal(world, chunkX, chunkZ, type, priority);
+ }
+
+ void setPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
+ final Priority priority) {
+ final ChunkDataController taskController = getControllerFor(world, type);
+ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (task != null) {
+ task.prioritisedTask.setPriority(priority);
+ }
+ }
+
+ /**
+ * Raises the priority for all regionfile types for the specified chunk.
+ *
+ * @param world Specified world.
+ * @param chunkX Specified chunk x.
+ * @param chunkZ Specified chunk z.
+ * @param priority New priority.
+ *
+ * @see #setPriority(ServerLevel, int, int, Priority)
+ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
+ * @see #lowerPriority(ServerLevel, int, int, Priority)
+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
+ */
+ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Priority priority) {
+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
+ RegionFileIOThread.raisePriority(world, chunkX, chunkZ, type, priority);
+ }
+ }
+
+ /**
+ * Raises the priority for the specified regionfile type for the specified chunk.
+ *
+ * @param world Specified world.
+ * @param chunkX Specified chunk x.
+ * @param chunkZ Specified chunk z.
+ * @param type Specified regionfile type.
+ * @param priority New priority.
+ *
+ * @see #setPriority(ServerLevel, int, int, Priority)
+ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
+ * @see #lowerPriority(ServerLevel, int, int, Priority)
+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
+ */
+ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
+ final Priority priority) {
+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
+ thread.raisePriorityInternal(world, chunkX, chunkZ, type, priority);
+ }
+
+ void raisePriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
+ final Priority priority) {
+ final ChunkDataController taskController = getControllerFor(world, type);
+ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (task != null) {
+ task.prioritisedTask.raisePriority(priority);
+ }
+ }
+
+ /**
+ * Lowers the priority for all regionfile types for the specified chunk.
+ *
+ * @param world Specified world.
+ * @param chunkX Specified chunk x.
+ * @param chunkZ Specified chunk z.
+ * @param priority New priority.
+ *
+ * @see #raisePriority(ServerLevel, int, int, Priority)
+ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
+ * @see #setPriority(ServerLevel, int, int, Priority)
+ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
+ */
+ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Priority priority) {
+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
+ RegionFileIOThread.lowerPriority(world, chunkX, chunkZ, type, priority);
+ }
+ }
+
+ /**
+ * Lowers the priority for the specified regionfile type for the specified chunk.
+ *
+ * @param world Specified world.
+ * @param chunkX Specified chunk x.
+ * @param chunkZ Specified chunk z.
+ * @param type Specified regionfile type.
+ * @param priority New priority.
+ *
+ * @see #raisePriority(ServerLevel, int, int, Priority)
+ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
+ * @see #setPriority(ServerLevel, int, int, Priority)
+ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
+ */
+ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
+ final Priority priority) {
+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
+ thread.lowerPriorityInternal(world, chunkX, chunkZ, type, priority);
+ }
+
+ void lowerPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
+ final Priority priority) {
+ final ChunkDataController taskController = getControllerFor(world, type);
+ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (task != null) {
+ task.prioritisedTask.lowerPriority(priority);
+ }
+ }
+
+ /**
+ * Schedules the chunk data to be written asynchronously.
+ * <p>
+ * Impl notes:
+ * </p>
+ * <li>
+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means
+ * saves must be scheduled before a chunk is unloaded.
+ * </li>
+ * <li>
+ * Writes may be called concurrently, although only the "later" write will go through.
+ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param data Chunk's data
+ * @param type The regionfile type to write to.
+ *
+ * @throws IllegalStateException If the file io thread has shutdown.
+ */
+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data,
+ final RegionFileType type) {
+ RegionFileIOThread.scheduleSave(world, chunkX, chunkZ, data, type, Priority.NORMAL);
+ }
+
+ /**
+ * Schedules the chunk data to be written asynchronously.
+ * <p>
+ * Impl notes:
+ * </p>
+ * <li>
+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means
+ * saves must be scheduled before a chunk is unloaded.
+ * </li>
+ * <li>
+ * Writes may be called concurrently, although only the "later" write will go through.
+ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param data Chunk's data
+ * @param type The regionfile type to write to.
+ * @param priority The minimum priority to schedule at.
+ *
+ * @throws IllegalStateException If the file io thread has shutdown.
+ */
+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data,
+ final RegionFileType type, final Priority priority) {
+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
+ thread.scheduleSaveInternal(world, chunkX, chunkZ, data, type, priority);
+ }
+
+ void scheduleSaveInternal(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data,
+ final RegionFileType type, final Priority priority) {
+ final ChunkDataController taskController = getControllerFor(world, type);
+
+ final boolean[] created = new boolean[1];
+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ final ChunkDataTask task = taskController.tasks.compute(key, (final long keyInMap, final ChunkDataTask taskRunning) -> {
+ if (taskRunning == null || taskRunning.failedWrite) {
+ // no task is scheduled or the previous write failed - meaning we need to overwrite it
+
+ // create task
+ final ChunkDataTask newTask = new ChunkDataTask(world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority);
+ newTask.inProgressWrite = data;
+ created[0] = true;
+
+ return newTask;
+ }
+
+ taskRunning.inProgressWrite = data;
+
+ return taskRunning;
+ });
+
+ if (created[0]) {
+ task.prioritisedTask.queue();
+ } else {
+ task.prioritisedTask.raisePriority(priority);
+ }
+ }
+
+ /**
+ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call
+ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)}
+ * for single load.
+ * <p>
+ * Impl notes:
+ * </p>
+ * <li>
+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
+ * data is undefined behaviour, and can cause deadlock.
+ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param onComplete Consumer to execute once this task has completed
+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
+ * of this call.
+ *
+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
+ *
+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)
+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)
+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...)
+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...)
+ */
+ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock) {
+ return RegionFileIOThread.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL);
+ }
+
+ /**
+ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call
+ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)}
+ * for single load.
+ * <p>
+ * Impl notes:
+ * </p>
+ * <li>
+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
+ * data is undefined behaviour, and can cause deadlock.
+ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param onComplete Consumer to execute once this task has completed
+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
+ * of this call.
+ * @param priority The minimum priority to load the data at.
+ *
+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
+ *
+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)
+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)
+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...)
+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...)
+ */
+ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock,
+ final Priority priority) {
+ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES);
+ }
+
+ /**
+ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and
+ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)}
+ * for single load.
+ * <p>
+ * Impl notes:
+ * </p>
+ * <li>
+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
+ * data is undefined behaviour, and can cause deadlock.
+ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param onComplete Consumer to execute once this task has completed
+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
+ * of this call.
+ * @param types The regionfile type(s) to load.
+ *
+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
+ *
+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)
+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)
+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean)
+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority)
+ */
+ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock,
+ final RegionFileType... types) {
+ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL, types);
+ }
+
+ /**
+ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and
+ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)}
+ * for single load.
+ * <p>
+ * Impl notes:
+ * </p>
+ * <li>
+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
+ * data is undefined behaviour, and can cause deadlock.
+ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param onComplete Consumer to execute once this task has completed
+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
+ * of this call.
+ * @param types The regionfile type(s) to load.
+ * @param priority The minimum priority to load the data at.
+ *
+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
+ *
+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)
+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)
+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean)
+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority)
+ */
+ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock,
+ final Priority priority, final RegionFileType... types) {
+ if (types == null) {
+ throw new NullPointerException("Types cannot be null");
+ }
+ if (types.length == 0) {
+ throw new IllegalArgumentException("Types cannot be empty");
+ }
+
+ final RegionFileData ret = new RegionFileData();
+
+ final Cancellable[] reads = new CancellableRead[types.length];
+ final AtomicInteger completions = new AtomicInteger();
+ final int expectedCompletions = types.length;
+
+ for (int i = 0; i < expectedCompletions; ++i) {
+ final RegionFileType type = types[i];
+ reads[i] = RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type,
+ (final CompoundTag data, final Throwable throwable) -> {
+ if (throwable != null) {
+ ret.setThrowable(type, throwable);
+ } else {
+ ret.setData(type, data);
+ }
+
+ if (completions.incrementAndGet() == expectedCompletions) {
+ onComplete.accept(ret);
+ }
+ }, intendingToBlock, priority);
+ }
+
+ return new CancellableReads(reads);
+ }
+
+ /**
+ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call
+ * {@code onComplete}.
+ * <p>
+ * Impl notes:
+ * </p>
+ * <li>
+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
+ * data is undefined behaviour, and can cause deadlock.
+ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param onComplete Consumer to execute once this task has completed
+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
+ * of this call.
+ *
+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
+ *
+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...)
+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...)
+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean)
+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority)
+ */
+ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ,
+ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete,
+ final boolean intendingToBlock) {
+ return RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, Priority.NORMAL);
+ }
+
+ /**
+ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call
+ * {@code onComplete}.
+ * <p>
+ * Impl notes:
+ * </p>
+ * <li>
+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may
+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of
+ * data is undefined behaviour, and can cause deadlock.
+ * </li>
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param onComplete Consumer to execute once this task has completed
+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost
+ * of this call.
+ * @param priority Minimum priority to load the data at.
+ *
+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data.
+ *
+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...)
+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...)
+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean)
+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority)
+ */
+ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ,
+ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete,
+ final boolean intendingToBlock, final Priority priority) {
+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
+ return thread.loadDataAsyncInternal(world, chunkX, chunkZ, type, onComplete, intendingToBlock, priority);
+ }
+
+ Cancellable loadDataAsyncInternal(final ServerLevel world, final int chunkX, final int chunkZ,
+ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete,
+ final boolean intendingToBlock, final Priority priority) {
+ final ChunkDataController taskController = getControllerFor(world, type);
+
+ final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion();
+
+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ final BiLong1Function<ChunkDataTask, ChunkDataTask> compute = (final long keyInMap, final ChunkDataTask running) -> {
+ if (running == null) {
+ // not scheduled
+
+ // set up task
+ final ChunkDataTask newTask = new ChunkDataTask(
+ world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority
+ );
+ newTask.inProgressRead = new InProgressRead();
+ newTask.inProgressRead.addToAsyncWaiters(onComplete);
+
+ callbackInfo.tasksNeedsScheduling = true;
+ return newTask;
+ }
+
+ final CompoundTag pendingWrite = running.inProgressWrite;
+
+ if (pendingWrite == ChunkDataTask.NOTHING_TO_WRITE) {
+ // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations
+ if (!running.inProgressRead.addToAsyncWaiters(onComplete)) {
+ callbackInfo.data = running.inProgressRead.value;
+ callbackInfo.throwable = running.inProgressRead.throwable;
+ callbackInfo.completeNow = true;
+ }
+ return running;
+ }
+
+ // at this stage we have to use the in progress write's data to avoid an order issue
+ callbackInfo.data = pendingWrite;
+ callbackInfo.throwable = null;
+ callbackInfo.completeNow = true;
+ return running;
+ };
+
+ final ChunkDataTask ret = taskController.tasks.compute(key, compute);
+
+ // needs to be scheduled
+ if (callbackInfo.tasksNeedsScheduling) {
+ ret.prioritisedTask.queue();
+ } else if (callbackInfo.completeNow) {
+ try {
+ onComplete.accept(callbackInfo.data == null ? null : callbackInfo.data.copy(), callbackInfo.throwable);
+ } catch (final Throwable thr) {
+ LOGGER.error("Callback " + ConcurrentUtil.genericToString(onComplete) + " synchronously failed to handle chunk data for task " + ret.toString(), thr);
+ }
+ } else {
+ // we're waiting on a task we didn't schedule, so raise its priority to what we want
+ ret.prioritisedTask.raisePriority(priority);
+ }
+
+ return new CancellableRead(onComplete, ret);
+ }
+
+ /**
+ * Schedules a load task to be executed asynchronously, and blocks on that task.
+ *
+ * @param world Chunk's world
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param type Regionfile type
+ * @param priority Minimum priority to load the data at.
+ *
+ * @return The chunk data for the chunk. Note that a {@code null} result means the chunk or regionfile does not exist on disk.
+ *
+ * @throws IOException If the load fails for any reason
+ */
+ public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
+ final Priority priority) throws IOException {
+ final CompletableFuture<CompoundTag> ret = new CompletableFuture<>();
+
+ RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> {
+ if (thr != null) {
+ ret.completeExceptionally(thr);
+ } else {
+ ret.complete(compound);
+ }
+ }, true, priority);
+
+ try {
+ return ret.join();
+ } catch (final CompletionException ex) {
+ throw new IOException(ex);
+ }
+ }
+
+ private static final class ImmediateCallbackCompletion {
+
+ public CompoundTag data;
+ public Throwable throwable;
+ public boolean completeNow;
+ public boolean tasksNeedsScheduling;
+
+ }
+
+ private static final class CancellableRead implements Cancellable {
+
+ private BiConsumer<CompoundTag, Throwable> callback;
+ private ChunkDataTask task;
+
+ CancellableRead(final BiConsumer<CompoundTag, Throwable> callback, final ChunkDataTask task) {
+ this.callback = callback;
+ this.task = task;
+ }
+
+ @Override
+ public boolean cancel() {
+ final BiConsumer<CompoundTag, Throwable> callback = this.callback;
+ final ChunkDataTask task = this.task;
+
+ if (callback == null || task == null) {
+ return false;
+ }
+
+ this.callback = null;
+ this.task = null;
+
+ final InProgressRead read = task.inProgressRead;
+
+ // read can be null if no read was scheduled (i.e no regionfile existed or chunk in regionfile didn't)
+ return read != null && read.cancel(callback);
+ }
+ }
+
+ private static final class CancellableReads implements Cancellable {
+
+ private Cancellable[] reads;
+
+ private static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class);
+
+ CancellableReads(final Cancellable[] reads) {
+ this.reads = reads;
+ }
+
+ @Override
+ public boolean cancel() {
+ final Cancellable[] reads = (Cancellable[])READS_HANDLE.getAndSet((CancellableReads)this, (Cancellable[])null);
+
+ if (reads == null) {
+ return false;
+ }
+
+ boolean ret = false;
+
+ for (final Cancellable read : reads) {
+ ret |= read.cancel();
+ }
+
+ return ret;
+ }
+ }
+
+ private static final class InProgressRead {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(InProgressRead.class);
+
+ private CompoundTag value;
+ private Throwable throwable;
+ private final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> callbacks = new MultiThreadedQueue<>();
+
+ public boolean hasNoWaiters() {
+ return this.callbacks.isEmpty();
+ }
+
+ public boolean addToAsyncWaiters(final BiConsumer<CompoundTag, Throwable> callback) {
+ return this.callbacks.add(callback);
+ }
+
+ public boolean cancel(final BiConsumer<CompoundTag, Throwable> callback) {
+ return this.callbacks.remove(callback);
+ }
+
+ public void complete(final ChunkDataTask task, final CompoundTag value, final Throwable throwable) {
+ this.value = value;
+ this.throwable = throwable;
+
+ BiConsumer<CompoundTag, Throwable> consumer;
+ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) {
+ try {
+ consumer.accept(value == null ? null : value.copy(), throwable);
+ } catch (final Throwable thr) {
+ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data for task " + task.toString(), thr);
+ }
+ }
+ }
+ }
+
+ public static abstract class ChunkDataController {
+
+ // ConcurrentHashMap synchronizes per chain, so reduce the chance of task's hashes colliding.
+ private final ConcurrentLong2ReferenceChainedHashTable<ChunkDataTask> tasks = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(8192, 0.5f);
+
+ public final RegionFileType type;
+
+ public ChunkDataController(final RegionFileType type) {
+ this.type = type;
+ }
+
+ public abstract RegionFileStorage getCache();
+
+ public abstract void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException;
+
+ public abstract CompoundTag readData(final int chunkX, final int chunkZ) throws IOException;
+
+ public boolean hasTasks() {
+ return !this.tasks.isEmpty();
+ }
+
+ public boolean doesRegionFileNotExist(final int chunkX, final int chunkZ) {
+ return ((ChunkSystemRegionFileStorage)(Object)this.getCache()).moonrise$doesRegionFileNotExistNoIO(chunkX, chunkZ);
+ }
+
+ public <T> T computeForRegionFile(final int chunkX, final int chunkZ, final boolean existingOnly, final Function<RegionFile, T> function) {
+ final RegionFileStorage cache = this.getCache();
+ final RegionFile regionFile;
+ synchronized (cache) {
+ try {
+ if (existingOnly) {
+ regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfExists(chunkX, chunkZ);
+ } else {
+ regionFile = cache.getRegionFile(new ChunkPos(chunkX, chunkZ), existingOnly);
+ }
+ } catch (final IOException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ return function.apply(regionFile);
+ }
+ }
+
+ public <T> T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function<RegionFile, T> function) {
+ final RegionFileStorage cache = this.getCache();
+ final RegionFile regionFile;
+
+ synchronized (cache) {
+ regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfLoaded(chunkX, chunkZ);
+
+ return function.apply(regionFile);
+ }
+ }
+ }
+
+ private static final class ChunkDataTask implements Runnable {
+
+ private static final CompoundTag NOTHING_TO_WRITE = new CompoundTag();
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkDataTask.class);
+
+ private InProgressRead inProgressRead;
+ private volatile CompoundTag inProgressWrite = NOTHING_TO_WRITE; // only needs to be acquire/release
+
+ private boolean failedWrite;
+
+ private final ServerLevel world;
+ private final int chunkX;
+ private final int chunkZ;
+ private final ChunkDataController taskController;
+
+ private final PrioritisedTask prioritisedTask;
+
+ /*
+ * IO thread will perform reads before writes for a given chunk x and z
+ *
+ * How reads/writes are scheduled:
+ *
+ * If read is scheduled while scheduling write, take no special action and just schedule write
+ * If read is scheduled while scheduling read and no write is scheduled, chain the read task
+ *
+ *
+ * If write is scheduled while scheduling read, use the pending write data and ret immediately (so no read is scheduled)
+ * If write is scheduled while scheduling write (ignore read in progress), overwrite the write in progress data
+ *
+ * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however
+ * it fails to properly propagate write failures thanks to writes overwriting each other
+ */
+
+ public ChunkDataTask(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkDataController taskController,
+ final PrioritisedExecutor executor, final Priority priority) {
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.taskController = taskController;
+ this.prioritisedTask = executor.createTask(this, priority);
+ }
+
+ @Override
+ public String toString() {
+ return "Task for world: '" + WorldUtil.getWorldName(this.world) + "' at (" + this.chunkX + "," + this.chunkZ +
+ ") type: " + this.taskController.type.name() + ", hash: " + this.hashCode();
+ }
+
+ @Override
+ public void run() {
+ final InProgressRead read = this.inProgressRead;
+ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ);
+
+ if (read != null) {
+ final boolean[] canRead = new boolean[] { true };
+
+ if (read.hasNoWaiters()) {
+ // cancelled read? go to task controller to confirm
+ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> {
+ if (valueInMap == null) {
+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
+ }
+ if (valueInMap != ChunkDataTask.this) {
+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
+ }
+
+ if (!read.hasNoWaiters()) {
+ return valueInMap;
+ } else {
+ canRead[0] = false;
+ }
+
+ return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap;
+ });
+
+ if (inMap == null) {
+ // read is cancelled - and no write pending, so we're done
+ return;
+ }
+ // if there is a write in progress, we don't actually have to worry about waiters gaining new entries -
+ // the readers will just use the in progress write, so the value in canRead is good to use without
+ // further synchronisation.
+ }
+
+ if (canRead[0]) {
+ CompoundTag compound = null;
+ Throwable throwable = null;
+
+ try {
+ compound = this.taskController.readData(this.chunkX, this.chunkZ);
+ } catch (final Throwable thr) {
+ throwable = thr;
+ LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr);
+ }
+ read.complete(this, compound, throwable);
+ }
+ }
+
+ CompoundTag write = this.inProgressWrite;
+
+ if (write == NOTHING_TO_WRITE) {
+ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> {
+ if (valueInMap == null) {
+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
+ }
+ if (valueInMap != ChunkDataTask.this) {
+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
+ }
+ return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap;
+ });
+
+ if (inMap == null) {
+ return; // set the task value to null, indicating we're done
+ } // else: inProgressWrite changed, so now we have something to write
+ }
+
+ for (;;) {
+ write = this.inProgressWrite;
+ final CompoundTag dataWritten = write;
+
+ boolean failedWrite = false;
+
+ try {
+ this.taskController.writeData(this.chunkX, this.chunkZ, write);
+ } catch (final Throwable thr) {
+ if (thr instanceof RegionFileStorage.RegionFileSizeException) {
+ final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024);
+ LOGGER.error("Chunk at (" + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk.");
+ } else {
+ failedWrite = thr instanceof IOException;
+ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr);
+ }
+ }
+
+ final boolean finalFailWrite = failedWrite;
+ final boolean[] done = new boolean[] { false };
+
+ this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> {
+ if (valueInMap == null) {
+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
+ }
+ if (valueInMap != ChunkDataTask.this) {
+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
+ }
+ if (valueInMap.inProgressWrite == dataWritten) {
+ valueInMap.failedWrite = finalFailWrite;
+ done[0] = true;
+ // keep the data in map if we failed the write so we can try to prevent data loss
+ return finalFailWrite ? valueInMap : null;
+ }
+ // different data than expected, means we need to retry write
+ return valueInMap;
+ });
+
+ if (done[0]) {
+ return;
+ }
+
+ // fetch & write new data
+ continue;
+ }
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
new file mode 100644
index 0000000000000000000000000000000000000000..c35e0c29700be48dda3e53e7d2db224766ef17b7
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
@@ -0,0 +1,56 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
+
+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
+import ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkStorage;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
+import java.io.IOException;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+
+public final class ChunkDataController extends RegionFileIOThread.ChunkDataController {
+
+ private final ServerLevel world;
+
+ public ChunkDataController(final ServerLevel world) {
+ super(RegionFileIOThread.RegionFileType.CHUNK_DATA);
+ this.world = world;
+ }
+
+ @Override
+ public RegionFileStorage getCache() {
+ return ((ChunkSystemChunkStorage)this.world.getChunkSource().chunkMap).moonrise$getRegionStorage();
+ }
+
+ @Override
+ public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
+ final CompletableFuture<Void> future = this.world.getChunkSource().chunkMap.write(new ChunkPos(chunkX, chunkZ), compound);
+
+ try {
+ if (future != null) {
+ // rets non-null when sync writing (i.e. future should be completed here)
+ future.join();
+ }
+ } catch (final CompletionException ex) {
+ if (ex.getCause() instanceof IOException ioException) {
+ throw ioException;
+ }
+ throw ex;
+ }
+ }
+
+ @Override
+ public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException {
+ try {
+ return this.world.getChunkSource().chunkMap.read(new ChunkPos(chunkX, chunkZ)).join().orElse(null);
+ } catch (final CompletionException ex) {
+ if (ex.getCause() instanceof IOException ioException) {
+ throw ioException;
+ }
+ throw ex;
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
new file mode 100644
index 0000000000000000000000000000000000000000..fdd189ef056187941d43809c5d61cab717aecf60
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
@@ -0,0 +1,55 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
+
+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.storage.EntityStorage;
+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
+import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
+import java.io.IOException;
+import java.nio.file.Path;
+
+public final class EntityDataController extends RegionFileIOThread.ChunkDataController {
+
+ private final EntityRegionFileStorage storage;
+
+ public EntityDataController(final EntityRegionFileStorage storage) {
+ super(RegionFileIOThread.RegionFileType.ENTITY_DATA);
+ this.storage = storage;
+ }
+
+ @Override
+ public RegionFileStorage getCache() {
+ return this.storage;
+ }
+
+ @Override
+ public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
+ this.storage.write(new ChunkPos(chunkX, chunkZ), compound);
+ }
+
+ @Override
+ public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException {
+ return this.storage.read(new ChunkPos(chunkX, chunkZ));
+ }
+
+ public static final class EntityRegionFileStorage extends RegionFileStorage {
+
+ public EntityRegionFileStorage(final RegionStorageInfo regionStorageInfo, final Path directory,
+ final boolean dsync) {
+ super(regionStorageInfo, directory, dsync);
+ }
+
+ @Override
+ public void write(final ChunkPos pos, final CompoundTag nbt) throws IOException {
+ final ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt);
+ if (nbtPos != null && !pos.equals(nbtPos)) {
+ throw new IllegalArgumentException(
+ "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString()
+ + " but compound says coordinate is " + nbtPos + " for world: " + this
+ );
+ }
+ super.write(pos, nbt);
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
new file mode 100644
index 0000000000000000000000000000000000000000..af867f8fedd0bb8f675e94243aa1a3f17363483b
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
@@ -0,0 +1,33 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
+
+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
+import java.io.IOException;
+
+public final class PoiDataController extends RegionFileIOThread.ChunkDataController {
+
+ private final ServerLevel world;
+
+ public PoiDataController(final ServerLevel world) {
+ super(RegionFileIOThread.RegionFileType.POI_DATA);
+ this.world = world;
+ }
+
+ @Override
+ public RegionFileStorage getCache() {
+ return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$getRegionStorage();
+ }
+
+ @Override
+ public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
+ ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$write(chunkX, chunkZ, compound);
+ }
+
+ @Override
+ public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException {
+ return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$read(chunkX, chunkZ);
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
new file mode 100644
index 0000000000000000000000000000000000000000..efcd9057f008f0b9cf0d22b2b21d1851205841e5
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
@@ -0,0 +1,22 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level;
+
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+
+public interface ChunkSystemLevel {
+
+ public EntityLookup moonrise$getEntityLookup();
+
+ public void moonrise$setEntityLookup(final EntityLookup entityLookup);
+
+ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ);
+
+ public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ);
+
+ public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus);
+
+ public void moonrise$midTickTasks();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..0b58701342d573fa43cdd06681534854a0e51d77
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java
@@ -0,0 +1,10 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level;
+
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+
+public interface ChunkSystemLevelReader {
+
+ public ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status);
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
new file mode 100644
index 0000000000000000000000000000000000000000..6828a3ca7151692a41b5370f680f867958a214fc
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
@@ -0,0 +1,50 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
+import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import java.util.List;
+import java.util.function.Consumer;
+
+public interface ChunkSystemServerLevel extends ChunkSystemLevel {
+
+ public ChunkTaskScheduler moonrise$getChunkTaskScheduler();
+
+ public RegionFileIOThread.ChunkDataController moonrise$getChunkDataController();
+
+ public RegionFileIOThread.ChunkDataController moonrise$getPoiChunkDataController();
+
+ public RegionFileIOThread.ChunkDataController moonrise$getEntityChunkDataController();
+
+ public int moonrise$getRegionChunkShift();
+
+ // Paper - marked closing not needed on CB
+
+ public RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader();
+
+ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
+ final PrioritisedExecutor.Priority priority,
+ final Consumer<List<ChunkAccess>> onLoad);
+
+ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
+ final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority,
+ final Consumer<List<ChunkAccess>> onLoad);
+
+ public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
+ final PrioritisedExecutor.Priority priority,
+ final Consumer<List<ChunkAccess>> onLoad);
+
+ public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
+ final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority,
+ final Consumer<List<ChunkAccess>> onLoad);
+
+ public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder();
+
+ public long moonrise$getLastMidTickFailure();
+
+ public void moonrise$setLastMidTickFailure(final long time);
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..7d049d750df88762566f13a9c4fc7574a2df4825
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java
@@ -0,0 +1,26 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
+
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.level.chunk.LevelChunk;
+import java.util.List;
+
+public interface ChunkSystemChunkHolder {
+
+ public NewChunkHolder moonrise$getRealChunkHolder();
+
+ public void moonrise$setRealChunkHolder(final NewChunkHolder newChunkHolder);
+
+ public void moonrise$addReceivedChunk(final ServerPlayer player);
+
+ public void moonrise$removeReceivedChunk(final ServerPlayer player);
+
+ public boolean moonrise$hasChunkBeenSent();
+
+ public boolean moonrise$hasChunkBeenSent(final ServerPlayer to);
+
+ public List<ServerPlayer> moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge);
+
+ public LevelChunk moonrise$getFullChunk();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..f4bc44bb266763345c4e6f859c89352c769a104d
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java
@@ -0,0 +1,26 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
+
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public interface ChunkSystemChunkStatus {
+
+ public boolean moonrise$isParallelCapable();
+
+ public void moonrise$setParallelCapable(final boolean value);
+
+ public int moonrise$getWriteRadius();
+
+ public void moonrise$setWriteRadius(final int value);
+
+ public ChunkStatus moonrise$getNextStatus();
+
+ public boolean moonrise$isEmptyLoadStatus();
+
+ public void moonrise$setEmptyLoadStatus(final boolean value);
+
+ public boolean moonrise$isEmptyGenStatus();
+
+ public AtomicBoolean moonrise$getWarnedAboutNoImmediateComplete();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..883fe6401f1b9711fa544d18a815b4d638f580df
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
+
+import net.minecraft.server.level.ChunkMap;
+
+public interface ChunkSystemDistanceManager {
+
+ public ChunkMap moonrise$getChunkMap();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java
new file mode 100644
index 0000000000000000000000000000000000000000..755b08dd32e568d341ceef8a8aef841831a0781d
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java
@@ -0,0 +1,7 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk;
+
+public interface ChunkSystemLevelChunk {
+
+ public boolean moonrise$isPostProcessingDone();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
new file mode 100644
index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548cc65efa8e
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
@@ -0,0 +1,819 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity;
+
+import ca.spottedleaf.moonrise.common.list.EntityList;
+import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity;
+import com.google.common.collect.ImmutableList;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.ListTag;
+import net.minecraft.nbt.NbtUtils;
+import net.minecraft.nbt.Tag;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.boss.EnderDragonPart;
+import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.chunk.storage.EntityStorage;
+import net.minecraft.world.level.entity.Visibility;
+import net.minecraft.world.phys.AABB;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Predicate;
+import org.bukkit.event.entity.EntityRemoveEvent;
+
+public final class ChunkEntitySlices {
+
+ public final int minSection;
+ public final int maxSection;
+ public final int chunkX;
+ public final int chunkZ;
+ public final Level world;
+
+ private final EntityCollectionBySection allEntities;
+ private final EntityCollectionBySection hardCollidingEntities;
+ private final Reference2ObjectOpenHashMap<Class<? extends Entity>, EntityCollectionBySection> entitiesByClass;
+ private final Reference2ObjectOpenHashMap<EntityType<?>, EntityCollectionBySection> entitiesByType;
+ private final EntityList entities = new EntityList();
+
+ public FullChunkStatus status;
+
+ private boolean isTransient;
+
+ public boolean isTransient() {
+ return this.isTransient;
+ }
+
+ public void setTransient(final boolean value) {
+ this.isTransient = value;
+ }
+
+ public ChunkEntitySlices(final Level world, final int chunkX, final int chunkZ, final FullChunkStatus status,
+ final int minSection, final int maxSection) { // inclusive, inclusive
+ this.minSection = minSection;
+ this.maxSection = maxSection;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.world = world;
+
+ this.allEntities = new EntityCollectionBySection(this);
+ this.hardCollidingEntities = new EntityCollectionBySection(this);
+ this.entitiesByClass = new Reference2ObjectOpenHashMap<>();
+ this.entitiesByType = new Reference2ObjectOpenHashMap<>();
+
+ this.status = status;
+ }
+
+ public static List<Entity> readEntities(final ServerLevel world, final CompoundTag compoundTag) {
+ // TODO check this and below on update for format changes
+ return EntityType.loadEntitiesRecursive(compoundTag.getList("Entities", 10), world).collect(ImmutableList.toImmutableList());
+ }
+
+ // Paper start - rewrite chunk system
+ public static void copyEntities(final CompoundTag from, final CompoundTag into) {
+ if (from == null) {
+ return;
+ }
+ final ListTag entitiesFrom = from.getList("Entities", Tag.TAG_COMPOUND);
+ if (entitiesFrom == null || entitiesFrom.isEmpty()) {
+ return;
+ }
+
+ final ListTag entitiesInto = into.getList("Entities", Tag.TAG_COMPOUND);
+ into.put("Entities", entitiesInto); // this is in case into doesn't have any entities
+ entitiesInto.addAll(0, entitiesFrom);
+ }
+
+ public static CompoundTag saveEntityChunk(final List<Entity> entities, final ChunkPos chunkPos, final ServerLevel world) {
+ return saveEntityChunk0(entities, chunkPos, world, false);
+ }
+
+ public static CompoundTag saveEntityChunk0(final List<Entity> entities, final ChunkPos chunkPos, final ServerLevel world, final boolean force) {
+ if (!force && entities.isEmpty()) {
+ return null;
+ }
+
+ final ListTag entitiesTag = new ListTag();
+ for (final Entity entity : entities) {
+ CompoundTag compoundTag = new CompoundTag();
+ if (entity.save(compoundTag)) {
+ entitiesTag.add(compoundTag);
+ }
+ }
+ final CompoundTag ret = NbtUtils.addCurrentDataVersion(new CompoundTag());
+ ret.put("Entities", entitiesTag);
+ EntityStorage.writeChunkPos(ret, chunkPos);
+
+ return !force && entitiesTag.isEmpty() ? null : ret;
+ }
+
+ public CompoundTag save() {
+ final int len = this.entities.size();
+ if (len == 0) {
+ return null;
+ }
+
+ final Entity[] rawData = this.entities.getRawData();
+ final List<Entity> collectedEntities = new ArrayList<>(len);
+ for (int i = 0; i < len; ++i) {
+ final Entity entity = rawData[i];
+ if (entity.shouldBeSaved()) {
+ collectedEntities.add(entity);
+ }
+ }
+
+ if (collectedEntities.isEmpty()) {
+ return null;
+ }
+
+ return saveEntityChunk(collectedEntities, new ChunkPos(this.chunkX, this.chunkZ), (ServerLevel)this.world);
+ }
+
+ // returns true if this chunk has transient entities remaining
+ public boolean unload() {
+ final int len = this.entities.size();
+ final Entity[] collectedEntities = Arrays.copyOf(this.entities.getRawData(), len);
+
+ for (int i = 0; i < len; ++i) {
+ final Entity entity = collectedEntities[i];
+ if (entity.isRemoved()) {
+ // removed by us below
+ continue;
+ }
+ if (entity.shouldBeSaved()) {
+ entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD);
+ if (entity.isVehicle()) {
+ // we cannot assume that these entities are contained within this chunk, because entities can
+ // desync - so we need to remove them all
+ for (final Entity passenger : entity.getIndirectPassengers()) {
+ passenger.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD);
+ }
+ }
+ }
+ }
+
+ return this.entities.size() != 0;
+ }
+
+ // Paper start
+ public org.bukkit.entity.Entity[] getChunkEntities() {
+ List<org.bukkit.entity.Entity> ret = new java.util.ArrayList<>();
+ final Entity[] entities = this.entities.getRawData();
+ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) {
+ final Entity entity = entities[i];
+ if (entity == null) {
+ continue;
+ }
+ final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity();
+ if (bukkit != null && bukkit.isValid()) {
+ ret.add(bukkit);
+ }
+ }
+
+ return ret.toArray(new org.bukkit.entity.Entity[0]);
+ }
+
+ public void callEntitiesLoadEvent() {
+ org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesLoadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities());
+ }
+
+ public void callEntitiesUnloadEvent() {
+ org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesUnloadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities());
+ }
+ // Paper end
+
+ private List<Entity> getAllEntities() {
+ final int len = this.entities.size();
+ if (len == 0) {
+ return new ArrayList<>();
+ }
+
+ final Entity[] rawData = this.entities.getRawData();
+ final List<Entity> collectedEntities = new ArrayList<>(len);
+ for (int i = 0; i < len; ++i) {
+ collectedEntities.add(rawData[i]);
+ }
+
+ return collectedEntities;
+ }
+
+ public boolean isEmpty() {
+ return this.entities.size() == 0;
+ }
+
+ public void mergeInto(final ChunkEntitySlices slices) {
+ final Entity[] entities = this.entities.getRawData();
+ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) {
+ final Entity entity = entities[i];
+ slices.addEntity(entity, ((ChunkSystemEntity)entity).moonrise$getSectionY());
+ }
+ }
+
+ private boolean preventStatusUpdates;
+ public boolean startPreventingStatusUpdates() {
+ final boolean ret = this.preventStatusUpdates;
+ this.preventStatusUpdates = true;
+ return ret;
+ }
+
+ public boolean isPreventingStatusUpdates() {
+ return this.preventStatusUpdates;
+ }
+
+ public void stopPreventingStatusUpdates(final boolean prev) {
+ this.preventStatusUpdates = prev;
+ }
+
+ public void updateStatus(final FullChunkStatus status, final EntityLookup lookup) {
+ this.status = status;
+
+ final Entity[] entities = this.entities.getRawData();
+
+ for (int i = 0, size = this.entities.size(); i < size; ++i) {
+ final Entity entity = entities[i];
+
+ final Visibility oldVisibility = EntityLookup.getEntityStatus(entity);
+ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(status);
+ final Visibility newVisibility = EntityLookup.getEntityStatus(entity);
+
+ lookup.entityStatusChange(entity, this, oldVisibility, newVisibility, false, false, false);
+ }
+ }
+
+ public boolean addEntity(final Entity entity, final int chunkSection) {
+ if (!this.entities.add(entity)) {
+ return false;
+ }
+ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(this.status);
+ final int sectionIndex = chunkSection - this.minSection;
+
+ this.allEntities.addEntity(entity, sectionIndex);
+
+ if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) {
+ this.hardCollidingEntities.addEntity(entity, sectionIndex);
+ }
+
+ for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator =
+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
+ final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next();
+
+ if (entry.getKey().isInstance(entity)) {
+ entry.getValue().addEntity(entity, sectionIndex);
+ }
+ }
+
+ EntityCollectionBySection byType = this.entitiesByType.get(entity.getType());
+ if (byType != null) {
+ byType.addEntity(entity, sectionIndex);
+ } else {
+ this.entitiesByType.put(entity.getType(), byType = new EntityCollectionBySection(this));
+ byType.addEntity(entity, sectionIndex);
+ }
+
+ return true;
+ }
+
+ public boolean removeEntity(final Entity entity, final int chunkSection) {
+ if (!this.entities.remove(entity)) {
+ return false;
+ }
+ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(null);
+ final int sectionIndex = chunkSection - this.minSection;
+
+ this.allEntities.removeEntity(entity, sectionIndex);
+
+ if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) {
+ this.hardCollidingEntities.removeEntity(entity, sectionIndex);
+ }
+
+ for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator =
+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
+ final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next();
+
+ if (entry.getKey().isInstance(entity)) {
+ entry.getValue().removeEntity(entity, sectionIndex);
+ }
+ }
+
+ final EntityCollectionBySection byType = this.entitiesByType.get(entity.getType());
+ byType.removeEntity(entity, sectionIndex);
+
+ return true;
+ }
+
+ public void getHardCollidingEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ this.hardCollidingEntities.getEntities(except, box, into, predicate);
+ }
+
+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate);
+ }
+
+ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ this.allEntities.getEntities(except, box, into, predicate);
+ }
+
+
+ public boolean getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
+ final int maxCount) {
+ return this.allEntities.getEntitiesWithEnderDragonPartsLimited(except, box, into, predicate, maxCount);
+ }
+
+ public boolean getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
+ final int maxCount) {
+ return this.allEntities.getEntitiesLimited(except, box, into, predicate, maxCount);
+ }
+
+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate) {
+ final EntityCollectionBySection byType = this.entitiesByType.get(type);
+
+ if (byType != null) {
+ byType.getEntities((Entity)null, box, (List)into, (Predicate) predicate);
+ }
+ }
+
+ public <T extends Entity> boolean getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate, final int maxCount) {
+ final EntityCollectionBySection byType = this.entitiesByType.get(type);
+
+ if (byType != null) {
+ return byType.getEntitiesLimited((Entity)null, box, (List)into, (Predicate)predicate, maxCount);
+ }
+
+ return false;
+ }
+
+ protected EntityCollectionBySection initClass(final Class<? extends Entity> clazz) {
+ final EntityCollectionBySection ret = new EntityCollectionBySection(this);
+
+ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) {
+ final BasicEntityList<Entity> sectionEntities = this.allEntities.entitiesBySection[sectionIndex];
+ if (sectionEntities == null) {
+ continue;
+ }
+
+ final Entity[] storage = sectionEntities.storage;
+
+ for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) {
+ final Entity entity = storage[i];
+
+ if (clazz.isInstance(entity)) {
+ ret.addEntity(entity, sectionIndex);
+ }
+ }
+ }
+
+ return ret;
+ }
+
+ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate) {
+ EntityCollectionBySection collection = this.entitiesByClass.get(clazz);
+ if (collection != null) {
+ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate);
+ } else {
+ this.entitiesByClass.put(clazz, collection = this.initClass(clazz));
+ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate);
+ }
+ }
+
+ public <T extends Entity> boolean getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate, final int maxCount) {
+ EntityCollectionBySection collection = this.entitiesByClass.get(clazz);
+ if (collection != null) {
+ return collection.getEntitiesWithEnderDragonPartsLimited(except, clazz, box, (List)into, (Predicate)predicate, maxCount);
+ } else {
+ this.entitiesByClass.put(clazz, collection = this.initClass(clazz));
+ return collection.getEntitiesWithEnderDragonPartsLimited(except, clazz, box, (List)into, (Predicate)predicate, maxCount);
+ }
+ }
+
+ private static final class BasicEntityList<E extends Entity> {
+
+ private static final Entity[] EMPTY = new Entity[0];
+ private static final int DEFAULT_CAPACITY = 4;
+
+ private E[] storage;
+ private int size;
+
+ public BasicEntityList() {
+ this(0);
+ }
+
+ public BasicEntityList(final int cap) {
+ this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]);
+ }
+
+ public boolean isEmpty() {
+ return this.size == 0;
+ }
+
+ public int size() {
+ return this.size;
+ }
+
+ private void resize() {
+ if (this.storage == EMPTY) {
+ this.storage = (E[])new Entity[DEFAULT_CAPACITY];
+ } else {
+ this.storage = Arrays.copyOf(this.storage, this.storage.length * 2);
+ }
+ }
+
+ public void add(final E entity) {
+ final int idx = this.size++;
+ if (idx >= this.storage.length) {
+ this.resize();
+ this.storage[idx] = entity;
+ } else {
+ this.storage[idx] = entity;
+ }
+ }
+
+ public int indexOf(final E entity) {
+ final E[] storage = this.storage;
+
+ for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) {
+ if (storage[i] == entity) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ public boolean remove(final E entity) {
+ final int idx = this.indexOf(entity);
+ if (idx == -1) {
+ return false;
+ }
+
+ final int size = --this.size;
+ final E[] storage = this.storage;
+ if (idx != size) {
+ System.arraycopy(storage, idx + 1, storage, idx, size - idx);
+ }
+
+ storage[size] = null;
+
+ return true;
+ }
+
+ public boolean has(final E entity) {
+ return this.indexOf(entity) != -1;
+ }
+ }
+
+ private static final class EntityCollectionBySection {
+
+ private final ChunkEntitySlices slices;
+ private final BasicEntityList<Entity>[] entitiesBySection;
+ private int count;
+
+ public EntityCollectionBySection(final ChunkEntitySlices slices) {
+ this.slices = slices;
+
+ final int sectionCount = slices.maxSection - slices.minSection + 1;
+
+ this.entitiesBySection = new BasicEntityList[sectionCount];
+ }
+
+ public void addEntity(final Entity entity, final int sectionIndex) {
+ BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex];
+
+ if (list != null && list.has(entity)) {
+ return;
+ }
+
+ if (list == null) {
+ this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>();
+ }
+
+ list.add(entity);
+ ++this.count;
+ }
+
+ public void removeEntity(final Entity entity, final int sectionIndex) {
+ final BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex];
+
+ if (list == null || !list.remove(entity)) {
+ return;
+ }
+
+ --this.count;
+
+ if (list.isEmpty()) {
+ this.entitiesBySection[sectionIndex] = null;
+ }
+ }
+
+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ if (this.count == 0) {
+ return;
+ }
+
+ final int minSection = this.slices.minSection;
+ final int maxSection = this.slices.maxSection;
+
+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+ for (int section = min; section <= max; ++section) {
+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+ if (list == null) {
+ continue;
+ }
+
+ final Entity[] storage = list.storage;
+
+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+ final Entity entity = storage[i];
+
+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate != null && !predicate.test(entity)) {
+ continue;
+ }
+
+ into.add(entity);
+ }
+ }
+ }
+
+ public boolean getEntitiesLimited(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
+ final int maxCount) {
+ if (this.count == 0) {
+ return false;
+ }
+
+ final int minSection = this.slices.minSection;
+ final int maxSection = this.slices.maxSection;
+
+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+ for (int section = min; section <= max; ++section) {
+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+ if (list == null) {
+ continue;
+ }
+
+ final Entity[] storage = list.storage;
+
+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+ final Entity entity = storage[i];
+
+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate != null && !predicate.test(entity)) {
+ continue;
+ }
+
+ into.add(entity);
+ if (into.size() >= maxCount) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public void getEntitiesWithEnderDragonParts(final Entity except, final AABB box, final List<Entity> into,
+ final Predicate<? super Entity> predicate) {
+ if (this.count == 0) {
+ return;
+ }
+
+ final int minSection = this.slices.minSection;
+ final int maxSection = this.slices.maxSection;
+
+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+ for (int section = min; section <= max; ++section) {
+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+ if (list == null) {
+ continue;
+ }
+
+ final Entity[] storage = list.storage;
+
+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+ final Entity entity = storage[i];
+
+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate == null || predicate.test(entity)) {
+ into.add(entity);
+ } // else: continue to test the ender dragon parts
+
+ if (entity instanceof EnderDragon) {
+ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) {
+ if (part == except || !part.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate != null && !predicate.test(part)) {
+ continue;
+ }
+
+ into.add(part);
+ }
+ }
+ }
+ }
+ }
+
+ public boolean getEntitiesWithEnderDragonPartsLimited(final Entity except, final AABB box, final List<Entity> into,
+ final Predicate<? super Entity> predicate, final int maxCount) {
+ if (this.count == 0) {
+ return false;
+ }
+
+ final int minSection = this.slices.minSection;
+ final int maxSection = this.slices.maxSection;
+
+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+ for (int section = min; section <= max; ++section) {
+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+ if (list == null) {
+ continue;
+ }
+
+ final Entity[] storage = list.storage;
+
+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+ final Entity entity = storage[i];
+
+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate == null || predicate.test(entity)) {
+ into.add(entity);
+ if (into.size() >= maxCount) {
+ return true;
+ }
+ } // else: continue to test the ender dragon parts
+
+ if (entity instanceof EnderDragon) {
+ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) {
+ if (part == except || !part.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate != null && !predicate.test(part)) {
+ continue;
+ }
+
+ into.add(part);
+ if (into.size() >= maxCount) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public void getEntitiesWithEnderDragonParts(final Entity except, final Class<?> clazz, final AABB box, final List<Entity> into,
+ final Predicate<? super Entity> predicate) {
+ if (this.count == 0) {
+ return;
+ }
+
+ final int minSection = this.slices.minSection;
+ final int maxSection = this.slices.maxSection;
+
+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+ for (int section = min; section <= max; ++section) {
+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+ if (list == null) {
+ continue;
+ }
+
+ final Entity[] storage = list.storage;
+
+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+ final Entity entity = storage[i];
+
+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate == null || predicate.test(entity)) {
+ into.add(entity);
+ } // else: continue to test the ender dragon parts
+
+ if (entity instanceof EnderDragon) {
+ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) {
+ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) {
+ continue;
+ }
+
+ if (predicate != null && !predicate.test(part)) {
+ continue;
+ }
+
+ into.add(part);
+ }
+ }
+ }
+ }
+ }
+
+ public boolean getEntitiesWithEnderDragonPartsLimited(final Entity except, final Class<?> clazz, final AABB box, final List<Entity> into,
+ final Predicate<? super Entity> predicate, final int maxCount) {
+ if (this.count == 0) {
+ return false;
+ }
+
+ final int minSection = this.slices.minSection;
+ final int maxSection = this.slices.maxSection;
+
+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+ for (int section = min; section <= max; ++section) {
+ final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+ if (list == null) {
+ continue;
+ }
+
+ final Entity[] storage = list.storage;
+
+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+ final Entity entity = storage[i];
+
+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
+ continue;
+ }
+
+ if (predicate == null || predicate.test(entity)) {
+ into.add(entity);
+ if (into.size() >= maxCount) {
+ return true;
+ }
+ } // else: continue to test the ender dragon parts
+
+ if (entity instanceof EnderDragon) {
+ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) {
+ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) {
+ continue;
+ }
+
+ if (predicate != null && !predicate.test(part)) {
+ continue;
+ }
+
+ into.add(part);
+ if (into.size() >= maxCount) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
new file mode 100644
index 0000000000000000000000000000000000000000..f6a3eb3d1bb070bcc74133818682571d520d9894
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
@@ -0,0 +1,1044 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity;
+
+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
+import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
+import ca.spottedleaf.moonrise.common.list.EntityList;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity;
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.util.AbortableIterationConsumer;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.entity.EntityInLevelCallback;
+import net.minecraft.world.level.entity.EntityTypeTest;
+import net.minecraft.world.level.entity.LevelCallback;
+import net.minecraft.world.level.entity.LevelEntityGetter;
+import net.minecraft.world.level.entity.Visibility;
+import net.minecraft.world.phys.AABB;
+import net.minecraft.world.phys.Vec3;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+public abstract class EntityLookup implements LevelEntityGetter<Entity> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(EntityLookup.class);
+
+ protected static final int REGION_SHIFT = 5;
+ protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1;
+ protected static final int REGION_SIZE = 1 << REGION_SHIFT;
+
+ public final Level world;
+
+ protected final SWMRLong2ObjectHashTable<ChunkSlicesRegion> regions = new SWMRLong2ObjectHashTable<>(128, 0.5f);
+
+ protected final int minSection; // inclusive
+ protected final int maxSection; // inclusive
+ protected final LevelCallback<Entity> worldCallback;
+
+ protected final ConcurrentLong2ReferenceChainedHashTable<Entity> entityById = new ConcurrentLong2ReferenceChainedHashTable<>();
+ protected final ConcurrentHashMap<UUID, Entity> entityByUUID = new ConcurrentHashMap<>();
+ protected final EntityList accessibleEntities = new EntityList();
+
+ public EntityLookup(final Level world, final LevelCallback<Entity> worldCallback) {
+ this.world = world;
+ this.minSection = WorldUtil.getMinSection(world);
+ this.maxSection = WorldUtil.getMaxSection(world);
+ this.worldCallback = worldCallback;
+ }
+
+ protected abstract Boolean blockTicketUpdates();
+
+ protected abstract void setBlockTicketUpdates(final Boolean value);
+
+ protected abstract void checkThread(final int chunkX, final int chunkZ, final String reason);
+
+ protected abstract void checkThread(final Entity entity, final String reason);
+
+ protected abstract ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk);
+
+ protected abstract void onEmptySlices(final int chunkX, final int chunkZ);
+
+ private static Entity maskNonAccessible(final Entity entity) {
+ if (entity == null) {
+ return null;
+ }
+ final Visibility visibility = EntityLookup.getEntityStatus(entity);
+ return visibility.isAccessible() ? entity : null;
+ }
+
+ @Override
+ public Entity get(final int id) {
+ return maskNonAccessible(this.entityById.get((long)id));
+ }
+
+ @Override
+ public Entity get(final UUID id) {
+ return maskNonAccessible(id == null ? null : this.entityByUUID.get(id));
+ }
+
+ public boolean hasEntity(final UUID uuid) {
+ return this.get(uuid) != null;
+ }
+
+ public String getDebugInfo() {
+ return "count_id:" + this.entityById.size() + ",count_uuid:" + this.entityByUUID.size() + ",count_accessible:" + this.getEntityCount() + ",region_count:" + this.regions.size();
+ }
+
+ protected static final class ArrayIterable<T> implements Iterable<T> {
+
+ private final T[] array;
+ private final int off;
+ private final int length;
+
+ public ArrayIterable(final T[] array, final int off, final int length) {
+ this.array = array;
+ this.off = off;
+ this.length = length;
+ if (length > array.length) {
+ throw new IllegalArgumentException("Length must be no greater-than the array length");
+ }
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ return new ArrayIterator<>(this.array, this.off, this.length);
+ }
+
+ protected static final class ArrayIterator<T> implements Iterator<T> {
+
+ private final T[] array;
+ private int off;
+ private final int length;
+
+ public ArrayIterator(final T[] array, final int off, final int length) {
+ this.array = array;
+ this.off = off;
+ this.length = length;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return this.off < this.length;
+ }
+
+ @Override
+ public T next() {
+ if (this.off >= this.length) {
+ throw new NoSuchElementException();
+ }
+ return this.array[this.off++];
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+
+ @Override
+ public Iterable<Entity> getAll() {
+ synchronized (this.accessibleEntities) {
+ final int len = this.accessibleEntities.size();
+ final Entity[] cpy = Arrays.copyOf(this.accessibleEntities.getRawData(), len, Entity[].class);
+
+ Objects.checkFromToIndex(0, len, cpy.length);
+
+ return new ArrayIterable<>(cpy, 0, len);
+ }
+ }
+
+ public int getEntityCount() {
+ synchronized (this.accessibleEntities) {
+ return this.accessibleEntities.size();
+ }
+ }
+
+ public Entity[] getAllCopy() {
+ synchronized (this.accessibleEntities) {
+ return Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size(), Entity[].class);
+ }
+ }
+
+ @Override
+ public <U extends Entity> void get(final EntityTypeTest<Entity, U> filter, final AbortableIterationConsumer<U> action) {
+ for (final Iterator<Entity> iterator = this.entityById.valueIterator(); iterator.hasNext();) {
+ final Entity entity = iterator.next();
+ final Visibility visibility = EntityLookup.getEntityStatus(entity);
+ if (!visibility.isAccessible()) {
+ continue;
+ }
+ final U casted = filter.tryCast(entity);
+ if (casted != null && action.accept(casted).shouldAbort()) {
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void get(final AABB box, final Consumer<Entity> action) {
+ List<Entity> entities = new ArrayList<>();
+ this.getEntitiesWithoutDragonParts(null, box, entities, null);
+ for (int i = 0, len = entities.size(); i < len; ++i) {
+ action.accept(entities.get(i));
+ }
+ }
+
+ @Override
+ public <U extends Entity> void get(final EntityTypeTest<Entity, U> filter, final AABB box, final AbortableIterationConsumer<U> action) {
+ List<Entity> entities = new ArrayList<>();
+ this.getEntitiesWithoutDragonParts(null, box, entities, null);
+ for (int i = 0, len = entities.size(); i < len; ++i) {
+ final U casted = filter.tryCast(entities.get(i));
+ if (casted != null && action.accept(casted).shouldAbort()) {
+ break;
+ }
+ }
+ }
+
+ public void entityStatusChange(final Entity entity, final ChunkEntitySlices slices, final Visibility oldVisibility, final Visibility newVisibility, final boolean moved,
+ final boolean created, final boolean destroyed) {
+ this.checkThread(entity, "Entity status change must only happen on the main thread");
+
+ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) {
+ // recursive status update
+ LOGGER.error("Cannot recursively update entity chunk status for entity " + entity, new Throwable());
+ return;
+ }
+
+ final boolean entityStatusUpdateBefore = slices == null ? false : slices.startPreventingStatusUpdates();
+
+ if (entityStatusUpdateBefore) {
+ LOGGER.error("Cannot update chunk status for entity " + entity + " since entity chunk (" + slices.chunkX + "," + slices.chunkZ + ") is receiving update", new Throwable());
+ return;
+ }
+
+ try {
+ final Boolean ticketBlockBefore = this.blockTicketUpdates();
+ try {
+ ((ChunkSystemEntity)entity).moonrise$setUpdatingSectionStatus(true);
+ try {
+ if (created) {
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onCreated(entity);
+ }
+ }
+
+ if (oldVisibility == newVisibility) {
+ if (moved && newVisibility.isAccessible()) {
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onSectionChange(entity);
+ }
+ }
+ return;
+ }
+
+ if (newVisibility.ordinal() > oldVisibility.ordinal()) {
+ // status upgrade
+ if (!oldVisibility.isAccessible() && newVisibility.isAccessible()) {
+ synchronized (this.accessibleEntities) {
+ this.accessibleEntities.add(entity);
+ }
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onTrackingStart(entity);
+ }
+ }
+
+ if (!oldVisibility.isTicking() && newVisibility.isTicking()) {
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onTickingStart(entity);
+ }
+ }
+ } else {
+ // status downgrade
+ if (oldVisibility.isTicking() && !newVisibility.isTicking()) {
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onTickingEnd(entity);
+ }
+ }
+
+ if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) {
+ synchronized (this.accessibleEntities) {
+ this.accessibleEntities.remove(entity);
+ }
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onTrackingEnd(entity);
+ }
+ }
+ }
+
+ if (moved && newVisibility.isAccessible()) {
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onSectionChange(entity);
+ }
+ }
+
+ if (destroyed) {
+ if (EntityLookup.this.worldCallback != null) {
+ EntityLookup.this.worldCallback.onDestroyed(entity);
+ }
+ }
+ } finally {
+ ((ChunkSystemEntity)entity).moonrise$setUpdatingSectionStatus(false);
+ }
+ } finally {
+ this.setBlockTicketUpdates(ticketBlockBefore);
+ }
+ } finally {
+ if (slices != null) {
+ slices.stopPreventingStatusUpdates(false);
+ }
+ }
+ }
+
+ public void chunkStatusChange(final int x, final int z, final FullChunkStatus newStatus) {
+ this.getChunk(x, z).updateStatus(newStatus, this);
+ }
+
+ public void addLegacyChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
+ this.addEntityChunk(entities, forChunk, true);
+ }
+
+ public void addEntityChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
+ this.addEntityChunk(entities, forChunk, true);
+ }
+
+ public void addWorldGenChunkEntities(final List<Entity> entities, final ChunkPos forChunk) {
+ this.addEntityChunk(entities, forChunk, false);
+ }
+
+ protected void addRecursivelySafe(final Entity root, final boolean fromDisk) {
+ if (!this.addEntity(root, fromDisk)) {
+ // possible we are a passenger, and so should dismount from any valid entity in the world
+ root.stopRiding();
+ return;
+ }
+ for (final Entity passenger : root.getPassengers()) {
+ this.addRecursivelySafe(passenger, fromDisk);
+ }
+ }
+
+ protected void addEntityChunk(final List<Entity> entities, final ChunkPos forChunk, final boolean fromDisk) {
+ for (int i = 0, len = entities.size(); i < len; ++i) {
+ final Entity entity = entities.get(i);
+ if (entity.isPassenger()) {
+ continue;
+ }
+
+ if (forChunk != null && !entity.chunkPosition().equals(forChunk)) {
+ LOGGER.warn("Root entity " + entity + " is outside of serialized chunk " + forChunk);
+ // can't set removed here, as we may not own the chunk position
+ // skip the entity
+ continue;
+ }
+
+ final Vec3 rootPosition = entity.position();
+
+ // always adjust positions before adding passengers in case plugins access the entity, and so that
+ // they are added to the right entity chunk
+ for (final Entity passenger : entity.getIndirectPassengers()) {
+ if (forChunk != null && !passenger.chunkPosition().equals(forChunk)) {
+ passenger.setPosRaw(rootPosition.x, rootPosition.y, rootPosition.z);
+ }
+ }
+
+ this.addRecursivelySafe(entity, fromDisk);
+ }
+ }
+
+ public boolean addNewEntity(final Entity entity) {
+ return this.addEntity(entity, false);
+ }
+
+ public static Visibility getEntityStatus(final Entity entity) {
+ if (entity.isAlwaysTicking()) {
+ return Visibility.TICKING;
+ }
+ final FullChunkStatus entityStatus = ((ChunkSystemEntity)entity).moonrise$getChunkStatus();
+ return Visibility.fromFullChunkStatus(entityStatus == null ? FullChunkStatus.INACCESSIBLE : entityStatus);
+ }
+
+ protected boolean addEntity(final Entity entity, final boolean fromDisk) {
+ final BlockPos pos = entity.blockPosition();
+ final int sectionX = pos.getX() >> 4;
+ final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection);
+ final int sectionZ = pos.getZ() >> 4;
+ this.checkThread(sectionX, sectionZ, "Cannot add entity off-main thread");
+
+ if (entity.isRemoved()) {
+ LOGGER.warn("Refusing to add removed entity: " + entity);
+ return false;
+ }
+
+ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) {
+ LOGGER.warn("Entity " + entity + " is currently prevented from being added/removed to world since it is processing section status updates", new Throwable());
+ return false;
+ }
+
+ Entity currentlyMapped = this.entityById.putIfAbsent((long)entity.getId(), entity);
+ if (currentlyMapped != null) {
+ LOGGER.warn("Entity id already exists: " + entity.getId() + ", mapped to " + currentlyMapped + ", can't add " + entity);
+ return false;
+ }
+
+ currentlyMapped = this.entityByUUID.putIfAbsent(entity.getUUID(), entity);
+ if (currentlyMapped != null) {
+ // need to remove mapping for id
+ this.entityById.remove((long)entity.getId(), entity);
+ LOGGER.warn("Entity uuid already exists: " + entity.getUUID() + ", mapped to " + currentlyMapped + ", can't add " + entity);
+ return false;
+ }
+
+ ((ChunkSystemEntity)entity).moonrise$setSectionX(sectionX);
+ ((ChunkSystemEntity)entity).moonrise$setSectionY(sectionY);
+ ((ChunkSystemEntity)entity).moonrise$setSectionZ(sectionZ);
+ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ);
+ if (!slices.addEntity(entity, sectionY)) {
+ LOGGER.warn("Entity " + entity + " added to world '" + WorldUtil.getWorldName(this.world) + "', but was already contained in entity chunk (" + sectionX + "," + sectionZ + ")");
+ }
+
+ entity.setLevelCallback(new EntityCallback(entity));
+
+ this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false);
+
+ return true;
+ }
+
+ public boolean canRemoveEntity(final Entity entity) {
+ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) {
+ return false;
+ }
+
+ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
+ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ();
+ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ);
+ return slices == null || !slices.isPreventingStatusUpdates();
+ }
+
+ protected void removeEntity(final Entity entity) {
+ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
+ final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY();
+ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ();
+ this.checkThread(sectionX, sectionZ, "Cannot remove entity off-main");
+ if (!entity.isRemoved()) {
+ throw new IllegalStateException("Only call Entity#setRemoved to remove an entity");
+ }
+ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ);
+ // all entities should be in a chunk
+ if (slices == null) {
+ LOGGER.warn("Cannot remove entity " + entity + " from null entity slices (" + sectionX + "," + sectionZ + ")");
+ } else {
+ if (slices.isPreventingStatusUpdates()) {
+ throw new IllegalStateException("Attempting to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ") that is receiving status updates");
+ }
+ if (!slices.removeEntity(entity, sectionY)) {
+ LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")");
+ }
+ }
+ ((ChunkSystemEntity)entity).moonrise$setSectionX(Integer.MIN_VALUE);
+ ((ChunkSystemEntity)entity).moonrise$setSectionY(Integer.MIN_VALUE);
+ ((ChunkSystemEntity)entity).moonrise$setSectionZ(Integer.MIN_VALUE);
+
+
+ Entity currentlyMapped;
+ if ((currentlyMapped = this.entityById.remove(entity.getId(), entity)) != entity) {
+ LOGGER.warn("Failed to remove entity " + entity + " by id, current entity mapped: " + currentlyMapped);
+ }
+
+ Entity[] currentlyMappedArr = new Entity[1];
+
+ // need reference equality
+ this.entityByUUID.compute(entity.getUUID(), (final UUID keyInMap, final Entity valueInMap) -> {
+ currentlyMappedArr[0] = valueInMap;
+ if (valueInMap != entity) {
+ return valueInMap;
+ }
+ return null;
+ });
+
+ if (currentlyMappedArr[0] != entity) {
+ LOGGER.warn("Failed to remove entity " + entity + " by uuid, current entity mapped: " + currentlyMappedArr[0]);
+ }
+
+ if (slices != null && slices.isEmpty()) {
+ this.onEmptySlices(sectionX, sectionZ);
+ }
+ }
+
+ protected ChunkEntitySlices moveEntity(final Entity entity) {
+ // ensure we own the entity
+ this.checkThread(entity, "Cannot move entity off-main");
+
+ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
+ final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY();
+ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ();
+ final BlockPos newPos = entity.blockPosition();
+ final int newSectionX = newPos.getX() >> 4;
+ final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection);
+ final int newSectionZ = newPos.getZ() >> 4;
+
+ if (newSectionX == sectionX && newSectionY == sectionY && newSectionZ == sectionZ) {
+ return null;
+ }
+
+ // ensure the new section is owned by this tick thread
+ this.checkThread(newSectionX, newSectionZ, "Cannot move entity off-main");
+
+ // ensure the old section is owned by this tick thread
+ this.checkThread(sectionX, sectionZ, "Cannot move entity off-main");
+
+ final ChunkEntitySlices old = this.getChunk(sectionX, sectionZ);
+ final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ);
+
+ if (!old.removeEntity(entity, sectionY)) {
+ LOGGER.warn("Could not remove entity " + entity + " from its old chunk section (" + sectionX + "," + sectionY + "," + sectionZ + ") since it was not contained in the section");
+ }
+
+ if (!slices.addEntity(entity, newSectionY)) {
+ LOGGER.warn("Could not add entity " + entity + " to its new chunk section (" + newSectionX + "," + newSectionY + "," + newSectionZ + ") as it is already contained in the section");
+ }
+
+ ((ChunkSystemEntity)entity).moonrise$setSectionX(newSectionX);
+ ((ChunkSystemEntity)entity).moonrise$setSectionY(newSectionY);
+ ((ChunkSystemEntity)entity).moonrise$setSectionZ(newSectionZ);
+
+ if (old.isEmpty()) {
+ this.onEmptySlices(sectionX, sectionZ);
+ }
+
+ return slices;
+ }
+
+ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ chunk.getEntitiesWithoutDragonParts(except, box, into, predicate);
+ }
+ }
+ }
+ }
+ }
+
+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ chunk.getEntities(except, box, into, predicate);
+ }
+ }
+ }
+ }
+ }
+
+ public void getHardCollidingEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ chunk.getHardCollidingEntities(except, box, into, predicate);
+ }
+ }
+ }
+ }
+ }
+
+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ chunk.getEntities(type, box, (List)into, (Predicate)predicate);
+ }
+ }
+ }
+ }
+ }
+
+ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ chunk.getEntities(clazz, except, box, into, predicate);
+ }
+ }
+ }
+ }
+ }
+
+ //////// Limited ////////
+
+ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
+ final int maxCount) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ if (chunk.getEntitiesWithoutDragonParts(except, box, into, predicate, maxCount)) {
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate,
+ final int maxCount) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ if (chunk.getEntities(except, box, into, predicate, maxCount)) {
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate, final int maxCount) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ if (chunk.getEntities(type, box, (List)into, (Predicate)predicate, maxCount)) {
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into,
+ final Predicate<? super T> predicate, final int maxCount) {
+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4;
+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4;
+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4;
+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4;
+
+ final int minRegionX = minChunkX >> REGION_SHIFT;
+ final int minRegionZ = minChunkZ >> REGION_SHIFT;
+ final int maxRegionX = maxChunkX >> REGION_SHIFT;
+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT;
+
+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) {
+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0;
+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK;
+
+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) {
+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ);
+
+ if (region == null) {
+ continue;
+ }
+
+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0;
+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT));
+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) {
+ continue;
+ }
+
+ if (chunk.getEntities(clazz, except, box, into, predicate, maxCount)) {
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public void entitySectionLoad(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) {
+ this.checkThread(chunkX, chunkZ, "Cannot load in entity section off-main");
+ synchronized (this) {
+ final ChunkEntitySlices curr = this.getChunk(chunkX, chunkZ);
+ if (curr != null) {
+ this.removeChunk(chunkX, chunkZ);
+
+ curr.mergeInto(slices);
+
+ this.addChunk(chunkX, chunkZ, slices);
+ } else {
+ this.addChunk(chunkX, chunkZ, slices);
+ }
+ }
+ }
+
+ public void entitySectionUnload(final int chunkX, final int chunkZ) {
+ this.checkThread(chunkX, chunkZ, "Cannot unload entity section off-main");
+ this.removeChunk(chunkX, chunkZ);
+ }
+
+ public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) {
+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
+ if (region == null) {
+ return null;
+ }
+
+ return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT));
+ }
+
+ public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) {
+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
+ ChunkEntitySlices ret;
+ if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) {
+ return this.createEntityChunk(chunkX, chunkZ, true);
+ }
+
+ return ret;
+ }
+
+ public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) {
+ final long key = CoordinateUtils.getChunkKey(regionX, regionZ);
+
+ return this.regions.get(key);
+ }
+
+ protected synchronized void removeChunk(final int chunkX, final int chunkZ) {
+ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
+ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT);
+
+ final ChunkSlicesRegion region = this.regions.get(key);
+ final int remaining = region.remove(relIndex);
+
+ if (remaining == 0) {
+ this.regions.remove(key);
+ }
+ }
+
+ public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) {
+ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
+ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT);
+
+ ChunkSlicesRegion region = this.regions.get(key);
+ if (region != null) {
+ region.add(relIndex, slices);
+ } else {
+ region = new ChunkSlicesRegion();
+ region.add(relIndex, slices);
+ this.regions.put(key, region);
+ }
+ }
+
+ public static final class ChunkSlicesRegion {
+
+ private final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE];
+ private int sliceCount;
+
+ public ChunkEntitySlices get(final int index) {
+ return this.slices[index];
+ }
+
+ public int remove(final int index) {
+ final ChunkEntitySlices slices = this.slices[index];
+ if (slices == null) {
+ throw new IllegalStateException();
+ }
+
+ this.slices[index] = null;
+
+ return --this.sliceCount;
+ }
+
+ public void add(final int index, final ChunkEntitySlices slices) {
+ final ChunkEntitySlices curr = this.slices[index];
+ if (curr != null) {
+ throw new IllegalStateException();
+ }
+
+ this.slices[index] = slices;
+
+ ++this.sliceCount;
+ }
+ }
+
+ protected final class EntityCallback implements EntityInLevelCallback {
+
+ public final Entity entity;
+
+ public EntityCallback(final Entity entity) {
+ this.entity = entity;
+ }
+
+ @Override
+ public void onMove() {
+ final Entity entity = this.entity;
+ final Visibility oldVisibility = getEntityStatus(entity);
+ final ChunkEntitySlices newSlices = EntityLookup.this.moveEntity(this.entity);
+ if (newSlices == null) {
+ // no new section, so didn't change sections
+ return;
+ }
+ final Visibility newVisibility = getEntityStatus(entity);
+
+ EntityLookup.this.entityStatusChange(entity, newSlices, oldVisibility, newVisibility, true, false, false);
+ }
+
+ @Override
+ public void onRemove(final Entity.RemovalReason reason) {
+ final Entity entity = this.entity;
+ EntityLookup.this.checkThread(entity, "Cannot remove entity off-main"); // Paper - rewrite chunk system
+ final Visibility tickingState = EntityLookup.getEntityStatus(entity);
+
+ EntityLookup.this.removeEntity(entity);
+
+ EntityLookup.this.entityStatusChange(entity, null, tickingState, Visibility.HIDDEN, false, false, reason.shouldDestroy());
+
+ this.entity.setLevelCallback(NoOpCallback.INSTANCE);
+ }
+ }
+
+ protected static final class NoOpCallback implements EntityInLevelCallback {
+
+ public static final NoOpCallback INSTANCE = new NoOpCallback();
+
+ @Override
+ public void onMove() {}
+
+ @Override
+ public void onRemove(final Entity.RemovalReason reason) {}
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
new file mode 100644
index 0000000000000000000000000000000000000000..fc4ea13aa4a21bd3d3f9377418a24b904868c401
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
@@ -0,0 +1,81 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.client;
+
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.entity.LevelCallback;
+
+public final class ClientEntityLookup extends EntityLookup {
+
+ private final LongOpenHashSet tickingChunks = new LongOpenHashSet();
+
+ public ClientEntityLookup(final Level world, final LevelCallback<Entity> worldCallback) {
+ super(world, worldCallback);
+ }
+
+ @Override
+ protected Boolean blockTicketUpdates() {
+ // not present on client
+ return null;
+ }
+
+ @Override
+ protected void setBlockTicketUpdates(Boolean value) {
+ // not present on client
+ }
+
+ @Override
+ protected void checkThread(final int chunkX, final int chunkZ, final String reason) {
+ // TODO implement?
+ }
+
+ @Override
+ protected void checkThread(final Entity entity, final String reason) {
+ // TODO implement?
+ }
+
+ @Override
+ protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
+ final boolean ticking = this.tickingChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ final ChunkEntitySlices ret = new ChunkEntitySlices(
+ this.world, chunkX, chunkZ,
+ ticking ? FullChunkStatus.ENTITY_TICKING : FullChunkStatus.FULL, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
+ );
+
+ // note: not handled by superclass
+ this.addChunk(chunkX, chunkZ, ret);
+
+ return ret;
+ }
+
+ @Override
+ protected void onEmptySlices(final int chunkX, final int chunkZ) {
+ this.removeChunk(chunkX, chunkZ);
+ }
+
+ public void markTicking(final long pos) {
+ if (this.tickingChunks.add(pos)) {
+ final int chunkX = CoordinateUtils.getChunkX(pos);
+ final int chunkZ = CoordinateUtils.getChunkZ(pos);
+ if (this.getChunk(chunkX, chunkZ) != null) {
+ this.chunkStatusChange(chunkX, chunkZ, FullChunkStatus.ENTITY_TICKING);
+ }
+ }
+ }
+
+ public void markNonTicking(final long pos) {
+ if (this.tickingChunks.remove(pos)) {
+ final int chunkX = CoordinateUtils.getChunkX(pos);
+ final int chunkZ = CoordinateUtils.getChunkZ(pos);
+ if (this.getChunk(chunkX, chunkZ) != null) {
+ this.chunkStatusChange(chunkX, chunkZ, FullChunkStatus.FULL);
+ }
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
new file mode 100644
index 0000000000000000000000000000000000000000..a9b0e8e90f433e141f36e47a9331cbdcb9ac9817
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
@@ -0,0 +1,72 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl;
+
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.entity.LevelCallback;
+
+public final class DefaultEntityLookup extends EntityLookup {
+ public DefaultEntityLookup(final Level world) {
+ super(world, new DefaultLevelCallback());
+ }
+
+ @Override
+ protected Boolean blockTicketUpdates() {
+ return null;
+ }
+
+ @Override
+ protected void setBlockTicketUpdates(final Boolean value) {}
+
+ @Override
+ protected void checkThread(final int chunkX, final int chunkZ, final String reason) {}
+
+ @Override
+ protected void checkThread(final Entity entity, final String reason) {}
+
+ @Override
+ protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
+ final ChunkEntitySlices ret = new ChunkEntitySlices(
+ this.world, chunkX, chunkZ, FullChunkStatus.FULL,
+ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
+ );
+
+ // note: not handled by superclass
+ this.addChunk(chunkX, chunkZ, ret);
+
+ return ret;
+ }
+
+ @Override
+ protected void onEmptySlices(final int chunkX, final int chunkZ) {
+ this.removeChunk(chunkX, chunkZ);
+ }
+
+ protected static final class DefaultLevelCallback implements LevelCallback<Entity> {
+
+ @Override
+ public void onCreated(final Entity entity) {}
+
+ @Override
+ public void onDestroyed(final Entity entity) {}
+
+ @Override
+ public void onTickingStart(final Entity entity) {}
+
+ @Override
+ public void onTickingEnd(final Entity entity) {}
+
+ @Override
+ public void onTrackingStart(final Entity entity) {}
+
+ @Override
+ public void onTrackingEnd(final Entity entity) {}
+
+ @Override
+ public void onSectionChange(final Entity entity) {}
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
new file mode 100644
index 0000000000000000000000000000000000000000..5b68279cae5952bdb7bdef3668980385a3a643e0
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
@@ -0,0 +1,50 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server;
+
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.entity.LevelCallback;
+
+public final class ServerEntityLookup extends EntityLookup {
+
+ private final ServerLevel serverWorld;
+
+ public ServerEntityLookup(final ServerLevel world, final LevelCallback<Entity> worldCallback) {
+ super(world, worldCallback);
+ this.serverWorld = world;
+ }
+
+ @Override
+ protected Boolean blockTicketUpdates() {
+ return ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager.blockTicketUpdates();
+ }
+
+ @Override
+ protected void setBlockTicketUpdates(final Boolean value) {
+ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager.unblockTicketUpdates(value);
+ }
+
+ @Override
+ protected void checkThread(final int chunkX, final int chunkZ, final String reason) {
+ io.papermc.paper.util.TickThread.ensureTickThread(this.serverWorld, chunkX, chunkZ, reason);
+ }
+
+ @Override
+ protected void checkThread(final Entity entity, final String reason) {
+ io.papermc.paper.util.TickThread.ensureTickThread(entity, reason);
+ }
+
+ @Override
+ protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
+ // loadInEntityChunk will call addChunk for us
+ return ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager
+ .getOrCreateEntityChunk(chunkX, chunkZ, transientChunk);
+ }
+
+ @Override
+ protected void onEmptySlices(final int chunkX, final int chunkZ) {
+ // entity slices unloading is managed by ticket levels in chunk system
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..458d1fc5e1222912512e6c59b56f6fca347d9ee9
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java
@@ -0,0 +1,17 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
+
+import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.chunk.ChunkAccess;
+
+public interface ChunkSystemPoiManager extends ChunkSystemSectionStorage {
+
+ public ServerLevel moonrise$getWorld();
+
+ public void moonrise$onUnload(final long coordinate);
+
+ public void moonrise$loadInPoiChunk(final PoiChunk poiChunk);
+
+ public void moonrise$checkConsistency(final ChunkAccess chunk);
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java
new file mode 100644
index 0000000000000000000000000000000000000000..89b956b8fdf1a0d862a843104511005e2990a897
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiSection.java
@@ -0,0 +1,12 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
+
+import net.minecraft.world.entity.ai.village.poi.PoiSection;
+import java.util.Optional;
+
+public interface ChunkSystemPoiSection {
+
+ public boolean moonrise$isEmpty();
+
+ public Optional<PoiSection> moonrise$asOptional();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
new file mode 100644
index 0000000000000000000000000000000000000000..cd1302a3aee6f543f39d71b91725128fa1aeddcc
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
@@ -0,0 +1,211 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
+
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.DataResult;
+import net.minecraft.SharedConstants;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.nbt.Tag;
+import net.minecraft.resources.RegistryOps;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.ai.village.poi.PoiManager;
+import net.minecraft.world.entity.ai.village.poi.PoiSection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.Optional;
+
+public final class PoiChunk {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PoiChunk.class);
+
+ public final ServerLevel world;
+ public final int chunkX;
+ public final int chunkZ;
+ public final int minSection;
+ public final int maxSection;
+
+ private final PoiSection[] sections;
+
+ private boolean isDirty;
+ private boolean loaded;
+
+ public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection) {
+ this(world, chunkX, chunkZ, minSection, maxSection, new PoiSection[maxSection - minSection + 1]);
+ }
+
+ public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection, final PoiSection[] sections) {
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.minSection = minSection;
+ this.maxSection = maxSection;
+ this.sections = sections;
+ if (this.sections.length != (maxSection - minSection + 1)) {
+ throw new IllegalStateException("Incorrect length used, expected " + (maxSection - minSection + 1) + ", got " + this.sections.length);
+ }
+ }
+
+ public void load() {
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Loading in poi chunk off-main");
+ if (this.loaded) {
+ return;
+ }
+ this.loaded = true;
+ ((ChunkSystemPoiManager)this.world.getChunkSource().getPoiManager()).moonrise$loadInPoiChunk(this);
+ }
+
+ public boolean isLoaded() {
+ return this.loaded;
+ }
+
+ public boolean isEmpty() {
+ for (final PoiSection section : this.sections) {
+ if (section != null && !((ChunkSystemPoiSection)section).moonrise$isEmpty()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public PoiSection getOrCreateSection(final int chunkY) {
+ if (chunkY >= this.minSection && chunkY <= this.maxSection) {
+ final int idx = chunkY - this.minSection;
+ final PoiSection ret = this.sections[idx];
+ if (ret != null) {
+ return ret;
+ }
+
+ final PoiManager poiManager = this.world.getPoiManager();
+ final long key = CoordinateUtils.getChunkSectionKey(this.chunkX, chunkY, this.chunkZ);
+
+ return this.sections[idx] = new PoiSection(() -> {
+ poiManager.setDirty(key);
+ });
+ }
+ throw new IllegalArgumentException("chunkY is out of bounds, chunkY: " + chunkY + " outside [" + this.minSection + "," + this.maxSection + "]");
+ }
+
+ public PoiSection getSection(final int chunkY) {
+ if (chunkY >= this.minSection && chunkY <= this.maxSection) {
+ return this.sections[chunkY - this.minSection];
+ }
+ return null;
+ }
+
+ public Optional<PoiSection> getSectionForVanilla(final int chunkY) {
+ if (chunkY >= this.minSection && chunkY <= this.maxSection) {
+ final PoiSection ret = this.sections[chunkY - this.minSection];
+ return ret == null ? Optional.empty() : ((ChunkSystemPoiSection)ret).moonrise$asOptional();
+ }
+ return Optional.empty();
+ }
+
+ public boolean isDirty() {
+ return this.isDirty;
+ }
+
+ public void setDirty(final boolean dirty) {
+ this.isDirty = dirty;
+ }
+
+ // returns null if empty
+ public CompoundTag save() {
+ final RegistryOps<Tag> registryOps = RegistryOps.create(NbtOps.INSTANCE, this.world.registryAccess());
+
+ final CompoundTag ret = new CompoundTag();
+ final CompoundTag sections = new CompoundTag();
+ ret.put("Sections", sections);
+
+ ret.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion());
+
+ final ServerLevel world = this.world;
+ final PoiManager poiManager = world.getPoiManager();
+ final int chunkX = this.chunkX;
+ final int chunkZ = this.chunkZ;
+
+ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) {
+ final PoiSection section = this.sections[sectionY - this.minSection];
+ if (section == null || ((ChunkSystemPoiSection)section).moonrise$isEmpty()) {
+ continue;
+ }
+
+ final long key = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ);
+ // codecs are honestly such a fucking disaster. What the fuck is this trash?
+ final Codec<PoiSection> codec = PoiSection.codec(() -> {
+ poiManager.setDirty(key);
+ });
+
+ final DataResult<Tag> serializedResult = codec.encodeStart(registryOps, section);
+ final int finalSectionY = sectionY;
+ final Tag serialized = serializedResult.resultOrPartial((final String description) -> {
+ LOGGER.error("Failed to serialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description);
+ }).orElse(null);
+ if (serialized == null) {
+ // failed, should be logged from the resultOrPartial
+ continue;
+ }
+
+ sections.put(Integer.toString(sectionY), serialized);
+ }
+
+ return sections.isEmpty() ? null : ret;
+ }
+
+ public static PoiChunk empty(final ServerLevel world, final int chunkX, final int chunkZ) {
+ final PoiChunk ret = new PoiChunk(world, chunkX, chunkZ, WorldUtil.getMinSection(world), WorldUtil.getMaxSection(world));
+ ret.loaded = true;
+ return ret;
+ }
+
+ public static PoiChunk parse(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data) {
+ final PoiChunk ret = empty(world, chunkX, chunkZ);
+
+ final RegistryOps<Tag> registryOps = RegistryOps.create(NbtOps.INSTANCE, world.registryAccess());
+
+ final CompoundTag sections = data.getCompound("Sections");
+
+ if (sections.isEmpty()) {
+ // nothing to parse
+ return ret;
+ }
+
+ final PoiManager poiManager = world.getPoiManager();
+
+ boolean readAnything = false;
+
+ for (int sectionY = ret.minSection; sectionY <= ret.maxSection; ++sectionY) {
+ final String key = Integer.toString(sectionY);
+ if (!sections.contains(key)) {
+ continue;
+ }
+
+ final long coordinateKey = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ);
+ // codecs are honestly such a fucking disaster. What the fuck is this trash?
+ final Codec<PoiSection> codec = PoiSection.codec(() -> {
+ poiManager.setDirty(coordinateKey);
+ });
+
+ final CompoundTag section = sections.getCompound(key);
+ final DataResult<PoiSection> deserializeResult = codec.parse(registryOps, section);
+ final int finalSectionY = sectionY;
+ final PoiSection deserialized = deserializeResult.resultOrPartial((final String description) -> {
+ LOGGER.error("Failed to deserialize poi chunk for world: " + WorldUtil.getWorldName(world) + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description);
+ }).orElse(null);
+
+ if (deserialized == null || ((ChunkSystemPoiSection)deserialized).moonrise$isEmpty()) {
+ // completely empty, no point in storing this
+ continue;
+ }
+
+ readAnything = true;
+ ret.sections[sectionY - ret.minSection] = deserialized;
+ }
+
+ ret.loaded = !readAnything; // Set loaded to false if we read anything to ensure proper callbacks to PoiManager are made on #load
+
+ return ret;
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..3f5edb756beb9c31b6f591a24b778d6ac2b0bf51
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
@@ -0,0 +1,21 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.storage;
+
+import com.mojang.serialization.Dynamic;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.Tag;
+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
+import java.io.IOException;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+public interface ChunkSystemSectionStorage {
+
+ public CompoundTag moonrise$read(final int chunkX, final int chunkZ) throws IOException;
+
+ public void moonrise$write(final int chunkX, final int chunkZ, final CompoundTag data) throws IOException;
+
+ public RegionFileStorage moonrise$getRegionStorage();
+
+ public void moonrise$close() throws IOException;
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java
new file mode 100644
index 0000000000000000000000000000000000000000..003a857e70ead858e8437e3c1bfaf22f4daba0df
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/ChunkSystemServerPlayer.java
@@ -0,0 +1,15 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.player;
+
+public interface ChunkSystemServerPlayer {
+
+ public boolean moonrise$isRealPlayer();
+
+ public void moonrise$setRealPlayer(final boolean real);
+
+ public RegionizedPlayerChunkLoader.PlayerChunkLoaderData moonrise$getChunkLoader();
+
+ public void moonrise$setChunkLoader(final RegionizedPlayerChunkLoader.PlayerChunkLoaderData loader);
+
+ public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..82e8ce73b77accd6a4210f88c9fccb325ae367d4
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
@@ -0,0 +1,1074 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.player;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.moonrise.common.misc.AllocatingRateLimiter;
+import ca.spottedleaf.moonrise.common.misc.SingleUserAreaMap;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import ca.spottedleaf.moonrise.patches.chunk_system.util.ParallelSearchRadiusIteration;
+import com.google.gson.JsonObject;
+import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongComparator;
+import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue;
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket;
+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket;
+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket;
+import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket;
+import net.minecraft.server.level.ChunkTrackingView;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.TicketType;
+import net.minecraft.server.network.PlayerChunkSender;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.GameRules;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.levelgen.BelowZeroRetrogen;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayDeque;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Function;
+
+public final class RegionizedPlayerChunkLoader {
+
+ public static final TicketType<Long> PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo);
+ public static final TicketType<Long> PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 5 * 20);
+
+ public static final int MIN_VIEW_DISTANCE = 2;
+ public static final int MAX_VIEW_DISTANCE = 32;
+
+ public static final int GENERATED_TICKET_LEVEL = ChunkHolderManager.FULL_LOADED_TICKET_LEVEL;
+ public static final int LOADED_TICKET_LEVEL = ChunkTaskScheduler.getTicketLevel(ChunkStatus.EMPTY);
+ public static final int TICK_TICKET_LEVEL = ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL;
+
+ public static final class ViewDistanceHolder {
+
+ private volatile ViewDistances viewDistances;
+ private static final VarHandle VIEW_DISTANCES_HANDLE = ConcurrentUtil.getVarHandle(ViewDistanceHolder.class, "viewDistances", ViewDistances.class);
+
+ public ViewDistanceHolder() {
+ VIEW_DISTANCES_HANDLE.setVolatile(this, new ViewDistances(-1, -1, -1));
+ }
+
+ public ViewDistances getViewDistances() {
+ return (ViewDistances)VIEW_DISTANCES_HANDLE.getVolatile(this);
+ }
+
+ public ViewDistances compareAndExchangeViewDistance(final ViewDistances expect, final ViewDistances update) {
+ return (ViewDistances)VIEW_DISTANCES_HANDLE.compareAndExchange(this, expect, update);
+ }
+
+ public void updateViewDistance(final Function<ViewDistances, ViewDistances> update) {
+ int failures = 0;
+ for (ViewDistances curr = this.getViewDistances();;) {
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (curr == (curr = this.compareAndExchangeViewDistance(curr, update.apply(curr)))) {
+ return;
+ }
+ ++failures;
+ }
+ }
+
+ public void setTickViewDistance(final int distance) {
+ this.updateViewDistance((final ViewDistances param) -> {
+ return param.setTickViewDistance(distance);
+ });
+ }
+
+ public void setLoadViewDistance(final int distance) {
+ this.updateViewDistance((final ViewDistances param) -> {
+ return param.setLoadViewDistance(distance);
+ });
+ }
+
+ public void setSendViewDistance(final int distance) {
+ this.updateViewDistance((final ViewDistances param) -> {
+ return param.setTickViewDistance(distance);
+ });
+ }
+
+ public JsonObject toJson() {
+ return this.getViewDistances().toJson();
+ }
+ }
+
+ public static final record ViewDistances(
+ int tickViewDistance,
+ int loadViewDistance,
+ int sendViewDistance
+ ) {
+ public ViewDistances setTickViewDistance(final int distance) {
+ return new ViewDistances(distance, this.loadViewDistance, this.sendViewDistance);
+ }
+
+ public ViewDistances setLoadViewDistance(final int distance) {
+ return new ViewDistances(this.tickViewDistance, distance, this.sendViewDistance);
+ }
+
+ public ViewDistances setSendViewDistance(final int distance) {
+ return new ViewDistances(this.tickViewDistance, this.loadViewDistance, distance);
+ }
+
+ public JsonObject toJson() {
+ final JsonObject ret = new JsonObject();
+
+ ret.addProperty("tick-view-distance", this.tickViewDistance);
+ ret.addProperty("load-view-distance", this.loadViewDistance);
+ ret.addProperty("send-view-distance", this.sendViewDistance);
+
+ return ret;
+ }
+ }
+
+ public static int getAPITickViewDistance(final ServerPlayer player) {
+ final ServerLevel level = player.serverLevel();
+ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (data == null) {
+ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPITickDistance();
+ }
+ return data.lastTickDistance;
+ }
+
+ public static int getAPIViewDistance(final ServerPlayer player) {
+ final ServerLevel level = player.serverLevel();
+ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (data == null) {
+ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPIViewDistance();
+ }
+ // view distance = load distance + 1
+ return data.lastLoadDistance - 1;
+ }
+
+ public static int getLoadViewDistance(final ServerPlayer player) {
+ final ServerLevel level = player.serverLevel();
+ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (data == null) {
+ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPIViewDistance();
+ }
+ // view distance = load distance + 1
+ return data.lastLoadDistance - 1;
+ }
+
+ public static int getAPISendViewDistance(final ServerPlayer player) {
+ final ServerLevel level = player.serverLevel();
+ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (data == null) {
+ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPISendViewDistance();
+ }
+ return data.lastSendDistance;
+ }
+
+ private final ServerLevel world;
+
+ public RegionizedPlayerChunkLoader(final ServerLevel world) {
+ this.world = world;
+ }
+
+ public void addPlayer(final ServerPlayer player) {
+ io.papermc.paper.util.TickThread.ensureTickThread(player, "Cannot add player to player chunk loader async");
+ if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) {
+ return;
+ }
+
+ if (((ChunkSystemServerPlayer)player).moonrise$getChunkLoader() != null) {
+ throw new IllegalStateException("Player is already added to player chunk loader");
+ }
+
+ final PlayerChunkLoaderData loader = new PlayerChunkLoaderData(this.world, player);
+
+ ((ChunkSystemServerPlayer)player).moonrise$setChunkLoader(loader);
+ loader.add();
+ }
+
+ public void updatePlayer(final ServerPlayer player) {
+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (loader != null) {
+ loader.update();
+ }
+ }
+
+ public void removePlayer(final ServerPlayer player) {
+ io.papermc.paper.util.TickThread.ensureTickThread(player, "Cannot remove player from player chunk loader async");
+ if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) {
+ return;
+ }
+
+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+
+ if (loader == null) {
+ return;
+ }
+
+ loader.remove();
+ ((ChunkSystemServerPlayer)player).moonrise$setChunkLoader(null);
+ }
+
+ public void setSendDistance(final int distance) {
+ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setSendViewDistance(distance);
+ }
+
+ public void setLoadDistance(final int distance) {
+ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setLoadViewDistance(distance);
+ }
+
+ public void setTickDistance(final int distance) {
+ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setTickViewDistance(distance);
+ }
+
+ // Note: follow the player chunk loader so everything stays consistent...
+ public int getAPITickDistance() {
+ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(
+ -1, distances.tickViewDistance,
+ -1, distances.loadViewDistance
+ );
+ return tickViewDistance;
+ }
+
+ public int getAPIViewDistance() {
+ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(
+ -1, distances.tickViewDistance,
+ -1, distances.loadViewDistance
+ );
+ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance);
+
+ // loadDistance = api view distance + 1
+ return loadDistance - 1;
+ }
+
+ public int getAPISendViewDistance() {
+ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(
+ -1, distances.tickViewDistance,
+ -1, distances.loadViewDistance
+ );
+ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance);
+ final int sendViewDistance = PlayerChunkLoaderData.getSendViewDistance(
+ loadDistance, -1, -1, distances.sendViewDistance
+ );
+
+ return sendViewDistance;
+ }
+
+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) {
+ return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ);
+ }
+
+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) {
+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (loader == null) {
+ return false;
+ }
+
+ return loader.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+
+ public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) {
+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (loader == null) {
+ return false;
+ }
+
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ if (!loader.sentChunks.contains(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ))) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public void tick() {
+ io.papermc.paper.util.TickThread.ensureTickThread("Cannot tick player chunk loader async");
+ long currTime = System.nanoTime();
+ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) {
+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (loader == null || loader.removed || loader.world != this.world) {
+ // not our problem anymore
+ continue;
+ }
+ loader.update(); // can't invoke plugin logic
+ loader.updateQueues(currTime);
+ }
+ }
+
+ public static final class PlayerChunkLoaderData {
+
+ private static final AtomicLong ID_GENERATOR = new AtomicLong();
+ private final long id = ID_GENERATOR.incrementAndGet();
+ private final Long idBoxed = Long.valueOf(this.id);
+
+ private static final long MAX_RATE = 10_000L;
+
+ private final ServerPlayer player;
+ private final ServerLevel world;
+
+ private int lastChunkX = Integer.MIN_VALUE;
+ private int lastChunkZ = Integer.MIN_VALUE;
+
+ private int lastSendDistance = Integer.MIN_VALUE;
+ private int lastLoadDistance = Integer.MIN_VALUE;
+ private int lastTickDistance = Integer.MIN_VALUE;
+
+ private int lastSentChunkCenterX = Integer.MIN_VALUE;
+ private int lastSentChunkCenterZ = Integer.MIN_VALUE;
+
+ private int lastSentChunkRadius = Integer.MIN_VALUE;
+ private int lastSentSimulationDistance = Integer.MIN_VALUE;
+
+ private boolean canGenerateChunks = true;
+
+ private final ArrayDeque<ChunkHolderManager.TicketOperation<?, ?>> delayedTicketOps = new ArrayDeque<>();
+ private final LongOpenHashSet sentChunks = new LongOpenHashSet();
+
+ private static final byte CHUNK_TICKET_STAGE_NONE = 0;
+ private static final byte CHUNK_TICKET_STAGE_LOADING = 1;
+ private static final byte CHUNK_TICKET_STAGE_LOADED = 2;
+ private static final byte CHUNK_TICKET_STAGE_GENERATING = 3;
+ private static final byte CHUNK_TICKET_STAGE_GENERATED = 4;
+ private static final byte CHUNK_TICKET_STAGE_TICK = 5;
+ private static final int[] TICKET_STAGE_TO_LEVEL = new int[] {
+ ChunkHolderManager.MAX_TICKET_LEVEL + 1,
+ LOADED_TICKET_LEVEL,
+ LOADED_TICKET_LEVEL,
+ GENERATED_TICKET_LEVEL,
+ GENERATED_TICKET_LEVEL,
+ TICK_TICKET_LEVEL
+ };
+ private final Long2ByteOpenHashMap chunkTicketStage = new Long2ByteOpenHashMap();
+ {
+ this.chunkTicketStage.defaultReturnValue(CHUNK_TICKET_STAGE_NONE);
+ }
+
+ // rate limiting
+ private static final long ALLOCATION_GRANULARITY = TimeUnit.SECONDS.toNanos(1L);
+ private final AllocatingRateLimiter chunkSendLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY);
+ private final AllocatingRateLimiter chunkLoadTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY);
+ private final AllocatingRateLimiter chunkGenerateTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY);
+
+ // queues
+ private final LongComparator CLOSEST_MANHATTAN_DIST = (final long c1, final long c2) -> {
+ final int c1x = CoordinateUtils.getChunkX(c1);
+ final int c1z = CoordinateUtils.getChunkZ(c1);
+
+ final int c2x = CoordinateUtils.getChunkX(c2);
+ final int c2z = CoordinateUtils.getChunkZ(c2);
+
+ final int centerX = PlayerChunkLoaderData.this.lastChunkX;
+ final int centerZ = PlayerChunkLoaderData.this.lastChunkZ;
+
+ return Integer.compare(
+ Math.abs(c1x - centerX) + Math.abs(c1z - centerZ),
+ Math.abs(c2x - centerX) + Math.abs(c2z - centerZ)
+ );
+ };
+ private final LongHeapPriorityQueue sendQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue tickingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue generatingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue genQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue loadingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+ private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST);
+
+ private volatile boolean removed;
+
+ public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) {
+ this.world = world;
+ this.player = player;
+ }
+
+ private void flushDelayedTicketOps() {
+ if (this.delayedTicketOps.isEmpty()) {
+ return;
+ }
+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.performTicketUpdates(this.delayedTicketOps);
+ this.delayedTicketOps.clear();
+ }
+
+ private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation<?, ?> op) {
+ this.delayedTicketOps.addLast(op);
+ }
+
+ private void sendChunk(final int chunkX, final int chunkZ) {
+ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
+ ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager
+ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player);
+ PlayerChunkSender.sendChunk(this.player.connection, this.world, ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ));
+ return;
+ }
+ throw new IllegalStateException();
+ }
+
+ private void sendUnloadChunk(final int chunkX, final int chunkZ) {
+ if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
+ return;
+ }
+ this.sendUnloadChunkRaw(chunkX, chunkZ);
+ }
+
+ private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) {
+ // Note: Check PlayerChunkSender#dropChunk for other logic
+ // Note: drop isAlive() check so that chunks properly unload client-side when the player dies
+ ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager
+ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$removeReceivedChunk(this.player);
+ this.player.connection.send(new ClientboundForgetLevelChunkPacket(new ChunkPos(chunkX, chunkZ)));
+ }
+
+ private final SingleUserAreaMap<PlayerChunkLoaderData> broadcastMap = new SingleUserAreaMap<>(this) {
+ @Override
+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ // do nothing, we only care about remove
+ }
+
+ @Override
+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ parameter.sendUnloadChunk(chunkX, chunkZ);
+ }
+ };
+ private final SingleUserAreaMap<PlayerChunkLoaderData> loadTicketCleanup = new SingleUserAreaMap<>(this) {
+ @Override
+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ // do nothing, we only care about remove
+ }
+
+ @Override
+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ final byte ticketStage = parameter.chunkTicketStage.remove(chunk);
+ final int level = TICKET_STAGE_TO_LEVEL[ticketStage];
+ if (level > ChunkHolderManager.MAX_TICKET_LEVEL) {
+ return;
+ }
+
+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove(
+ chunk,
+ PLAYER_TICKET_DELAYED, level, parameter.idBoxed,
+ PLAYER_TICKET, level, parameter.idBoxed
+ ));
+ }
+ };
+ private final SingleUserAreaMap<PlayerChunkLoaderData> tickMap = new SingleUserAreaMap<>(this) {
+ @Override
+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ // do nothing, we will detect ticking chunks when we try to load them
+ }
+
+ @Override
+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) {
+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ // note: by the time this is called, the tick cleanup should have ran - so, if the chunk is at
+ // the tick stage it was deemed in range for loading. Thus, we need to move it to generated
+ if (!parameter.chunkTicketStage.replace(chunk, CHUNK_TICKET_STAGE_TICK, CHUNK_TICKET_STAGE_GENERATED)) {
+ return;
+ }
+
+ // Since we are possibly downgrading the ticket level, we add the delayed unload ticket so that
+ // the level is kept for a short period of time
+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove(
+ chunk,
+ PLAYER_TICKET_DELAYED, TICK_TICKET_LEVEL, parameter.idBoxed,
+ PLAYER_TICKET, TICK_TICKET_LEVEL, parameter.idBoxed
+ ));
+ // keep chunk at new generated level
+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addOp(
+ chunk, PLAYER_TICKET, GENERATED_TICKET_LEVEL, parameter.idBoxed
+ ));
+ }
+ };
+
+ private static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ,
+ final int sendRadius) {
+ // expect sendRadius to be = 1 + target viewable radius
+ return ChunkTrackingView.isWithinDistance(centerX, centerZ, sendRadius, chunkX, chunkZ, true);
+ }
+
+ private static int getClientViewDistance(final ServerPlayer player) {
+ final Integer vd = player.requestedViewDistance();
+ return vd == null ? -1 : Math.max(0, vd.intValue());
+ }
+
+ private static int getTickDistance(final int playerTickViewDistance, final int worldTickViewDistance,
+ final int playerLoadViewDistance, final int worldLoadViewDistance) {
+ return Math.min(
+ playerTickViewDistance < 0 ? worldTickViewDistance : playerTickViewDistance,
+ playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance
+ );
+ }
+
+ private static int getLoadViewDistance(final int tickViewDistance, final int playerLoadViewDistance,
+ final int worldLoadViewDistance) {
+ return Math.max(tickViewDistance + 1, playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance);
+ }
+
+ private static int getSendViewDistance(final int loadViewDistance, final int clientViewDistance,
+ final int playerSendViewDistance, final int worldSendViewDistance) {
+ return Math.min(
+ loadViewDistance - 1,
+ playerSendViewDistance < 0 ? (!io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance
+ );
+ }
+
+ private Packet<?> updateClientChunkRadius(final int radius) {
+ this.lastSentChunkRadius = radius;
+ return new ClientboundSetChunkCacheRadiusPacket(radius);
+ }
+
+ private Packet<?> updateClientSimulationDistance(final int distance) {
+ this.lastSentSimulationDistance = distance;
+ return new ClientboundSetSimulationDistancePacket(distance);
+ }
+
+ private Packet<?> updateClientChunkCenter(final int chunkX, final int chunkZ) {
+ this.lastSentChunkCenterX = chunkX;
+ this.lastSentChunkCenterZ = chunkZ;
+ return new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ);
+ }
+
+ private boolean canPlayerGenerateChunks() {
+ return !this.player.isSpectator() || this.world.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS);
+ }
+
+ private double getMaxChunkLoadRate() {
+ final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate;
+
+ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
+ }
+
+ private double getMaxChunkGenRate() {
+ final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate;
+
+ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
+ }
+
+ private double getMaxChunkSendRate() {
+ final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate;
+
+ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate);
+ }
+
+ private long getMaxChunkLoads() {
+ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L);
+ long configLimit = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads;
+ if (configLimit == 0L) {
+ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active
+ configLimit = Math.max(5L, radiusChunks / 5L);
+ } else if (configLimit < 0L) {
+ configLimit = Integer.MAX_VALUE;
+ } // else: use the value configured
+ configLimit = configLimit - this.loadingQueue.size();
+
+ return configLimit;
+ }
+
+ private long getMaxChunkGenerates() {
+ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L);
+ long configLimit = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates;
+ if (configLimit == 0L) {
+ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active
+ configLimit = Math.max(5L, radiusChunks / 5L);
+ } else if (configLimit < 0L) {
+ configLimit = Integer.MAX_VALUE;
+ } // else: use the value configured
+ configLimit = configLimit - this.generatingQueue.size();
+
+ return configLimit;
+ }
+
+ private boolean wantChunkSent(final int chunkX, final int chunkZ) {
+ final int dx = this.lastChunkX - chunkX;
+ final int dz = this.lastChunkZ - chunkZ;
+ return (Math.max(Math.abs(dx), Math.abs(dz)) <= (this.lastSendDistance + 1)) && wantChunkLoaded(
+ this.lastChunkX, this.lastChunkZ, chunkX, chunkZ, this.lastSendDistance
+ );
+ }
+
+ private boolean wantChunkTicked(final int chunkX, final int chunkZ) {
+ final int dx = this.lastChunkX - chunkX;
+ final int dz = this.lastChunkZ - chunkZ;
+ return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance;
+ }
+
+ private boolean areNeighboursGenerated(final int chunkX, final int chunkZ, final int radius) {
+ for (int dz = -radius; dz <= radius; ++dz) {
+ for (int dx = -radius; dx <= radius; ++dx) {
+ if ((dx | dz) == 0) {
+ continue;
+ }
+
+ final long neighbour = CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ);
+ final byte stage = this.chunkTicketStage.get(neighbour);
+
+ if (stage != CHUNK_TICKET_STAGE_GENERATED && stage != CHUNK_TICKET_STAGE_TICK) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ void updateQueues(final long time) {
+ io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async");
+ if (this.removed) {
+ throw new IllegalStateException("Ticking removed player chunk loader");
+ }
+ // update rate limits
+ final double loadRate = this.getMaxChunkLoadRate();
+ final double genRate = this.getMaxChunkGenRate();
+ final double sendRate = this.getMaxChunkSendRate();
+
+ this.chunkLoadTicketLimiter.tickAllocation(time, loadRate, loadRate);
+ this.chunkGenerateTicketLimiter.tickAllocation(time, genRate, genRate);
+ this.chunkSendLimiter.tickAllocation(time, sendRate, sendRate);
+
+ // try to progress chunk loads
+ while (!this.loadingQueue.isEmpty()) {
+ final long pendingLoadChunk = this.loadingQueue.firstLong();
+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingLoadChunk);
+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingLoadChunk);
+ final ChunkAccess pending = ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(pendingChunkX, pendingChunkZ);
+ if (pending == null) {
+ // nothing to do here
+ break;
+ }
+ // chunk has loaded, so we can take it out of the queue
+ this.loadingQueue.dequeueLong();
+
+ // try to move to generate queue
+ final byte prev = this.chunkTicketStage.put(pendingLoadChunk, CHUNK_TICKET_STAGE_LOADED);
+ if (prev != CHUNK_TICKET_STAGE_LOADING) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADING + ", not " + prev);
+ }
+
+ if (this.canGenerateChunks || this.isLoadedChunkGeneratable(pending)) {
+ this.genQueue.enqueue(pendingLoadChunk);
+ } // else: don't want to generate, so just leave it loaded
+ }
+
+ // try to push more chunk loads
+ final long maxLoads = Math.max(0L, Math.min(MAX_RATE, Math.min(this.loadQueue.size(), this.getMaxChunkLoads())));
+ final int maxLoadsThisTick = (int)this.chunkLoadTicketLimiter.takeAllocation(time, loadRate, maxLoads);
+ if (maxLoadsThisTick > 0) {
+ final LongArrayList chunks = new LongArrayList(maxLoadsThisTick);
+ for (int i = 0; i < maxLoadsThisTick; ++i) {
+ final long chunk = this.loadQueue.dequeueLong();
+ final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_LOADING);
+ if (prev != CHUNK_TICKET_STAGE_NONE) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_NONE + ", not " + prev);
+ }
+ this.pushDelayedTicketOp(
+ ChunkHolderManager.TicketOperation.addOp(
+ chunk,
+ PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed
+ )
+ );
+ chunks.add(chunk);
+ this.loadingQueue.enqueue(chunk);
+ }
+
+ // here we need to flush tickets, as scheduleChunkLoad requires tickets to be propagated with addTicket = false
+ this.flushDelayedTicketOps();
+ // we only need to call scheduleChunkLoad because the loaded ticket level is not enough to start the chunk
+ // load - only generate ticket levels start anything, but they start generation...
+ // propagate levels
+ // Note: this CAN call plugin logic, so it is VITAL that our bookkeeping logic is completely done by the time this is invoked
+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates();
+
+ if (this.removed) {
+ // process ticket updates may invoke plugin logic, which may remove this player
+ return;
+ }
+
+ for (int i = 0; i < maxLoadsThisTick; ++i) {
+ final long queuedLoadChunk = chunks.getLong(i);
+ final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk);
+ final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk);
+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkLoad(
+ queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, PrioritisedExecutor.Priority.NORMAL, null
+ );
+ if (this.removed) {
+ return;
+ }
+ }
+ }
+
+ // try to progress chunk generations
+ while (!this.generatingQueue.isEmpty()) {
+ final long pendingGenChunk = this.generatingQueue.firstLong();
+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingGenChunk);
+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingGenChunk);
+ final LevelChunk pending = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingChunkX, pendingChunkZ);
+ if (pending == null) {
+ // nothing to do here
+ break;
+ }
+
+ // chunk has generated, so we can take it out of queue
+ this.generatingQueue.dequeueLong();
+
+ final byte prev = this.chunkTicketStage.put(pendingGenChunk, CHUNK_TICKET_STAGE_GENERATED);
+ if (prev != CHUNK_TICKET_STAGE_GENERATING) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATING + ", not " + prev);
+ }
+
+ // try to move to send queue
+ if (this.wantChunkSent(pendingChunkX, pendingChunkZ)) {
+ this.sendQueue.enqueue(pendingGenChunk);
+ }
+ // try to move to tick queue
+ if (this.wantChunkTicked(pendingChunkX, pendingChunkZ)) {
+ this.tickingQueue.enqueue(pendingGenChunk);
+ }
+ }
+
+ // try to push more chunk generations
+ final long maxGens = Math.max(0L, Math.min(MAX_RATE, Math.min(this.genQueue.size(), this.getMaxChunkGenerates())));
+ // preview the allocations, as we may not actually utilise all of them
+ final long maxGensThisTick = this.chunkGenerateTicketLimiter.previewAllocation(time, genRate, maxGens);
+ long ratedGensThisTick = 0L;
+ while (!this.genQueue.isEmpty()) {
+ final long chunkKey = this.genQueue.firstLong();
+ final int chunkX = CoordinateUtils.getChunkX(chunkKey);
+ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey);
+ final ChunkAccess chunk = ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ);
+ if (chunk.getPersistedStatus() != ChunkStatus.FULL) {
+ // only rate limit actual generations
+ if ((ratedGensThisTick + 1L) > maxGensThisTick) {
+ break;
+ }
+ ++ratedGensThisTick;
+ }
+
+ this.genQueue.dequeueLong();
+
+ final byte prev = this.chunkTicketStage.put(chunkKey, CHUNK_TICKET_STAGE_GENERATING);
+ if (prev != CHUNK_TICKET_STAGE_LOADED) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADED + ", not " + prev);
+ }
+ this.pushDelayedTicketOp(
+ ChunkHolderManager.TicketOperation.addAndRemove(
+ chunkKey,
+ PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed,
+ PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed
+ )
+ );
+ this.generatingQueue.enqueue(chunkKey);
+ }
+ // take the allocations we actually used
+ this.chunkGenerateTicketLimiter.takeAllocation(time, genRate, ratedGensThisTick);
+
+ // try to pull ticking chunks
+ while (!this.tickingQueue.isEmpty()) {
+ final long pendingTicking = this.tickingQueue.firstLong();
+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingTicking);
+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingTicking);
+
+ if (!this.areNeighboursGenerated(pendingChunkX, pendingChunkZ,
+ ChunkHolderManager.FULL_LOADED_TICKET_LEVEL - ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL)) {
+ break;
+ }
+
+ // only gets here if all neighbours were marked as generated or ticking themselves
+ this.tickingQueue.dequeueLong();
+ this.pushDelayedTicketOp(
+ ChunkHolderManager.TicketOperation.addAndRemove(
+ pendingTicking,
+ PLAYER_TICKET, TICK_TICKET_LEVEL, this.idBoxed,
+ PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed
+ )
+ );
+ // note: there is no queue to add after ticking
+ final byte prev = this.chunkTicketStage.put(pendingTicking, CHUNK_TICKET_STAGE_TICK);
+ if (prev != CHUNK_TICKET_STAGE_GENERATED) {
+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATED + ", not " + prev);
+ }
+ }
+
+ // try to pull sending chunks
+ final long maxSends = Math.max(0L, Math.min(MAX_RATE, Integer.MAX_VALUE)); // note: no logic to track concurrent sends
+ final int maxSendsThisTick = Math.min((int)this.chunkSendLimiter.takeAllocation(time, sendRate, maxSends), this.sendQueue.size());
+ // we do not return sends that we took from the allocation back because we want to limit the max send rate, not target it
+ for (int i = 0; i < maxSendsThisTick; ++i) {
+ final long pendingSend = this.sendQueue.firstLong();
+ final int pendingSendX = CoordinateUtils.getChunkX(pendingSend);
+ final int pendingSendZ = CoordinateUtils.getChunkZ(pendingSend);
+ final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingSendX, pendingSendZ);
+ if (!this.areNeighboursGenerated(pendingSendX, pendingSendZ, 1) || !io.papermc.paper.util.TickThread.isTickThreadFor(this.world, pendingSendX, pendingSendZ)) {
+ // nothing to do
+ // the target chunk may not be owned by this region, but this should be resolved in the future
+ break;
+ }
+ if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) {
+ // not yet post-processed, need to do this so that tile entities can properly be sent to clients
+ chunk.postProcessGeneration();
+ // check if there was any recursive action
+ if (this.removed || this.sendQueue.isEmpty() || this.sendQueue.firstLong() != pendingSend) {
+ return;
+ } // else: good to dequeue and send, fall through
+ }
+ this.sendQueue.dequeueLong();
+
+ this.sendChunk(pendingSendX, pendingSendZ);
+
+ if (this.removed) {
+ // sendChunk may invoke plugin logic
+ return;
+ }
+ }
+
+ this.flushDelayedTicketOps();
+ }
+
+ void add() {
+ io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
+ if (this.removed) {
+ throw new IllegalStateException("Adding removed player chunk loader");
+ }
+ final ViewDistances playerDistances = ((ChunkSystemServerPlayer)this.player).moonrise$getViewDistanceHolder().getViewDistances();
+ final ViewDistances worldDistances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
+ final int chunkX = this.player.chunkPosition().x;
+ final int chunkZ = this.player.chunkPosition().z;
+
+ final int tickViewDistance = getTickDistance(
+ playerDistances.tickViewDistance, worldDistances.tickViewDistance,
+ playerDistances.loadViewDistance, worldDistances.loadViewDistance
+ );
+ // load view cannot be less-than tick view + 1
+ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance);
+ // send view cannot be greater-than load view
+ final int clientViewDistance = getClientViewDistance(this.player);
+ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance);
+
+ // TODO check PlayerList diff in paper chunk system patch
+ // send view distances
+ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance));
+ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance));
+
+ // add to distance maps
+ this.broadcastMap.add(chunkX, chunkZ, sendViewDistance + 1);
+ this.loadTicketCleanup.add(chunkX, chunkZ, loadViewDistance + 1);
+ this.tickMap.add(chunkX, chunkZ, tickViewDistance);
+
+ // update chunk center
+ this.player.connection.send(this.updateClientChunkCenter(chunkX, chunkZ));
+
+ // reset limiters, they will start at a zero allocation
+ final long time = System.nanoTime();
+ this.chunkLoadTicketLimiter.reset(time);
+ this.chunkGenerateTicketLimiter.reset(time);
+ this.chunkSendLimiter.reset(time);
+
+ // now we can update
+ this.update();
+ }
+
+ private boolean isLoadedChunkGeneratable(final int chunkX, final int chunkZ) {
+ return this.isLoadedChunkGeneratable(((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ));
+ }
+
+ private boolean isLoadedChunkGeneratable(final ChunkAccess chunkAccess) {
+ final BelowZeroRetrogen belowZeroRetrogen;
+ // see PortalForcer#findPortalAround
+ return chunkAccess != null && (
+ chunkAccess.getPersistedStatus() == ChunkStatus.FULL ||
+ ((belowZeroRetrogen = chunkAccess.getBelowZeroRetrogen()) != null && belowZeroRetrogen.targetStatus().isOrAfter(ChunkStatus.SPAWN))
+ );
+ }
+
+ void update() {
+ io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot update player asynchronously");
+ if (this.removed) {
+ throw new IllegalStateException("Updating removed player chunk loader");
+ }
+ final ViewDistances playerDistances = ((ChunkSystemServerPlayer)this.player).moonrise$getViewDistanceHolder().getViewDistances();
+ final ViewDistances worldDistances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances();
+
+ final int tickViewDistance = getTickDistance(
+ playerDistances.tickViewDistance, worldDistances.tickViewDistance,
+ playerDistances.loadViewDistance, worldDistances.loadViewDistance
+ );
+ // load view cannot be less-than tick view + 1
+ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance);
+ // send view cannot be greater-than load view
+ final int clientViewDistance = getClientViewDistance(this.player);
+ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance);
+
+ final ChunkPos playerPos = this.player.chunkPosition();
+ final boolean canGenerateChunks = this.canPlayerGenerateChunks();
+ final int currentChunkX = playerPos.x;
+ final int currentChunkZ = playerPos.z;
+
+ final int prevChunkX = this.lastChunkX;
+ final int prevChunkZ = this.lastChunkZ;
+
+ if (
+ // has view distance stayed the same?
+ sendViewDistance == this.lastSendDistance
+ && loadViewDistance == this.lastLoadDistance
+ && tickViewDistance == this.lastTickDistance
+
+ // has our chunk stayed the same?
+ && prevChunkX == currentChunkX
+ && prevChunkZ == currentChunkZ
+
+ // can we still generate chunks?
+ && this.canGenerateChunks == canGenerateChunks
+ ) {
+ // nothing we care about changed, so we're not re-calculating
+ return;
+ }
+
+ // update distance maps
+ this.broadcastMap.update(currentChunkX, currentChunkZ, sendViewDistance + 1);
+ this.loadTicketCleanup.update(currentChunkX, currentChunkZ, loadViewDistance + 1);
+ this.tickMap.update(currentChunkX, currentChunkZ, tickViewDistance);
+ if (sendViewDistance > loadViewDistance || tickViewDistance > loadViewDistance) {
+ throw new IllegalStateException();
+ }
+
+ // update VDs for client
+ // this should be after the distance map updates, as they will send unload packets
+ if (this.lastSentChunkRadius != sendViewDistance) {
+ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance));
+ }
+ if (this.lastSentSimulationDistance != tickViewDistance) {
+ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance));
+ }
+
+ this.sendQueue.clear();
+ this.tickingQueue.clear();
+ this.generatingQueue.clear();
+ this.genQueue.clear();
+ this.loadingQueue.clear();
+ this.loadQueue.clear();
+
+ this.lastChunkX = currentChunkX;
+ this.lastChunkZ = currentChunkZ;
+ this.lastSendDistance = sendViewDistance;
+ this.lastLoadDistance = loadViewDistance;
+ this.lastTickDistance = tickViewDistance;
+ this.canGenerateChunks = canGenerateChunks;
+
+ // +1 since we need to load chunks +1 around the load view distance...
+ final long[] toIterate = ParallelSearchRadiusIteration.getSearchIteration(loadViewDistance + 1);
+ // the iteration order is by increasing manhattan distance - so, we do NOT need to
+ // sort anything in the queue!
+ for (final long deltaChunk : toIterate) {
+ final int dx = CoordinateUtils.getChunkX(deltaChunk);
+ final int dz = CoordinateUtils.getChunkZ(deltaChunk);
+ final int chunkX = dx + currentChunkX;
+ final int chunkZ = dz + currentChunkZ;
+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz));
+ final int manhattanDistance = Math.abs(dx) + Math.abs(dz);
+
+ // since chunk sending is not by radius alone, we need an extra check here to account for
+ // everything <= sendDistance
+ // Note: Vanilla may want to send chunks outside the send view distance, so we do need
+ // the dist <= view check
+ final boolean sendChunk = (squareDistance <= (sendViewDistance + 1))
+ && wantChunkLoaded(currentChunkX, currentChunkZ, chunkX, chunkZ, sendViewDistance);
+ final boolean sentChunk = sendChunk ? this.sentChunks.contains(chunk) : this.sentChunks.remove(chunk);
+
+ if (!sendChunk && sentChunk) {
+ // have sent the chunk, but don't want it anymore
+ // unload it now
+ this.sendUnloadChunkRaw(chunkX, chunkZ);
+ }
+
+ final byte stage = this.chunkTicketStage.get(chunk);
+ switch (stage) {
+ case CHUNK_TICKET_STAGE_NONE: {
+ // we want the chunk to be at least loaded
+ this.loadQueue.enqueue(chunk);
+ break;
+ }
+ case CHUNK_TICKET_STAGE_LOADING: {
+ this.loadingQueue.enqueue(chunk);
+ break;
+ }
+ case CHUNK_TICKET_STAGE_LOADED: {
+ if (canGenerateChunks || this.isLoadedChunkGeneratable(chunkX, chunkZ)) {
+ this.genQueue.enqueue(chunk);
+ }
+ break;
+ }
+ case CHUNK_TICKET_STAGE_GENERATING: {
+ this.generatingQueue.enqueue(chunk);
+ break;
+ }
+ case CHUNK_TICKET_STAGE_GENERATED: {
+ if (sendChunk && !sentChunk) {
+ this.sendQueue.enqueue(chunk);
+ }
+ if (squareDistance <= tickViewDistance) {
+ this.tickingQueue.enqueue(chunk);
+ }
+ break;
+ }
+ case CHUNK_TICKET_STAGE_TICK: {
+ if (sendChunk && !sentChunk) {
+ this.sendQueue.enqueue(chunk);
+ }
+ break;
+ }
+ default: {
+ throw new IllegalStateException("Unknown stage: " + stage);
+ }
+ }
+ }
+
+ // update the chunk center
+ // this must be done last so that the client does not ignore any of our unload chunk packets above
+ if (this.lastSentChunkCenterX != currentChunkX || this.lastSentChunkCenterZ != currentChunkZ) {
+ this.player.connection.send(this.updateClientChunkCenter(currentChunkX, currentChunkZ));
+ }
+
+ this.flushDelayedTicketOps();
+ }
+
+ void remove() {
+ io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot add player asynchronously");
+ if (this.removed) {
+ throw new IllegalStateException("Removing removed player chunk loader");
+ }
+ this.removed = true;
+ // sends the chunk unload packets
+ this.broadcastMap.remove();
+ // cleans up loading/generating tickets
+ this.loadTicketCleanup.remove();
+ // cleans up ticking tickets
+ this.tickMap.remove();
+
+ // purge queues
+ this.sendQueue.clear();
+ this.tickingQueue.clear();
+ this.generatingQueue.clear();
+ this.genQueue.clear();
+ this.loadingQueue.clear();
+ this.loadQueue.clear();
+
+ // flush ticket changes
+ this.flushDelayedTicketOps();
+
+ // now all tickets should be removed, which is all of our external state
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
@@ -0,0 +1,144 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.queue;
+
+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import it.unimi.dsi.fastutil.longs.LongIterator;
+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+public final class ChunkUnloadQueue {
+
+ public final int coordinateShift;
+ private final AtomicLong orderGenerator = new AtomicLong();
+ private final ConcurrentLong2ReferenceChainedHashTable<UnloadSection> unloadSections = new ConcurrentLong2ReferenceChainedHashTable<>();
+
+ /*
+ * Note: write operations do not occur in parallel for any given section.
+ * Note: coordinateShift <= region shift in order for retrieveForCurrentRegion() to function correctly
+ */
+
+ public ChunkUnloadQueue(final int coordinateShift) {
+ this.coordinateShift = coordinateShift;
+ }
+
+ public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {}
+
+ public List<SectionToUnload> retrieveForAllRegions() {
+ final List<SectionToUnload> ret = new ArrayList<>();
+
+ for (final Iterator<ConcurrentLong2ReferenceChainedHashTable.TableEntry<UnloadSection>> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) {
+ final ConcurrentLong2ReferenceChainedHashTable.TableEntry<UnloadSection> entry = iterator.next();
+ final long key = entry.getKey();
+ final UnloadSection section = entry.getValue();
+ final int sectionX = CoordinateUtils.getChunkX(key);
+ final int sectionZ = CoordinateUtils.getChunkZ(key);
+
+ ret.add(new SectionToUnload(sectionX, sectionZ, section.order, section.chunks.size()));
+ }
+
+ ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> {
+ return Long.compare(s1.order, s2.order);
+ });
+
+ return ret;
+ }
+
+ public UnloadSection getSectionUnsynchronized(final int sectionX, final int sectionZ) {
+ return this.unloadSections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ));
+ }
+
+ public UnloadSection removeSection(final int sectionX, final int sectionZ) {
+ return this.unloadSections.remove(CoordinateUtils.getChunkKey(sectionX, sectionZ));
+ }
+
+ // write operation
+ public boolean addChunk(final int chunkX, final int chunkZ) {
+ // write operations do not occur in parallel for a given section
+ final int shift = this.coordinateShift;
+ final int sectionX = chunkX >> shift;
+ final int sectionZ = chunkZ >> shift;
+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ UnloadSection section = this.unloadSections.get(sectionKey);
+ if (section == null) {
+ section = new UnloadSection(this.orderGenerator.getAndIncrement());
+ this.unloadSections.put(sectionKey, section);
+ }
+
+ return section.chunks.add(chunkKey);
+ }
+
+ // write operation
+ public boolean removeChunk(final int chunkX, final int chunkZ) {
+ // write operations do not occur in parallel for a given section
+ final int shift = this.coordinateShift;
+ final int sectionX = chunkX >> shift;
+ final int sectionZ = chunkZ >> shift;
+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ final UnloadSection section = this.unloadSections.get(sectionKey);
+
+ if (section == null) {
+ return false;
+ }
+
+ if (!section.chunks.remove(chunkKey)) {
+ return false;
+ }
+
+ if (section.chunks.isEmpty()) {
+ this.unloadSections.remove(sectionKey);
+ }
+
+ return true;
+ }
+
+ public JsonElement toDebugJson() {
+ final JsonArray ret = new JsonArray();
+
+ for (final SectionToUnload section : this.retrieveForAllRegions()) {
+ final JsonObject sectionJson = new JsonObject();
+ ret.add(sectionJson);
+
+ sectionJson.addProperty("sectionX", section.sectionX());
+ sectionJson.addProperty("sectionZ", section.sectionX());
+ sectionJson.addProperty("order", section.order());
+
+ final JsonArray coordinates = new JsonArray();
+ sectionJson.add("coordinates", coordinates);
+
+ final UnloadSection actualSection = this.getSectionUnsynchronized(section.sectionX(), section.sectionZ());
+ if (actualSection != null) {
+ for (final LongIterator iterator = actualSection.chunks.clone().iterator(); iterator.hasNext(); ) {
+ final long coordinate = iterator.nextLong();
+
+ final JsonObject coordinateJson = new JsonObject();
+ coordinates.add(coordinateJson);
+
+ coordinateJson.addProperty("chunkX", Integer.valueOf(CoordinateUtils.getChunkX(coordinate)));
+ coordinateJson.addProperty("chunkZ", Integer.valueOf(CoordinateUtils.getChunkZ(coordinate)));
+ }
+ }
+ }
+
+ return ret;
+ }
+
+ public static final class UnloadSection {
+
+ public final long order;
+ public final LongLinkedOpenHashSet chunks = new LongLinkedOpenHashSet();
+
+ public UnloadSection(final long order) {
+ this.order = order;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..3d902f382977a194e09986419391c3ca1568885c
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
@@ -0,0 +1,1425 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem;
+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk;
+import ca.spottedleaf.moonrise.patches.chunk_system.queue.ChunkUnloadQueue;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.GenericDataLoadTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket;
+import ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.mojang.logging.LogUtils;
+import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ByteMap;
+import it.unimi.dsi.fastutil.longs.Long2IntMap;
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongIterator;
+import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.ChunkLevel;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.Ticket;
+import net.minecraft.server.level.TicketType;
+import net.minecraft.util.SortedArraySet;
+import net.minecraft.util.Unit;
+import net.minecraft.world.level.ChunkPos;
+import org.slf4j.Logger;
+import java.io.IOException;
+import java.text.DecimalFormat;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.PrimitiveIterator;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.LockSupport;
+import java.util.function.Predicate;
+
+public final class ChunkHolderManager {
+
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+
+ public static final int FULL_LOADED_TICKET_LEVEL = ChunkLevel.FULL_CHUNK_LEVEL;
+ public static final int BLOCK_TICKING_TICKET_LEVEL = ChunkLevel.BLOCK_TICKING_LEVEL;
+ public static final int ENTITY_TICKING_TICKET_LEVEL = ChunkLevel.ENTITY_TICKING_LEVEL;
+ public static final int MAX_TICKET_LEVEL = ChunkLevel.MAX_LEVEL; // inclusive
+
+ public static final TicketType<Unit> UNLOAD_COOLDOWN = TicketType.create("unload_cooldown", (u1, u2) -> 0, 5 * 20);
+
+ private static final long NO_TIMEOUT_MARKER = Long.MIN_VALUE;
+ private static final long PROBE_MARKER = Long.MIN_VALUE + 1;
+ public final ReentrantAreaLock ticketLockArea;
+
+ private final ConcurrentLong2ReferenceChainedHashTable<SortedArraySet<Ticket<?>>> tickets = new ConcurrentLong2ReferenceChainedHashTable<>();
+ private final ConcurrentLong2ReferenceChainedHashTable<Long2IntOpenHashMap> sectionToChunkToExpireCount = new ConcurrentLong2ReferenceChainedHashTable<>();
+ final ChunkUnloadQueue unloadQueue;
+
+ private final ConcurrentLong2ReferenceChainedHashTable<NewChunkHolder> chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f);
+ private final ServerLevel world;
+ private final ChunkTaskScheduler taskScheduler;
+ private long currentTick;
+
+ private final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>();
+ private final ObjectRBTreeSet<NewChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
+ if (c1 == c2) {
+ return 0;
+ }
+
+ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
+
+ if (saveTickCompare != 0) {
+ return saveTickCompare;
+ }
+
+ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
+ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
+
+ if (coord1 == coord2) {
+ throw new IllegalStateException("Duplicate chunkholder in auto save queue");
+ }
+
+ return Long.compare(coord1, coord2);
+ });
+
+ public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
+ this.world = world;
+ this.taskScheduler = taskScheduler;
+ this.ticketLockArea = new ReentrantAreaLock(taskScheduler.getChunkSystemLockShift());
+ this.unloadQueue = new ChunkUnloadQueue(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift());
+ }
+
+ public boolean processTicketUpdates(final int posX, final int posZ) {
+ final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT;
+ final int ticketMask = (1 << ticketShift) - 1;
+ final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>();
+ final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
+ final boolean ret;
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
+ ((posX >> ticketShift) - 1) << ticketShift,
+ ((posZ >> ticketShift) - 1) << ticketShift,
+ (((posX >> ticketShift) + 1) << ticketShift) | ticketMask,
+ (((posZ >> ticketShift) + 1) << ticketShift) | ticketMask
+ );
+ try {
+ ret = this.processTicketUpdatesNoLock(posX >> ticketShift, posZ >> ticketShift, scheduledTasks, changedFullStatus);
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+
+ this.addChangedStatuses(changedFullStatus);
+
+ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) {
+ scheduledTasks.get(i).schedule();
+ }
+
+ return ret;
+ }
+
+ private boolean processTicketUpdatesNoLock(final int sectionX, final int sectionZ, final List<ChunkProgressionTask> scheduledTasks,
+ final List<NewChunkHolder> changedFullStatus) {
+ return this.ticketLevelPropagator.performUpdate(
+ sectionX, sectionZ, this.taskScheduler.schedulingLockArea, scheduledTasks, changedFullStatus
+ );
+ }
+
+ public List<ChunkHolder> getOldChunkHolders() {
+ final List<ChunkHolder> ret = new ArrayList<>(this.chunkHolders.size() + 1);
+ for (final Iterator<NewChunkHolder> iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) {
+ ret.add(iterator.next().vanillaChunkHolder);
+ }
+ return ret;
+ }
+
+ public List<NewChunkHolder> getChunkHolders() {
+ final List<NewChunkHolder> ret = new ArrayList<>(this.chunkHolders.size() + 1);
+ for (final Iterator<NewChunkHolder> iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) {
+ ret.add(iterator.next());
+ }
+ return ret;
+ }
+
+ public int size() {
+ return this.chunkHolders.size();
+ }
+
+ // TODO replace the need for this, specifically: optimise ServerChunkCache#tickChunks
+ public Iterable<ChunkHolder> getOldChunkHoldersIterable() {
+ return new Iterable<ChunkHolder>() {
+ @Override
+ public Iterator<ChunkHolder> iterator() {
+ final Iterator<NewChunkHolder> iterator = ChunkHolderManager.this.chunkHolders.valueIterator();
+ return new Iterator<ChunkHolder>() {
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public ChunkHolder next() {
+ return iterator.next().vanillaChunkHolder;
+ }
+ };
+ }
+ };
+ }
+
+ public void close(final boolean save, final boolean halt) {
+ io.papermc.paper.util.TickThread.ensureTickThread("Closing world off-main");
+ if (halt) {
+ LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'");
+ if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) {
+ LOGGER.warn("Failed to halt world generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'");
+ } else {
+ LOGGER.info("Halted chunk system for world '" + WorldUtil.getWorldName(this.world) + "'");
+ }
+ }
+
+ if (save) {
+ this.saveAllChunks(true, true, true);
+ }
+
+ boolean hasTasks = false;
+ for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) {
+ if (RegionFileIOThread.getControllerFor(this.world, type).hasTasks()) {
+ hasTasks = true;
+ break;
+ }
+ }
+ if (hasTasks) {
+ RegionFileIOThread.flush();
+ }
+
+ // kill regionfile cache
+ for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) {
+ try {
+ RegionFileIOThread.getControllerFor(this.world, type).getCache().close();
+ } catch (final IOException ex) {
+ LOGGER.error("Failed to close '" + type.name() + "' regionfile cache for world '" + WorldUtil.getWorldName(this.world) + "'", ex);
+ }
+ }
+ }
+
+ void ensureInAutosave(final NewChunkHolder holder) {
+ if (!this.autoSaveQueue.contains(holder)) {
+ holder.lastAutoSave = this.currentTick;
+ this.autoSaveQueue.add(holder);
+ }
+ }
+
+ public void autoSave() {
+ final List<NewChunkHolder> reschedule = new ArrayList<>();
+ final long currentTick = this.currentTick;
+ final long maxSaveTime = currentTick - Math.max(1L, this.world.paperConfig().chunks.autoSaveInterval.value());
+ final int maxToSave = this.world.paperConfig().chunks.maxAutoSaveChunksPerTick;
+ for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) {
+ final NewChunkHolder holder = this.autoSaveQueue.first();
+
+ if (holder.lastAutoSave > maxSaveTime) {
+ break;
+ }
+
+ this.autoSaveQueue.remove(holder);
+
+ holder.lastAutoSave = currentTick;
+ if (holder.save(false) != null) {
+ ++autoSaved;
+ }
+
+ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) {
+ reschedule.add(holder);
+ }
+ }
+
+ for (final NewChunkHolder holder : reschedule) {
+ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) {
+ this.autoSaveQueue.add(holder);
+ }
+ }
+ }
+
+ public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) {
+ final List<NewChunkHolder> holders = this.getChunkHolders();
+
+ if (logProgress) {
+ LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'");
+ }
+
+ final DecimalFormat format = new DecimalFormat("#0.00");
+
+ int saved = 0;
+
+ long start = System.nanoTime();
+ long lastLog = start;
+ boolean needsFlush = false;
+ final int flushInterval = 50;
+
+ int savedChunk = 0;
+ int savedEntity = 0;
+ int savedPoi = 0;
+
+ for (int i = 0, len = holders.size(); i < len; ++i) {
+ final NewChunkHolder holder = holders.get(i);
+ try {
+ final NewChunkHolder.SaveStat saveStat = holder.save(shutdown);
+ if (saveStat != null) {
+ ++saved;
+ needsFlush = flush;
+ if (saveStat.savedChunk()) {
+ ++savedChunk;
+ }
+ if (saveStat.savedEntityChunk()) {
+ ++savedEntity;
+ }
+ if (saveStat.savedPoiChunk()) {
+ ++savedPoi;
+ }
+ }
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to save chunk (" + holder.chunkX + "," + holder.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr);
+ }
+ if (needsFlush && (saved % flushInterval) == 0) {
+ needsFlush = false;
+ RegionFileIOThread.partialFlush(flushInterval / 2);
+ }
+ if (logProgress) {
+ final long currTime = System.nanoTime();
+ if ((currTime - lastLog) > TimeUnit.SECONDS.toNanos(10L)) {
+ lastLog = currTime;
+ LOGGER.info("Saved " + saved + " chunks (" + format.format((double)(i+1)/(double)len * 100.0) + "%) in world '" + WorldUtil.getWorldName(this.world) + "'");
+ }
+ }
+ }
+ if (flush) {
+ RegionFileIOThread.flush();
+ try {
+ RegionFileIOThread.flushRegionStorages(this.world);
+ } catch (final IOException ex) {
+ LOGGER.error("Exception when flushing regions in world '" + WorldUtil.getWorldName(this.world) + "'", ex);
+ }
+ }
+ if (logProgress) {
+ LOGGER.info("Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "' in " + format.format(1.0E-9 * (System.nanoTime() - start)) + "s");
+ }
+ }
+
+ private final ThreadedTicketLevelPropagator ticketLevelPropagator = new ThreadedTicketLevelPropagator() {
+ @Override
+ protected void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates) {
+ // first the necessary chunkholders must be created, so just update the ticket levels
+ for (final Iterator<Long2ByteMap.Entry> iterator = updates.long2ByteEntrySet().fastIterator(); iterator.hasNext();) {
+ final Long2ByteMap.Entry entry = iterator.next();
+ final long key = entry.getLongKey();
+ final int newLevel = convertBetweenTicketLevels((int)entry.getByteValue());
+
+ NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key);
+ if (current == null && newLevel > MAX_TICKET_LEVEL) {
+ // not loaded and it shouldn't be loaded!
+ iterator.remove();
+ continue;
+ }
+
+ final int currentLevel = current == null ? MAX_TICKET_LEVEL + 1 : current.getCurrentTicketLevel();
+ if (currentLevel == newLevel) {
+ // nothing to do
+ iterator.remove();
+ continue;
+ }
+
+ if (current == null) {
+ // must create
+ current = ChunkHolderManager.this.createChunkHolder(key);
+ ChunkHolderManager.this.chunkHolders.put(key, current);
+ current.updateTicketLevel(newLevel);
+ } else {
+ current.updateTicketLevel(newLevel);
+ }
+ }
+ }
+
+ @Override
+ protected void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List<ChunkProgressionTask> scheduledTasks,
+ final List<NewChunkHolder> changedFullStatus) {
+ final List<ChunkProgressionTask> prev = CURRENT_TICKET_UPDATE_SCHEDULING.get();
+ CURRENT_TICKET_UPDATE_SCHEDULING.set(scheduledTasks);
+ try {
+ for (final LongIterator iterator = updates.keySet().iterator(); iterator.hasNext();) {
+ final long key = iterator.nextLong();
+ final NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key);
+
+ if (current == null) {
+ throw new IllegalStateException("Expected chunk holder to be created");
+ }
+
+ current.processTicketLevelUpdate(scheduledTasks, changedFullStatus);
+ }
+ } finally {
+ CURRENT_TICKET_UPDATE_SCHEDULING.set(prev);
+ }
+ }
+ };
+ // function for converting between ticket levels and propagator levels and vice versa
+ // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects
+ // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator
+ // and the levels we get out of the propagator
+
+ public static int convertBetweenTicketLevels(final int level) {
+ return ChunkLevel.MAX_LEVEL - level + 1;
+ }
+
+ public String getTicketDebugString(final long coordinate) {
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate));
+ try {
+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coordinate);
+
+ return tickets != null ? tickets.first().toString() : "no_ticket";
+ } finally {
+ if (ticketLock != null) {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+ }
+
+ public Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> getTicketsCopy() {
+ final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> ret = new Long2ObjectOpenHashMap<>();
+ final Long2ObjectOpenHashMap<LongArrayList> sections = new Long2ObjectOpenHashMap<>();
+ final int sectionShift = this.taskScheduler.getChunkSystemLockShift();
+ for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) {
+ final long coord = iterator.nextLong();
+ sections.computeIfAbsent(
+ CoordinateUtils.getChunkKey(
+ CoordinateUtils.getChunkX(coord) >> sectionShift,
+ CoordinateUtils.getChunkZ(coord) >> sectionShift
+ ),
+ (final long keyInMap) -> {
+ return new LongArrayList();
+ }
+ ).add(coord);
+ }
+
+ for (final Iterator<Long2ObjectMap.Entry<LongArrayList>> iterator = sections.long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ObjectMap.Entry<LongArrayList> entry = iterator.next();
+ final long sectionKey = entry.getLongKey();
+ final LongArrayList coordinates = entry.getValue();
+
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
+ CoordinateUtils.getChunkX(sectionKey) << sectionShift,
+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift
+ );
+ try {
+ for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) {
+ final long coord = iterator2.nextLong();
+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coord);
+ if (tickets == null) {
+ // removed before we acquired lock
+ continue;
+ }
+ ret.put(coord, ((ChunkSystemSortedArraySet<Ticket<?>>)tickets).moonrise$copy());
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+
+ return ret;
+ }
+
+ // Paper start
+ public Collection<org.bukkit.plugin.Plugin> getPluginChunkTickets(int x, int z) {
+ com.google.common.collect.ImmutableList.Builder<org.bukkit.plugin.Plugin> ret;
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(x, z);
+ try {
+ final long coordinate = CoordinateUtils.getChunkKey(x, z);
+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coordinate);
+
+ if (tickets == null) {
+ return java.util.Collections.emptyList();
+ }
+
+ ret = com.google.common.collect.ImmutableList.builder();
+ for (Ticket<?> ticket : tickets) {
+ if (ticket.getType() == TicketType.PLUGIN_TICKET) {
+ ret.add((org.bukkit.plugin.Plugin)ticket.key);
+ }
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+
+ return ret.build();
+ }
+ // Paper end
+
+ protected final void updateTicketLevel(final long coordinate, final int ticketLevel) {
+ if (ticketLevel > ChunkLevel.MAX_LEVEL) {
+ this.ticketLevelPropagator.removeSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate));
+ } else {
+ this.ticketLevelPropagator.setSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate), convertBetweenTicketLevels(ticketLevel));
+ }
+ }
+
+ private static int getTicketLevelAt(SortedArraySet<Ticket<?>> tickets) {
+ return !tickets.isEmpty() ? tickets.first().getTicketLevel() : MAX_TICKET_LEVEL + 1;
+ }
+
+ public <T> boolean addTicketAtLevel(final TicketType<T> type, final ChunkPos chunkPos, final int level,
+ final T identifier) {
+ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier);
+ }
+
+ public <T> boolean addTicketAtLevel(final TicketType<T> type, final int chunkX, final int chunkZ, final int level,
+ final T identifier) {
+ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier);
+ }
+
+ private void addExpireCount(final int chunkX, final int chunkZ) {
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
+ final long sectionKey = CoordinateUtils.getChunkKey(
+ chunkX >> sectionShift,
+ chunkZ >> sectionShift
+ );
+
+ this.sectionToChunkToExpireCount.computeIfAbsent(sectionKey, (final long keyInMap) -> {
+ return new Long2IntOpenHashMap();
+ }).addTo(chunkKey, 1);
+ }
+
+ private void removeExpireCount(final int chunkX, final int chunkZ) {
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
+ final long sectionKey = CoordinateUtils.getChunkKey(
+ chunkX >> sectionShift,
+ chunkZ >> sectionShift
+ );
+
+ final Long2IntOpenHashMap removeCounts = this.sectionToChunkToExpireCount.get(sectionKey);
+ final int prevCount = removeCounts.addTo(chunkKey, -1);
+
+ if (prevCount == 1) {
+ removeCounts.remove(chunkKey);
+ if (removeCounts.isEmpty()) {
+ this.sectionToChunkToExpireCount.remove(sectionKey);
+ }
+ }
+ }
+
+ // supposed to return true if the ticket was added and did not replace another
+ // but, we always return false if the ticket cannot be added
+ public <T> boolean addTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier) {
+ return this.addTicketAtLevel(type, chunk, level, identifier, true);
+ }
+
+ <T> boolean addTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier, final boolean lock) {
+ final long removeDelay = type.timeout <= 0 ? NO_TIMEOUT_MARKER : type.timeout;
+ if (level > MAX_TICKET_LEVEL) {
+ return false;
+ }
+
+ final int chunkX = CoordinateUtils.getChunkX(chunk);
+ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
+ final Ticket<T> ticket = new Ticket<>(type, level, identifier);
+ ((ChunkSystemTicket<T>)(Object)ticket).moonrise$setRemoveDelay(removeDelay);
+
+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null;
+ try {
+ final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> {
+ return SortedArraySet.create(4);
+ });
+
+ final int levelBefore = getTicketLevelAt(ticketsAtChunk);
+ final Ticket<T> current = (Ticket<T>)((ChunkSystemSortedArraySet<Ticket<?>>)ticketsAtChunk).moonrise$replace(ticket);
+ final int levelAfter = getTicketLevelAt(ticketsAtChunk);
+
+ if (current != ticket) {
+ final long oldRemoveDelay = ((ChunkSystemTicket<T>)(Object)current).moonrise$getRemoveDelay();
+ if (removeDelay != oldRemoveDelay) {
+ if (oldRemoveDelay != NO_TIMEOUT_MARKER && removeDelay == NO_TIMEOUT_MARKER) {
+ this.removeExpireCount(chunkX, chunkZ);
+ } else if (oldRemoveDelay == NO_TIMEOUT_MARKER) {
+ // since old != new, we have that NO_TIMEOUT_MARKER != new
+ this.addExpireCount(chunkX, chunkZ);
+ }
+ }
+ } else {
+ if (removeDelay != NO_TIMEOUT_MARKER) {
+ this.addExpireCount(chunkX, chunkZ);
+ }
+ }
+
+ if (levelBefore != levelAfter) {
+ this.updateTicketLevel(chunk, levelAfter);
+ }
+
+ return current == ticket;
+ } finally {
+ if (ticketLock != null) {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+ }
+
+ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final ChunkPos chunkPos, final int level, final T identifier) {
+ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier);
+ }
+
+ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final int chunkX, final int chunkZ, final int level, final T identifier) {
+ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier);
+ }
+
+ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier) {
+ return this.removeTicketAtLevel(type, chunk, level, identifier, true);
+ }
+
+ <T> boolean removeTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier, final boolean lock) {
+ if (level > MAX_TICKET_LEVEL) {
+ return false;
+ }
+
+ final int chunkX = CoordinateUtils.getChunkX(chunk);
+ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
+ final Ticket<T> probe = new Ticket<>(type, level, identifier);
+
+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null;
+ try {
+ final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.get(chunk);
+ if (ticketsAtChunk == null) {
+ return false;
+ }
+
+ final int oldLevel = getTicketLevelAt(ticketsAtChunk);
+ final Ticket<T> ticket = (Ticket<T>)((ChunkSystemSortedArraySet<Ticket<?>>)ticketsAtChunk).moonrise$removeAndGet(probe);
+
+ if (ticket == null) {
+ return false;
+ }
+
+ final int newLevel = getTicketLevelAt(ticketsAtChunk);
+ // we should not change the ticket levels while the target region may be ticking
+ if (oldLevel != newLevel) {
+ final Ticket<ChunkPos> unknownTicket = new Ticket<>(TicketType.UNKNOWN, level, new ChunkPos(chunk));
+ ((ChunkSystemTicket<ChunkPos>)(Object)unknownTicket).moonrise$setRemoveDelay(Math.max(1, TicketType.UNKNOWN.timeout));
+ if (ticketsAtChunk.add(unknownTicket)) {
+ this.addExpireCount(chunkX, chunkZ);
+ } else {
+ throw new IllegalStateException("Should have been able to add " + unknownTicket + " to " + ticketsAtChunk);
+ }
+ }
+
+ final long removeDelay = ((ChunkSystemTicket<T>)(Object)ticket).moonrise$getRemoveDelay();
+ if (removeDelay != NO_TIMEOUT_MARKER) {
+ this.removeExpireCount(chunkX, chunkZ);
+ }
+
+ return true;
+ } finally {
+ if (ticketLock != null) {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+ }
+
+ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk
+ public <T, V> void addAndRemoveTickets(final long chunk, final TicketType<T> addType, final int addLevel, final T addIdentifier,
+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk));
+ try {
+ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false);
+ this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false);
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+
+ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk
+ public <T, V> boolean addIfRemovedTicket(final long chunk, final TicketType<T> addType, final int addLevel, final T addIdentifier,
+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk));
+ try {
+ if (this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false)) {
+ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false);
+ return true;
+ }
+ return false;
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+
+ public <T> void removeAllTicketsFor(final TicketType<T> ticketType, final int ticketLevel, final T ticketIdentifier) {
+ if (ticketLevel > MAX_TICKET_LEVEL) {
+ return;
+ }
+
+ final Long2ObjectOpenHashMap<LongArrayList> sections = new Long2ObjectOpenHashMap<>();
+ final int sectionShift = this.taskScheduler.getChunkSystemLockShift();
+ for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) {
+ final long coord = iterator.nextLong();
+ sections.computeIfAbsent(
+ CoordinateUtils.getChunkKey(
+ CoordinateUtils.getChunkX(coord) >> sectionShift,
+ CoordinateUtils.getChunkZ(coord) >> sectionShift
+ ),
+ (final long keyInMap) -> {
+ return new LongArrayList();
+ }
+ ).add(coord);
+ }
+
+ for (final Iterator<Long2ObjectMap.Entry<LongArrayList>> iterator = sections.long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ObjectMap.Entry<LongArrayList> entry = iterator.next();
+ final long sectionKey = entry.getLongKey();
+ final LongArrayList coordinates = entry.getValue();
+
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
+ CoordinateUtils.getChunkX(sectionKey) << sectionShift,
+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift
+ );
+ try {
+ for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) {
+ final long coord = iterator2.nextLong();
+ this.removeTicketAtLevel(ticketType, coord, ticketLevel, ticketIdentifier, false);
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+ }
+
+ public void tick() {
+ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
+
+ final Predicate<Ticket<?>> expireNow = (final Ticket<?> ticket) -> {
+ long removeDelay = ((ChunkSystemTicket<?>)(Object)ticket).moonrise$getRemoveDelay();
+ if (removeDelay == NO_TIMEOUT_MARKER) {
+ return false;
+ }
+ --removeDelay;
+ ((ChunkSystemTicket<?>)(Object)ticket).moonrise$setRemoveDelay(removeDelay);
+ return removeDelay <= 0L;
+ };
+
+ for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) {
+ final long sectionKey = iterator.nextLong();
+
+ if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) {
+ // removed concurrently
+ continue;
+ }
+
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
+ CoordinateUtils.getChunkX(sectionKey) << sectionShift,
+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift
+ );
+
+ try {
+ final Long2IntOpenHashMap chunkToExpireCount = this.sectionToChunkToExpireCount.get(sectionKey);
+ if (chunkToExpireCount == null) {
+ // lost to some race
+ continue;
+ }
+
+ for (final Iterator<Long2IntMap.Entry> iterator1 = chunkToExpireCount.long2IntEntrySet().fastIterator(); iterator1.hasNext();) {
+ final Long2IntMap.Entry entry = iterator1.next();
+
+ final long chunkKey = entry.getLongKey();
+ final int expireCount = entry.getIntValue();
+
+ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(chunkKey);
+ final int levelBefore = getTicketLevelAt(tickets);
+
+ final int sizeBefore = tickets.size();
+ tickets.removeIf(expireNow);
+ final int sizeAfter = tickets.size();
+ final int levelAfter = getTicketLevelAt(tickets);
+
+ if (tickets.isEmpty()) {
+ this.tickets.remove(chunkKey);
+ }
+ if (levelBefore != levelAfter) {
+ this.updateTicketLevel(chunkKey, levelAfter);
+ }
+
+ final int newExpireCount = expireCount - (sizeBefore - sizeAfter);
+
+ if (newExpireCount == expireCount) {
+ continue;
+ }
+
+ if (newExpireCount != 0) {
+ entry.setValue(newExpireCount);
+ } else {
+ iterator1.remove();
+ }
+ }
+
+ if (chunkToExpireCount.isEmpty()) {
+ this.sectionToChunkToExpireCount.remove(sectionKey);
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+ }
+
+ this.processTicketUpdates();
+ }
+
+ public NewChunkHolder getChunkHolder(final int chunkX, final int chunkZ) {
+ return this.chunkHolders.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+
+ public NewChunkHolder getChunkHolder(final long position) {
+ return this.chunkHolders.get(position);
+ }
+
+ public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
+ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z);
+ if (chunkHolder != null) {
+ chunkHolder.raisePriority(priority);
+ }
+ }
+
+ public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
+ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z);
+ if (chunkHolder != null) {
+ chunkHolder.setPriority(priority);
+ }
+ }
+
+ public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
+ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z);
+ if (chunkHolder != null) {
+ chunkHolder.lowerPriority(priority);
+ }
+ }
+
+ private NewChunkHolder createChunkHolder(final long position) {
+ final NewChunkHolder ret = new NewChunkHolder(this.world, CoordinateUtils.getChunkX(position), CoordinateUtils.getChunkZ(position), this.taskScheduler);
+
+ ChunkSystem.onChunkHolderCreate(this.world, ret.vanillaChunkHolder);
+
+ return ret;
+ }
+
+ // because this function creates the chunk holder without a ticket, it is the caller's responsibility to ensure
+ // the chunk holder eventually unloads. this should only be used to avoid using processTicketUpdates to create chunkholders,
+ // as processTicketUpdates may call plugin logic; in every other case a ticket is appropriate
+ private NewChunkHolder getOrCreateChunkHolder(final int chunkX, final int chunkZ) {
+ return this.getOrCreateChunkHolder(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+
+ private NewChunkHolder getOrCreateChunkHolder(final long position) {
+ final int chunkX = CoordinateUtils.getChunkX(position);
+ final int chunkZ = CoordinateUtils.getChunkZ(position);
+
+ if (!this.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ)) {
+ throw new IllegalStateException("Must hold ticket level update lock!");
+ }
+ if (!this.taskScheduler.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ)) {
+ throw new IllegalStateException("Must hold scheduler lock!!");
+ }
+
+ // we could just acquire these locks, but...
+ // must own the locks because the caller needs to ensure that no unload can occur AFTER this function returns
+
+ NewChunkHolder current = this.chunkHolders.get(position);
+ if (current != null) {
+ return current;
+ }
+
+ current = this.createChunkHolder(position);
+ this.chunkHolders.put(position, current);
+
+
+ return current;
+ }
+
+ public ChunkEntitySlices getOrCreateEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main");
+ ChunkEntitySlices ret;
+
+ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ);
+ if (current != null && (ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) {
+ return ret;
+ }
+
+ final AtomicBoolean isCompleted = new AtomicBoolean();
+ final Thread waiter = Thread.currentThread();
+ final Long entityLoadId = ChunkTaskScheduler.getNextEntityLoadId();
+ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null;
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ);
+ try {
+ this.addTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
+ final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ);
+ try {
+ current = this.getOrCreateChunkHolder(chunkX, chunkZ);
+ if ((ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) {
+ this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
+ return ret;
+ }
+
+ if (!transientChunk) {
+ if (current.isEntityChunkNBTLoaded()) {
+ isCompleted.setPlain(true);
+ } else {
+ loadTask = current.getOrLoadEntityData((final GenericDataLoadTask.TaskResult<CompoundTag, Throwable> result) -> {
+ isCompleted.set(true);
+ LockSupport.unpark(waiter);
+ });
+ final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask();
+
+ if (entityLoad != null) {
+ entityLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
+ }
+ }
+ }
+ } finally {
+ this.taskScheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+
+ if (loadTask != null) {
+ loadTask.schedule();
+ }
+
+ if (!transientChunk) {
+ // Note: no need to busy wait on the chunk queue, entity load will complete off-main
+ boolean interrupted = false;
+ while (!isCompleted.get()) {
+ interrupted |= Thread.interrupted();
+ LockSupport.park();
+ }
+
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ // now that the entity data is loaded, we can load it into the world
+
+ ret = current.loadInEntityChunk(transientChunk);
+
+ this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId);
+
+ return ret;
+ }
+
+ public PoiChunk getPoiChunkIfLoaded(final int chunkX, final int chunkZ, final boolean checkLoadInCallback) {
+ final NewChunkHolder holder = this.getChunkHolder(chunkX, chunkZ);
+ if (holder != null) {
+ final PoiChunk ret = holder.getPoiChunk();
+ return ret == null || (checkLoadInCallback && !ret.isLoaded()) ? null : ret;
+ }
+ return null;
+ }
+
+ public PoiChunk loadPoiChunk(final int chunkX, final int chunkZ) {
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create poi chunk off-main");
+ PoiChunk ret;
+
+ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ);
+ if (current != null && (ret = current.getPoiChunk()) != null) {
+ ret.load();
+ return ret;
+ }
+
+ final AtomicReference<PoiChunk> completed = new AtomicReference<>();
+ final AtomicBoolean isCompleted = new AtomicBoolean();
+ final Thread waiter = Thread.currentThread();
+ final Long poiLoadId = ChunkTaskScheduler.getNextPoiLoadId();
+ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null;
+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ);
+ try {
+ this.addTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId);
+ final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ);
+ try {
+ current = this.getOrCreateChunkHolder(chunkX, chunkZ);
+ if (null == (ret = current.getPoiChunk())) {
+ loadTask = current.getOrLoadPoiData((final GenericDataLoadTask.TaskResult<PoiChunk, Throwable> result) -> {
+ completed.setPlain(result.left());
+ isCompleted.set(true);
+ LockSupport.unpark(waiter);
+ });
+ final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask();
+
+ if (poiLoad != null) {
+ poiLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
+ }
+ }
+ } finally {
+ this.taskScheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+
+ if (loadTask != null) {
+ loadTask.schedule();
+
+ // Note: no need to busy wait on the chunk queue, poi load will complete off-main
+
+ boolean interrupted = false;
+ while (!isCompleted.get()) {
+ interrupted |= Thread.interrupted();
+ LockSupport.park();
+ }
+
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+
+ ret = completed.getPlain();
+ } // else: became loaded during the scheduling attempt, need to ensure load() is invoked
+
+ ret.load();
+
+ this.removeTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId);
+
+ return ret;
+ }
+
+ void addChangedStatuses(final List<NewChunkHolder> changedFullStatus) {
+ if (changedFullStatus.isEmpty()) {
+ return;
+ }
+ if (!io.papermc.paper.util.TickThread.isTickThread()) {
+ this.taskScheduler.scheduleChunkTask(() -> {
+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate;
+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
+ pendingFullLoadUpdate.add(changedFullStatus.get(i));
+ }
+
+ ChunkHolderManager.this.processPendingFullUpdate();
+ }, PrioritisedExecutor.Priority.HIGHEST);
+ } else {
+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
+ pendingFullLoadUpdate.add(changedFullStatus.get(i));
+ }
+ }
+ }
+
+ private void removeChunkHolder(final NewChunkHolder holder) {
+ holder.markUnloaded();
+ this.autoSaveQueue.remove(holder);
+ ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder);
+ this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ));
+
+ }
+
+ // note: never call while inside the chunk system, this will absolutely break everything
+ public void processUnloads() {
+ io.papermc.paper.util.TickThread.ensureTickThread("Cannot unload chunks off-main");
+
+ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) {
+ throw new IllegalStateException("Cannot unload chunks recursively");
+ }
+ final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift
+ final List<ChunkUnloadQueue.SectionToUnload> unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions();
+ int unloadCountTentative = 0;
+ for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
+ final ChunkUnloadQueue.UnloadSection section
+ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ());
+
+ if (section == null) {
+ // removed concurrently
+ continue;
+ }
+
+ // technically reading the size field is unsafe, and it may be incorrect.
+ // We assume that the error here cumulatively goes away over many ticks. If it did not, then it is possible
+ // for chunks to never unload or not unload fast enough.
+ unloadCountTentative += section.chunks.size();
+ }
+
+ if (unloadCountTentative <= 0) {
+ // no work to do
+ return;
+ }
+
+ // We do need to process updates here so that any addTicket that is synchronised before this call does not go missed.
+ this.processTicketUpdates();
+
+ final int toUnloadCount = Math.max(50, (int)(unloadCountTentative * 0.05));
+ int processedCount = 0;
+
+ for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
+ final List<NewChunkHolder> stage1 = new ArrayList<>();
+ final List<NewChunkHolder.UnloadState> stage2 = new ArrayList<>();
+
+ final int sectionLowerX = sectionRef.sectionX() << sectionShift;
+ final int sectionLowerZ = sectionRef.sectionZ() << sectionShift;
+
+ // stage 1: set up for stage 2 while holding critical locks
+ ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ);
+ try {
+ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ);
+ try {
+ final ChunkUnloadQueue.UnloadSection section
+ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ());
+
+ if (section == null) {
+ // removed concurrently
+ continue;
+ }
+
+ // collect the holders to run stage 1 on
+ final int sectionCount = section.chunks.size();
+
+ if ((sectionCount + processedCount) <= toUnloadCount) {
+ // we can just drain the entire section
+
+ for (final LongIterator iterator = section.chunks.iterator(); iterator.hasNext();) {
+ final NewChunkHolder holder = this.chunkHolders.get(iterator.nextLong());
+ if (holder == null) {
+ throw new IllegalStateException();
+ }
+ stage1.add(holder);
+ }
+
+ // remove section
+ this.unloadQueue.removeSection(sectionRef.sectionX(), sectionRef.sectionZ());
+ } else {
+ // processedCount + len = toUnloadCount
+ // we cannot drain the entire section
+ for (int i = 0, len = toUnloadCount - processedCount; i < len; ++i) {
+ final NewChunkHolder holder = this.chunkHolders.get(section.chunks.removeFirstLong());
+ if (holder == null) {
+ throw new IllegalStateException();
+ }
+ stage1.add(holder);
+ }
+ }
+
+ // run stage 1
+ for (int i = 0, len = stage1.size(); i < len; ++i) {
+ final NewChunkHolder chunkHolder = stage1.get(i);
+ chunkHolder.removeFromUnloadQueue();
+ if (chunkHolder.isSafeToUnload() != null) {
+ LOGGER.error("Chunkholder " + chunkHolder + " is not safe to unload but is inside the unload queue?");
+ continue;
+ }
+ final NewChunkHolder.UnloadState state = chunkHolder.unloadStage1();
+ if (state == null) {
+ // can unload immediately
+ this.removeChunkHolder(chunkHolder);
+ continue;
+ }
+ stage2.add(state);
+ }
+ } finally {
+ this.taskScheduler.schedulingLockArea.unlock(scheduleLock);
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+
+ // stage 2: invoke expensive unload logic, designed to run without locks thanks to stage 1
+ final List<NewChunkHolder> stage3 = new ArrayList<>(stage2.size());
+
+ final Boolean before = this.blockTicketUpdates();
+ try {
+ for (int i = 0, len = stage2.size(); i < len; ++i) {
+ final NewChunkHolder.UnloadState state = stage2.get(i);
+ final NewChunkHolder holder = state.holder();
+
+ holder.unloadStage2(state);
+ stage3.add(holder);
+ }
+ } finally {
+ this.unblockTicketUpdates(before);
+ }
+
+ // stage 3: actually attempt to remove the chunk holders
+ ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ);
+ try {
+ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ);
+ try {
+ for (int i = 0, len = stage3.size(); i < len; ++i) {
+ final NewChunkHolder holder = stage3.get(i);
+
+ if (holder.unloadStage3()) {
+ this.removeChunkHolder(holder);
+ } else {
+ // add cooldown so the next unload check is not immediately next tick
+ this.addTicketAtLevel(UNLOAD_COOLDOWN, CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ), MAX_TICKET_LEVEL, Unit.INSTANCE, false);
+ }
+ }
+ } finally {
+ this.taskScheduler.schedulingLockArea.unlock(scheduleLock);
+ }
+ } finally {
+ this.ticketLockArea.unlock(ticketLock);
+ }
+
+ processedCount += stage1.size();
+
+ if (processedCount >= toUnloadCount) {
+ break;
+ }
+ }
+ }
+
+ public enum TicketOperationType {
+ ADD, REMOVE, ADD_IF_REMOVED, ADD_AND_REMOVE
+ }
+
+ public static record TicketOperation<T, V> (
+ TicketOperationType op, long chunkCoord,
+ TicketType<T> ticketType, int ticketLevel, T identifier,
+ TicketType<V> ticketType2, int ticketLevel2, V identifier2
+ ) {
+
+ private TicketOperation(TicketOperationType op, long chunkCoord,
+ TicketType<T> ticketType, int ticketLevel, T identifier) {
+ this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null);
+ }
+
+ public static <T> TicketOperation<T, T> addOp(final ChunkPos chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> addOp(final int chunkX, final int chunkZ, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> addOp(final long chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> removeOp(final ChunkPos chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> removeOp(final int chunkX, final int chunkZ, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier);
+ }
+
+ public static <T> TicketOperation<T, T> removeOp(final long chunk, final TicketType<T> type, final int ticketLevel, final T identifier) {
+ return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier);
+ }
+
+ public static <T, V> TicketOperation<T, V> addIfRemovedOp(final long chunk,
+ final TicketType<T> addType, final int addLevel, final T addIdentifier,
+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
+ return new TicketOperation<>(
+ TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier,
+ removeType, removeLevel, removeIdentifier
+ );
+ }
+
+ public static <T, V> TicketOperation<T, V> addAndRemove(final long chunk,
+ final TicketType<T> addType, final int addLevel, final T addIdentifier,
+ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) {
+ return new TicketOperation<>(
+ TicketOperationType.ADD_AND_REMOVE, chunk, addType, addLevel, addIdentifier,
+ removeType, removeLevel, removeIdentifier
+ );
+ }
+ }
+
+ private <T, V> boolean processTicketOp(TicketOperation<T, V> operation) {
+ boolean ret = false;
+ switch (operation.op) {
+ case ADD: {
+ ret |= this.addTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier);
+ break;
+ }
+ case REMOVE: {
+ ret |= this.removeTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier);
+ break;
+ }
+ case ADD_IF_REMOVED: {
+ ret |= this.addIfRemovedTicket(
+ operation.chunkCoord,
+ operation.ticketType, operation.ticketLevel, operation.identifier,
+ operation.ticketType2, operation.ticketLevel2, operation.identifier2
+ );
+ break;
+ }
+ case ADD_AND_REMOVE: {
+ ret = true;
+ this.addAndRemoveTickets(
+ operation.chunkCoord,
+ operation.ticketType, operation.ticketLevel, operation.identifier,
+ operation.ticketType2, operation.ticketLevel2, operation.identifier2
+ );
+ break;
+ }
+ }
+
+ return ret;
+ }
+
+ public void performTicketUpdates(final Collection<TicketOperation<?, ?>> operations) {
+ for (final TicketOperation<?, ?> operation : operations) {
+ this.processTicketOp(operation);
+ }
+ }
+
+ private final ThreadLocal<Boolean> BLOCK_TICKET_UPDATES = ThreadLocal.withInitial(() -> {
+ return Boolean.FALSE;
+ });
+
+ public Boolean blockTicketUpdates() {
+ final Boolean ret = BLOCK_TICKET_UPDATES.get();
+ BLOCK_TICKET_UPDATES.set(Boolean.TRUE);
+ return ret;
+ }
+
+ public void unblockTicketUpdates(final Boolean before) {
+ BLOCK_TICKET_UPDATES.set(before);
+ }
+
+ public boolean processTicketUpdates() {
+ return this.processTicketUpdates(true, null);
+ }
+
+ private static final ThreadLocal<List<ChunkProgressionTask>> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>();
+
+ static List<ChunkProgressionTask> getCurrentTicketUpdateScheduling() {
+ return CURRENT_TICKET_UPDATE_SCHEDULING.get();
+ }
+
+ private boolean processTicketUpdates(final boolean processFullUpdates, List<ChunkProgressionTask> scheduledTasks) {
+ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) {
+ throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager");
+ }
+
+ List<NewChunkHolder> changedFullStatus = null;
+
+ final boolean isTickThread = io.papermc.paper.util.TickThread.isTickThread();
+
+ boolean ret = false;
+ final boolean canProcessFullUpdates = processFullUpdates & isTickThread;
+ final boolean canProcessScheduling = scheduledTasks == null;
+
+ if (this.ticketLevelPropagator.hasPendingUpdates()) {
+ if (scheduledTasks == null) {
+ scheduledTasks = new ArrayList<>();
+ }
+ changedFullStatus = new ArrayList<>();
+
+ ret |= this.ticketLevelPropagator.performUpdates(
+ this.ticketLockArea, this.taskScheduler.schedulingLockArea,
+ scheduledTasks, changedFullStatus
+ );
+ }
+
+ if (changedFullStatus != null) {
+ this.addChangedStatuses(changedFullStatus);
+ }
+
+ if (canProcessScheduling && scheduledTasks != null) {
+ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) {
+ scheduledTasks.get(i).schedule();
+ }
+ }
+
+ if (canProcessFullUpdates) {
+ ret |= this.processPendingFullUpdate();
+ }
+
+ return ret;
+ }
+
+ // only call on tick thread
+ private boolean processPendingFullUpdate() {
+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
+
+ boolean ret = false;
+
+ final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
+
+ NewChunkHolder holder;
+ while ((holder = pendingFullLoadUpdate.poll()) != null) {
+ ret |= holder.handleFullStatusChange(changedFullStatus);
+
+ if (!changedFullStatus.isEmpty()) {
+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
+ pendingFullLoadUpdate.add(changedFullStatus.get(i));
+ }
+ changedFullStatus.clear();
+ }
+ }
+
+ return ret;
+ }
+
+ public JsonObject getDebugJson() {
+ final JsonObject ret = new JsonObject();
+
+ ret.add("unload_queue", this.unloadQueue.toDebugJson());
+
+ final JsonArray holders = new JsonArray();
+ ret.add("chunkholders", holders);
+
+ for (final NewChunkHolder holder : this.getChunkHolders()) {
+ holders.add(holder.getDebugJson());
+ }
+
+ final JsonArray allTicketsJson = new JsonArray();
+ ret.add("tickets", allTicketsJson);
+
+ for (final Iterator<ConcurrentLong2ReferenceChainedHashTable.TableEntry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.entryIterator();
+ iterator.hasNext();) {
+ final ConcurrentLong2ReferenceChainedHashTable.TableEntry<SortedArraySet<Ticket<?>>> coordinateTickets = iterator.next();
+ final long coordinate = coordinateTickets.getKey();
+ final SortedArraySet<Ticket<?>> tickets = coordinateTickets.getValue();
+
+ final JsonObject coordinateJson = new JsonObject();
+ allTicketsJson.add(coordinateJson);
+
+ coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate)));
+ coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate)));
+
+ final JsonArray ticketsSerialized = new JsonArray();
+ coordinateJson.add("tickets", ticketsSerialized);
+
+ // note: by using a copy of the backing array, we can avoid explicit exceptions we may trip when iterating
+ // directly over the set using the iterator
+ // however, it also means we need to null-check the values, and there is a possibility that we _miss_ an
+ // entry OR iterate over an entry multiple times
+ for (final Object ticketUncasted : ((ChunkSystemSortedArraySet<Ticket<?>>)tickets).moonrise$copyBackingArray()) {
+ if (ticketUncasted == null) {
+ continue;
+ }
+ final Ticket<?> ticket = (Ticket<?>)ticketUncasted;
+ final JsonObject ticketSerialized = new JsonObject();
+ ticketsSerialized.add(ticketSerialized);
+
+ ticketSerialized.addProperty("type", ticket.getType().toString());
+ ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel()));
+ ticketSerialized.addProperty("identifier", Objects.toString(ticket.key));
+ ticketSerialized.addProperty("remove_tick", Long.valueOf(((ChunkSystemTicket<?>)(Object)ticket).moonrise$getRemoveDelay()));
+ }
+ }
+
+ return ret;
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
new file mode 100644
index 0000000000000000000000000000000000000000..c1c119e2e788d5963de3a74a6b9724c71a168a8a
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
@@ -0,0 +1,1037 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue;
+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.JsonUtil;
+import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
+import ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor.RadiusAwarePrioritisedExecutor;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkFullTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLightTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkUpgradeGenericStatusTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer;
+import ca.spottedleaf.moonrise.patches.chunk_system.status.ChunkSystemChunkStep;
+import ca.spottedleaf.moonrise.patches.chunk_system.util.ParallelSearchRadiusIteration;
+import com.mojang.logging.LogUtils;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import net.minecraft.CrashReport;
+import net.minecraft.CrashReportCategory;
+import net.minecraft.ReportedException;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ChunkLevel;
+import net.minecraft.server.level.ChunkMap;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.server.level.GenerationChunkHolder;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.TicketType;
+import net.minecraft.util.StaticCache2D;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.status.ChunkPyramid;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.chunk.status.ChunkStep;
+import net.minecraft.world.phys.Vec3;
+import org.slf4j.Logger;
+import java.io.File;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+
+public final class ChunkTaskScheduler {
+
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+
+ static int newChunkSystemIOThreads;
+ static int newChunkSystemGenParallelism;
+ static int newChunkSystemGenPopulationParallelism;
+ static int newChunkSystemLoadParallelism;
+
+ private static boolean initialised = false;
+
+ public static void init(io.papermc.paper.configuration.GlobalConfiguration.ChunkSystem chunkSystem) {
+ if (initialised) {
+ return;
+ }
+ initialised = true;
+ MoonriseCommon.init(chunkSystem); // Paper
+ newChunkSystemIOThreads = chunkSystem.ioThreads;
+ if (newChunkSystemIOThreads <= 0) {
+ newChunkSystemIOThreads = 1;
+ } else {
+ newChunkSystemIOThreads = Math.max(1, newChunkSystemIOThreads);
+ }
+
+ String newChunkSystemGenParallelism = chunkSystem.genParallelism;
+ if (newChunkSystemGenParallelism.equalsIgnoreCase("default")) {
+ newChunkSystemGenParallelism = "true";
+ }
+
+ boolean useParallelGen;
+ if (newChunkSystemGenParallelism.equalsIgnoreCase("on") || newChunkSystemGenParallelism.equalsIgnoreCase("enabled")
+ || newChunkSystemGenParallelism.equalsIgnoreCase("true")) {
+ useParallelGen = true;
+ } else if (newChunkSystemGenParallelism.equalsIgnoreCase("off") || newChunkSystemGenParallelism.equalsIgnoreCase("disabled")
+ || newChunkSystemGenParallelism.equalsIgnoreCase("false")) {
+ useParallelGen = false;
+ } else {
+ throw new IllegalStateException("Invalid option for gen-parallelism: must be one of [on, off, enabled, disabled, true, false, default]");
+ }
+
+ ChunkTaskScheduler.newChunkSystemGenParallelism = MoonriseCommon.WORKER_THREADS;
+ ChunkTaskScheduler.newChunkSystemGenPopulationParallelism = useParallelGen ? MoonriseCommon.WORKER_THREADS : 1;
+ ChunkTaskScheduler.newChunkSystemLoadParallelism = MoonriseCommon.WORKER_THREADS;
+
+ RegionFileIOThread.init(newChunkSystemIOThreads);
+
+ LOGGER.info("Chunk system is using " + newChunkSystemIOThreads + " I/O threads, " + MoonriseCommon.WORKER_THREADS + " worker threads, and population gen parallelism of " + ChunkTaskScheduler.newChunkSystemGenPopulationParallelism + " threads");
+ }
+
+ public static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_system:chunk_load", Long::compareTo);
+ private static final AtomicLong CHUNK_LOAD_IDS = new AtomicLong();
+
+ public static Long getNextChunkLoadId() {
+ return Long.valueOf(CHUNK_LOAD_IDS.getAndIncrement());
+ }
+
+ public static final TicketType<Long> NON_FULL_CHUNK_LOAD = TicketType.create("chunk_system:non_full_load", Long::compareTo);
+ private static final AtomicLong NON_FULL_CHUNK_LOAD_IDS = new AtomicLong();
+
+ public static Long getNextNonFullLoadId() {
+ return Long.valueOf(NON_FULL_CHUNK_LOAD_IDS.getAndIncrement());
+ }
+
+ public static final TicketType<Long> ENTITY_LOAD = TicketType.create("chunk_system:entity_load", Long::compareTo);
+ private static final AtomicLong ENTITY_LOAD_IDS = new AtomicLong();
+
+ public static Long getNextEntityLoadId() {
+ return Long.valueOf(ENTITY_LOAD_IDS.getAndIncrement());
+ }
+
+ public static final TicketType<Long> POI_LOAD = TicketType.create("chunk_system:poi_load", Long::compareTo);
+ private static final AtomicLong POI_LOAD_IDS = new AtomicLong();
+
+ public static Long getNextPoiLoadId() {
+ return Long.valueOf(POI_LOAD_IDS.getAndIncrement());
+ }
+
+ public static final TicketType<Long> CHUNK_RELIGHT = TicketType.create("starlight:chunk_relight", Long::compareTo);
+ private static final AtomicLong CHUNK_RELIGHT_IDS = new AtomicLong();
+
+ public static Long getNextChunkRelightId() {
+ return Long.valueOf(CHUNK_RELIGHT_IDS.getAndIncrement());
+ }
+
+
+ public static int getTicketLevel(final ChunkStatus status) {
+ return ChunkLevel.byStatus(status);
+ }
+
+ public final ServerLevel world;
+ public final PrioritisedThreadPool workers;
+ public final RadiusAwarePrioritisedExecutor radiusAwareScheduler;
+ public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor;
+ private final PrioritisedThreadPool.PrioritisedPoolExecutor radiusAwareGenExecutor;
+ public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor;
+
+ private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue();
+
+ public final ChunkHolderManager chunkHolderManager;
+
+ static {
+ ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_STARTS).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setWriteRadius(1);
+ ((ChunkSystemChunkStatus)ChunkStatus.INITIALIZE_LIGHT).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$setWriteRadius(2);
+ ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setWriteRadius(0);
+ ((ChunkSystemChunkStatus)ChunkStatus.FULL).moonrise$setWriteRadius(0);
+
+ ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setEmptyLoadStatus(true);
+ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setEmptyLoadStatus(true);
+ ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setEmptyLoadStatus(true);
+ ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setEmptyLoadStatus(true);
+ ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setEmptyLoadStatus(true);
+ ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setEmptyLoadStatus(true);
+ ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setEmptyLoadStatus(true);
+ ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setEmptyLoadStatus(true);
+
+ /*
+ It's important that the neighbour read radius is taken into account. If _any_ later status is using some chunk as
+ a neighbour, it must be also safe if that neighbour is being generated. i.e for any status later than FEATURES,
+ for a status to be parallel safe it must not read the block data from its neighbours.
+ */
+ final List<ChunkStatus> parallelCapableStatus = Arrays.asList(
+ // No-op executor.
+ ChunkStatus.EMPTY,
+
+ // This is parallel capable, as CB has fixed the concurrency issue with stronghold generations.
+ // Does not touch neighbour chunks.
+ ChunkStatus.STRUCTURE_STARTS,
+
+ // Surprisingly this is parallel capable. It is simply reading the already-created structure starts
+ // into the structure references for the chunk. So while it reads from it neighbours, its neighbours
+ // will not change, even if executed in parallel.
+ ChunkStatus.STRUCTURE_REFERENCES,
+
+ // Safe. Mojang runs it in parallel as well.
+ ChunkStatus.BIOMES,
+
+ // Safe. Mojang runs it in parallel as well.
+ ChunkStatus.NOISE,
+
+ // Parallel safe. Only touches the target chunk. Biome retrieval is now noise based, which is
+ // completely thread-safe.
+ ChunkStatus.SURFACE,
+
+ // No global state is modified in the carvers. It only touches the specified chunk. So it is parallel safe.
+ ChunkStatus.CARVERS,
+
+ // FEATURES is not parallel safe. It writes to neighbours.
+
+ // no-op executor
+ ChunkStatus.INITIALIZE_LIGHT
+
+ // LIGHT is not parallel safe. It also doesn't run on the generation executor, so no point.
+
+ // Only writes to the specified chunk. State is not read by later statuses. Parallel safe.
+ // Note: it may look unsafe because it writes to a worldgenregion, but the region size is always 0 -
+ // see the task margin.
+ // However, if the neighbouring FEATURES chunk is unloaded, but then fails to load in again (for whatever
+ // reason), then it would write to this chunk - and since this status reads blocks from itself, it's not
+ // safe to execute this in parallel.
+ // SPAWN
+
+ // FULL is executed on main.
+ );
+
+ for (final ChunkStatus status : parallelCapableStatus) {
+ ((ChunkSystemChunkStatus)status).moonrise$setParallelCapable(true);
+ }
+ }
+
+ private static final int[] ACCESS_RADIUS_TABLE_LOAD = new int[ChunkStatus.getStatusList().size()];
+ private static final int[] ACCESS_RADIUS_TABLE_GEN = new int[ChunkStatus.getStatusList().size()];
+ private static final int[] ACCESS_RADIUS_TABLE = new int[ChunkStatus.getStatusList().size()];
+ static {
+ Arrays.fill(ACCESS_RADIUS_TABLE_LOAD, -1);
+ Arrays.fill(ACCESS_RADIUS_TABLE_GEN, -1);
+ Arrays.fill(ACCESS_RADIUS_TABLE, -1);
+ }
+
+ private static int getAccessRadius0(final ChunkStatus toStatus, final ChunkPyramid pyramid) {
+ if (toStatus == ChunkStatus.EMPTY) {
+ return 0;
+ }
+
+ final ChunkStep chunkStep = pyramid.getStepTo(toStatus);
+
+ final int radius = chunkStep.getAccumulatedRadiusOf(ChunkStatus.EMPTY);
+ int maxRange = radius;
+
+ for (int dist = 0; dist <= radius; ++dist) {
+ final ChunkStatus requiredNeighbourStatus = ((ChunkSystemChunkStep)(Object)chunkStep).moonrise$getRequiredStatusAtRadius(dist);
+ final int rad = ACCESS_RADIUS_TABLE[requiredNeighbourStatus.getIndex()];
+ if (rad == -1) {
+ throw new IllegalStateException();
+ }
+
+ maxRange = Math.max(maxRange, dist + rad);
+ }
+
+ return maxRange;
+ }
+
+ private static final int MAX_ACCESS_RADIUS;
+
+ static {
+ final List<ChunkStatus> statuses = ChunkStatus.getStatusList();
+ for (int i = 0, len = statuses.size(); i < len; ++i) {
+ final ChunkStatus status = statuses.get(i);
+ ACCESS_RADIUS_TABLE_LOAD[i] = getAccessRadius0(status, ChunkPyramid.LOADING_PYRAMID);
+ ACCESS_RADIUS_TABLE_GEN[i] = getAccessRadius0(status, ChunkPyramid.GENERATION_PYRAMID);
+ ACCESS_RADIUS_TABLE[i] = Math.max(
+ ACCESS_RADIUS_TABLE_LOAD[i],
+ ACCESS_RADIUS_TABLE_GEN[i]
+ );
+ }
+ MAX_ACCESS_RADIUS = ACCESS_RADIUS_TABLE[ACCESS_RADIUS_TABLE.length - 1];
+ }
+
+ public static int getMaxAccessRadius() {
+ return MAX_ACCESS_RADIUS;
+ }
+
+ public static int getAccessRadius(final ChunkStatus genStatus) {
+ return ACCESS_RADIUS_TABLE[genStatus.getIndex()];
+ }
+
+ public static int getAccessRadius(final FullChunkStatus status) {
+ return (status.ordinal() - 1) + getAccessRadius(ChunkStatus.FULL);
+ }
+
+
+ public final ReentrantAreaLock schedulingLockArea;
+ private final int lockShift;
+
+ public final int getChunkSystemLockShift() {
+ return this.lockShift;
+ }
+
+ public ChunkTaskScheduler(final ServerLevel world, final PrioritisedThreadPool workers) {
+ this.world = world;
+ this.workers = workers;
+ // must be >= region shift (in paper, doesn't exist) and must be >= ticket propagator section shift
+ // it must be >= region shift since the regioniser assumes ticket updates do not occur in parallel for the region sections
+ // it must be >= ticket propagator section shift so that the ticket propagator can assume that owning a position implies owning
+ // the entire section
+ // we just take the max, as we want the smallest shift that satisfies these properties
+ this.lockShift = Math.max(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift(), ThreadedTicketLevelPropagator.SECTION_SHIFT);
+ this.schedulingLockArea = new ReentrantAreaLock(this.getChunkSystemLockShift());
+
+ final String worldName = WorldUtil.getWorldName(world);
+ this.parallelGenExecutor = workers.createExecutor("Chunk parallel generation executor for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenParallelism));
+ this.radiusAwareGenExecutor = workers.createExecutor("Chunk radius aware generator for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenPopulationParallelism));
+ this.loadExecutor = workers.createExecutor("Chunk load executor for world '" + worldName + "'", 1, newChunkSystemLoadParallelism);
+ this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, Math.max(2, 1 + newChunkSystemGenPopulationParallelism));
+ this.chunkHolderManager = new ChunkHolderManager(world, this);
+ }
+
+ private final AtomicBoolean failedChunkSystem = new AtomicBoolean();
+
+ public static Object stringIfNull(final Object obj) {
+ return obj == null ? "null" : obj;
+ }
+
+ public void unrecoverableChunkSystemFailure(final int chunkX, final int chunkZ, final Map<String, Object> objectsOfInterest, final Throwable thr) {
+ final NewChunkHolder holder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ);
+ LOGGER.error("Chunk system error at chunk (" + chunkX + "," + chunkZ + "), holder: " + holder + ", exception:", new Throwable(thr));
+
+ if (this.failedChunkSystem.getAndSet(true)) {
+ return;
+ }
+
+ final ReportedException reportedException = thr instanceof ReportedException ? (ReportedException)thr : new ReportedException(new CrashReport("Chunk system error", thr));
+
+ CrashReportCategory crashReportCategory = reportedException.getReport().addCategory("Chunk system details");
+ crashReportCategory.setDetail("Chunk coordinate", new ChunkPos(chunkX, chunkZ).toString());
+ crashReportCategory.setDetail("ChunkHolder", Objects.toString(holder));
+ crashReportCategory.setDetail("unrecoverableChunkSystemFailure caller thread", Thread.currentThread().getName());
+
+ crashReportCategory = reportedException.getReport().addCategory("Chunk System Objects of Interest");
+ for (final Map.Entry<String, Object> entry : objectsOfInterest.entrySet()) {
+ if (entry.getValue() instanceof Throwable thrObject) {
+ crashReportCategory.setDetailError(Objects.toString(entry.getKey()), thrObject);
+ } else {
+ crashReportCategory.setDetail(Objects.toString(entry.getKey()), Objects.toString(entry.getValue()));
+ }
+ }
+
+ final Runnable crash = () -> {
+ throw new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException);
+ };
+
+ // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions
+ this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING);
+ // so, make the main thread pick it up
+ ((ChunkSystemMinecraftServer)this.world.getServer()).moonrise$setChunkSystemCrash(new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException));
+ }
+
+ public boolean executeMainThreadTask() {
+ io.papermc.paper.util.TickThread.ensureTickThread("Cannot execute main thread task off-main");
+ return this.mainThreadExecutor.executeTask();
+ }
+
+ public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
+ this.chunkHolderManager.raisePriority(x, z, priority);
+ }
+
+ public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
+ this.chunkHolderManager.setPriority(x, z, priority);
+ }
+
+ public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
+ this.chunkHolderManager.lowerPriority(x, z, priority);
+ }
+
+ public void scheduleTickingState(final int chunkX, final int chunkZ, final FullChunkStatus toStatus,
+ final boolean addTicket, final PrioritisedExecutor.Priority priority,
+ final Consumer<LevelChunk> onComplete) {
+ if (!io.papermc.paper.util.TickThread.isTickThread()) {
+ this.scheduleChunkTask(chunkX, chunkZ, () -> {
+ ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ }, priority);
+ return;
+ }
+ final int accessRadius = getAccessRadius(toStatus);
+ if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) {
+ throw new IllegalStateException("Cannot schedule chunk load during ticket level update");
+ }
+ if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) {
+ throw new IllegalStateException("Cannot schedule chunk loading recursively");
+ }
+
+ if (toStatus == FullChunkStatus.INACCESSIBLE) {
+ throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
+ }
+
+ final int minLevel = 33 - (toStatus.ordinal() - 1);
+ final Long chunkReference = addTicket ? getNextChunkLoadId() : null;
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ if (addTicket) {
+ this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
+ this.chunkHolderManager.processTicketUpdates();
+ }
+
+ final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> {
+ try {
+ if (onComplete != null) {
+ onComplete.accept(chunk);
+ }
+ } finally {
+ if (addTicket) {
+ ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
+ }
+ }
+ };
+
+ final boolean scheduled;
+ final LevelChunk chunk;
+ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius);
+ try {
+ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius);
+ try {
+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey);
+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) {
+ scheduled = false;
+ chunk = null;
+ } else {
+ final FullChunkStatus currStatus = chunkHolder.getChunkStatus();
+ if (currStatus.isOrAfter(toStatus)) {
+ scheduled = false;
+ chunk = (LevelChunk)chunkHolder.getCurrentChunk();
+ } else {
+ scheduled = true;
+ chunk = null;
+
+ final int radius = toStatus.ordinal() - 1; // 0 -> BORDER, 1 -> TICKING, 2 -> ENTITY_TICKING
+ for (int dz = -radius; dz <= radius; ++dz) {
+ for (int dx = -radius; dx <= radius; ++dx) {
+ final NewChunkHolder neighbour =
+ (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ);
+ if (neighbour != null) {
+ neighbour.raisePriority(priority);
+ }
+ }
+ }
+
+ // ticket level should schedule for us
+ chunkHolder.addFullStatusConsumer(toStatus, loadCallback);
+ }
+ }
+ } finally {
+ this.schedulingLockArea.unlock(schedulingLock);
+ }
+ } finally {
+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock);
+ }
+
+ if (!scheduled) {
+ // couldn't schedule
+ try {
+ loadCallback.accept(chunk);
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to process chunk full status callback", thr);
+ }
+ }
+ }
+
+ public void scheduleChunkLoad(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) {
+ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ return;
+ }
+ this.scheduleChunkLoad(chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
+ if (chunk == null) {
+ if (onComplete != null) {
+ onComplete.accept(null);
+ }
+ } else {
+ if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
+ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ } else {
+ if (onComplete != null) {
+ onComplete.accept(null);
+ }
+ }
+ }
+ });
+ }
+
+ // only appropriate to use with syncLoadNonFull
+ public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+ final PrioritisedExecutor.Priority priority) {
+ final int accessRadius = getAccessRadius(toStatus);
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus);
+ final List<ChunkProgressionTask> tasks = new ArrayList<>();
+ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention
+ try {
+ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention
+ try {
+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey);
+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) {
+ return false;
+ } else {
+ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus();
+ if (genStatus != null && genStatus.isOrAfter(toStatus)) {
+ return true;
+ } else {
+ chunkHolder.raisePriority(priority);
+
+ if (!chunkHolder.upgradeGenTarget(toStatus)) {
+ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks);
+ }
+ }
+ }
+ } finally {
+ this.schedulingLockArea.unlock(schedulingLock);
+ }
+ } finally {
+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock);
+ }
+
+ for (int i = 0, len = tasks.size(); i < len; ++i) {
+ tasks.get(i).schedule();
+ }
+
+ return true;
+ }
+
+ // Note: on Moonrise the non-full sync load requires blocking on managedBlock, but this is fine since there is only
+ // one main thread. On Folia, it is required that the non-full load can occur completely asynchronously to avoid deadlock
+ // between regions
+ public ChunkAccess syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status) {
+ if (status == null || status.isOrAfter(ChunkStatus.FULL)) {
+ throw new IllegalArgumentException("Status: " + status);
+ }
+ ChunkAccess loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status);
+ if (loaded != null) {
+ return loaded;
+ }
+
+ final Long ticketId = getNextNonFullLoadId();
+ final int ticketLevel = getTicketLevel(status);
+ this.chunkHolderManager.addTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId);
+ this.chunkHolderManager.processTicketUpdates();
+
+ this.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, PrioritisedExecutor.Priority.BLOCKING);
+
+ // we could do a simple spinwait here, since we do not need to process tasks while performing this load
+ // but we process tasks only because it's a better use of the time spent
+ this.world.getChunkSource().mainThreadProcessor.managedBlock(() -> {
+ return ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status) != null;
+ });
+
+ loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status);
+
+ this.chunkHolderManager.removeTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId);
+
+ if (loaded == null) {
+ throw new IllegalStateException("Expected chunk to be loaded for status " + status);
+ }
+
+ return loaded;
+ }
+
+ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket,
+ final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
+ if (!io.papermc.paper.util.TickThread.isTickThread()) {
+ this.scheduleChunkTask(chunkX, chunkZ, () -> {
+ ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ }, priority);
+ return;
+ }
+ final int accessRadius = getAccessRadius(toStatus);
+ if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) {
+ throw new IllegalStateException("Cannot schedule chunk load during ticket level update");
+ }
+ if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) {
+ throw new IllegalStateException("Cannot schedule chunk loading recursively");
+ }
+
+ if (toStatus == ChunkStatus.FULL) {
+ this.scheduleTickingState(chunkX, chunkZ, FullChunkStatus.FULL, addTicket, priority, (Consumer)onComplete);
+ return;
+ }
+
+ final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus);
+ final Long chunkReference = addTicket ? getNextChunkLoadId() : null;
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ if (addTicket) {
+ this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
+ this.chunkHolderManager.processTicketUpdates();
+ }
+
+ final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
+ try {
+ if (onComplete != null) {
+ onComplete.accept(chunk);
+ }
+ } finally {
+ if (addTicket) {
+ ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference);
+ }
+ }
+ };
+
+ final List<ChunkProgressionTask> tasks = new ArrayList<>();
+
+ final boolean scheduled;
+ final ChunkAccess chunk;
+ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius);
+ try {
+ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius);
+ try {
+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey);
+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) {
+ scheduled = false;
+ chunk = null;
+ } else {
+ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus();
+ if (genStatus != null && genStatus.isOrAfter(toStatus)) {
+ scheduled = false;
+ chunk = chunkHolder.getCurrentChunk();
+ } else {
+ scheduled = true;
+ chunk = null;
+ chunkHolder.raisePriority(priority);
+
+ if (!chunkHolder.upgradeGenTarget(toStatus)) {
+ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks);
+ }
+ chunkHolder.addStatusConsumer(toStatus, loadCallback);
+ }
+ }
+ } finally {
+ this.schedulingLockArea.unlock(schedulingLock);
+ }
+ } finally {
+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock);
+ }
+
+ for (int i = 0, len = tasks.size(); i < len; ++i) {
+ tasks.get(i).schedule();
+ }
+
+ if (!scheduled) {
+ // couldn't schedule
+ try {
+ loadCallback.accept(chunk);
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to process chunk status callback", thr);
+ }
+ }
+ }
+
+ private ChunkProgressionTask createTask(final int chunkX, final int chunkZ, final ChunkAccess chunk,
+ final NewChunkHolder chunkHolder, final StaticCache2D<GenerationChunkHolder> neighbours,
+ final ChunkStatus toStatus, final PrioritisedExecutor.Priority initialPriority) {
+ if (toStatus == ChunkStatus.EMPTY) {
+ return new ChunkLoadTask(this, this.world, chunkX, chunkZ, chunkHolder, initialPriority);
+ }
+ if (toStatus == ChunkStatus.LIGHT) {
+ return new ChunkLightTask(this, this.world, chunkX, chunkZ, chunk, initialPriority);
+ }
+ if (toStatus == ChunkStatus.FULL) {
+ return new ChunkFullTask(this, this.world, chunkX, chunkZ, chunkHolder, chunk, initialPriority);
+ }
+
+ return new ChunkUpgradeGenericStatusTask(this, this.world, chunkX, chunkZ, chunk, neighbours, toStatus, initialPriority);
+ }
+
+ ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, final NewChunkHolder chunkHolder,
+ final List<ChunkProgressionTask> allTasks) {
+ return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL));
+ }
+
+ // rets new task scheduled for the _specified_ chunk
+ // note: this must hold the scheduling lock
+ // minPriority is only used to pass the priority through to neighbours, as priority calculation has not yet been done
+ // schedule will ignore the generation target, so it should be checked by the caller to ensure the target is not regressed!
+ private ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus,
+ final NewChunkHolder chunkHolder, final List<ChunkProgressionTask> allTasks,
+ final PrioritisedExecutor.Priority minPriority) {
+ if (!this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, getAccessRadius(targetStatus))) {
+ throw new IllegalStateException("Not holding scheduling lock");
+ }
+
+ if (chunkHolder.hasGenerationTask()) {
+ chunkHolder.upgradeGenTarget(targetStatus);
+ return null;
+ }
+
+ final PrioritisedExecutor.Priority requestedPriority = PrioritisedExecutor.Priority.max(
+ minPriority, chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
+ );
+ final ChunkStatus currentGenStatus = chunkHolder.getCurrentGenStatus();
+ final ChunkAccess chunk = chunkHolder.getCurrentChunk();
+
+ if (currentGenStatus == null) {
+ // not yet loaded
+ final ChunkProgressionTask task = this.createTask(
+ chunkX, chunkZ, chunk, chunkHolder, null, ChunkStatus.EMPTY, requestedPriority
+ );
+
+ allTasks.add(task);
+
+ final List<NewChunkHolder> chunkHolderNeighbours = new ArrayList<>(1);
+ chunkHolderNeighbours.add(chunkHolder);
+
+ chunkHolder.setGenerationTarget(targetStatus);
+ chunkHolder.setGenerationTask(task, ChunkStatus.EMPTY, chunkHolderNeighbours);
+
+ return task;
+ }
+
+ if (currentGenStatus.isOrAfter(targetStatus)) {
+ // nothing to do
+ return null;
+ }
+
+ // we know for sure now that we want to schedule _something_, so set the target
+ chunkHolder.setGenerationTarget(targetStatus);
+
+ final ChunkStatus chunkRealStatus = chunk.getPersistedStatus();
+ final ChunkStatus toStatus = ((ChunkSystemChunkStatus)currentGenStatus).moonrise$getNextStatus();
+ final ChunkPyramid chunkPyramid = chunkRealStatus.isOrAfter(toStatus) ? ChunkPyramid.LOADING_PYRAMID : ChunkPyramid.GENERATION_PYRAMID;
+ final ChunkStep chunkStep = chunkPyramid.getStepTo(toStatus);
+
+ final int neighbourReadRadius = Math.max(
+ 0,
+ chunkPyramid.getStepTo(toStatus).getAccumulatedRadiusOf(ChunkStatus.EMPTY)
+ );
+
+ boolean unGeneratedNeighbours = false;
+
+ if (neighbourReadRadius > 0) {
+ final ChunkMap chunkMap = this.world.getChunkSource().chunkMap;
+ for (final long pos : ParallelSearchRadiusIteration.getSearchIteration(neighbourReadRadius)) {
+ final int x = CoordinateUtils.getChunkX(pos);
+ final int z = CoordinateUtils.getChunkZ(pos);
+ final int radius = Math.max(Math.abs(x), Math.abs(z));
+ final ChunkStatus requiredNeighbourStatus = ((ChunkSystemChunkStep)(Object)chunkStep).moonrise$getRequiredStatusAtRadius(radius);
+
+ unGeneratedNeighbours |= this.checkNeighbour(
+ chunkX + x, chunkZ + z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority
+ );
+ }
+ }
+
+ if (unGeneratedNeighbours) {
+ // can't schedule, but neighbour completion will schedule for us when they're ALL done
+
+ // propagate our priority to neighbours
+ chunkHolder.recalculateNeighbourPriorities();
+ return null;
+ }
+
+ // need to gather neighbours
+
+ final List<NewChunkHolder> chunkHolderNeighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1));
+ final StaticCache2D<GenerationChunkHolder> neighbours = StaticCache2D
+ .create(chunkX, chunkZ, neighbourReadRadius, (final int nx, final int nz) -> {
+ final NewChunkHolder holder = nx == chunkX && nz == chunkZ ? chunkHolder : this.chunkHolderManager.getChunkHolder(nx, nz);
+ chunkHolderNeighbours.add(holder);
+
+ return holder.vanillaChunkHolder;
+ });
+
+ final ChunkProgressionTask task = this.createTask(
+ chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus,
+ chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
+ );
+ allTasks.add(task);
+
+ chunkHolder.setGenerationTask(task, toStatus, chunkHolderNeighbours);
+
+ return task;
+ }
+
+ // rets true if the neighbour is not at the required status, false otherwise
+ private boolean checkNeighbour(final int chunkX, final int chunkZ, final ChunkStatus requiredStatus, final NewChunkHolder center,
+ final List<ChunkProgressionTask> tasks, final PrioritisedExecutor.Priority minPriority) {
+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ);
+
+ if (chunkHolder == null) {
+ throw new IllegalStateException("Missing chunkholder when required");
+ }
+
+ final ChunkStatus holderStatus = chunkHolder.getCurrentGenStatus();
+ if (holderStatus != null && holderStatus.isOrAfter(requiredStatus)) {
+ return false;
+ }
+
+ if (chunkHolder.hasFailedGeneration()) {
+ return true;
+ }
+
+ center.addGenerationBlockingNeighbour(chunkHolder);
+ chunkHolder.addWaitingNeighbour(center, requiredStatus);
+
+ if (chunkHolder.upgradeGenTarget(requiredStatus)) {
+ return true;
+ }
+
+ // not at status required, so we need to schedule its generation
+ this.schedule(
+ chunkX, chunkZ, requiredStatus, chunkHolder, tasks, minPriority
+ );
+
+ return true;
+ }
+
+ /**
+ * @deprecated Chunk tasks must be tied to coordinates in the future
+ */
+ @Deprecated
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) {
+ return this.scheduleChunkTask(run, PrioritisedExecutor.Priority.NORMAL);
+ }
+
+ /**
+ * @deprecated Chunk tasks must be tied to coordinates in the future
+ */
+ @Deprecated
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ return this.mainThreadExecutor.queueRunnable(run, priority);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
+ return this.createChunkTask(chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run,
+ final PrioritisedExecutor.Priority priority) {
+ return this.mainThreadExecutor.createTask(run, priority);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
+ return this.mainThreadExecutor.queueRunnable(run);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run,
+ final PrioritisedExecutor.Priority priority) {
+ return this.mainThreadExecutor.queueRunnable(run, priority);
+ }
+
+ public boolean halt(final boolean sync, final long maxWaitNS) {
+ this.radiusAwareGenExecutor.halt();
+ this.parallelGenExecutor.halt();
+ this.loadExecutor.halt();
+ final long time = System.nanoTime();
+ if (sync) {
+ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) {
+ if (
+ !this.radiusAwareGenExecutor.isActive() &&
+ !this.parallelGenExecutor.isActive() &&
+ !this.loadExecutor.isActive()
+ ) {
+ return true;
+ }
+ if ((System.nanoTime() - time) >= maxWaitNS) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public static final ArrayDeque<ChunkInfo> WAITING_CHUNKS = new ArrayDeque<>(); // stack
+
+ public static final class ChunkInfo {
+
+ public final int chunkX;
+ public final int chunkZ;
+ public final ServerLevel world;
+
+ public ChunkInfo(final int chunkX, final int chunkZ, final ServerLevel world) {
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.world = world;
+ }
+
+ public JsonObject toJson() {
+ final JsonObject ret = new JsonObject();
+
+ ret.addProperty("chunk-x", Integer.valueOf(this.chunkX));
+ ret.addProperty("chunk-z", Integer.valueOf(this.chunkZ));
+ ret.addProperty("world-name", WorldUtil.getWorldName(this.world));
+
+ return ret;
+ }
+
+ @Override
+ public String toString() {
+ return "[( " + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "']";
+ }
+ }
+
+ public static void pushChunkWait(final ServerLevel world, final int chunkX, final int chunkZ) {
+ synchronized (WAITING_CHUNKS) {
+ WAITING_CHUNKS.push(new ChunkInfo(chunkX, chunkZ, world));
+ }
+ }
+
+ public static void popChunkWait() {
+ synchronized (WAITING_CHUNKS) {
+ WAITING_CHUNKS.pop();
+ }
+ }
+
+ public static ChunkInfo[] getChunkInfos() {
+ synchronized (WAITING_CHUNKS) {
+ return WAITING_CHUNKS.toArray(new ChunkInfo[0]);
+ }
+ }
+
+ private static JsonObject debugPlayer(final ServerPlayer player) {
+ final Level world = player.level();
+
+ final JsonObject ret = new JsonObject();
+
+ ret.addProperty("name", player.getScoreboardName());
+ ret.addProperty("uuid", player.getUUID().toString());
+ ret.addProperty("real", ((ChunkSystemServerPlayer)player).moonrise$isRealPlayer());
+
+ ret.addProperty("world-name", WorldUtil.getWorldName(world));
+
+ final Vec3 pos = player.position();
+
+ ret.addProperty("x", pos.x);
+ ret.addProperty("y", pos.y);
+ ret.addProperty("z", pos.z);
+
+ final Entity.RemovalReason removalReason = player.getRemovalReason();
+
+ ret.addProperty("removal-reason", removalReason == null ? "null" : removalReason.name());
+
+ ret.add("view-distances", ((ChunkSystemServerPlayer)player).moonrise$getViewDistanceHolder().toJson());
+
+ return ret;
+ }
+
+ public JsonObject getDebugJson() {
+ final JsonObject ret = new JsonObject();
+
+ ret.addProperty("lock_shift", Integer.valueOf(this.getChunkSystemLockShift()));
+ ret.addProperty("ticket_shift", Integer.valueOf(ThreadedTicketLevelPropagator.SECTION_SHIFT));
+ ret.addProperty("region_shift", Integer.valueOf(((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift()));
+
+ ret.addProperty("name", WorldUtil.getWorldName(this.world));
+ ret.addProperty("view-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPIViewDistance());
+ ret.addProperty("tick-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPITickDistance());
+ ret.addProperty("send-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPISendViewDistance());
+
+ final JsonArray players = new JsonArray();
+ ret.add("players", players);
+
+ for (final ServerPlayer player : this.world.players()) {
+ players.add(debugPlayer(player));
+ }
+
+ ret.add("chunk-holder-manager", this.chunkHolderManager.getDebugJson());
+
+ return ret;
+ }
+
+ public static JsonObject debugAllWorlds(final MinecraftServer server) {
+ final JsonObject ret = new JsonObject();
+
+ ret.addProperty("data-version", 2);
+
+ final JsonArray allPlayers = new JsonArray();
+ ret.add("all-players", allPlayers);
+
+ for (final ServerPlayer player : server.getPlayerList().getPlayers()) {
+ allPlayers.add(debugPlayer(player));
+ }
+
+ final JsonArray chunkWaitInfos = new JsonArray();
+ ret.add("chunk-wait-infos", chunkWaitInfos);
+
+ for (final ChunkTaskScheduler.ChunkInfo info : getChunkInfos()) {
+ chunkWaitInfos.add(info.toJson());
+ }
+
+ final JsonArray worlds = new JsonArray();
+ ret.add("worlds", worlds);
+
+ for (final ServerLevel world : server.getAllLevels()) {
+ worlds.add(((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().getDebugJson());
+ }
+
+ return ret;
+ }
+
+ public static File getChunkDebugFile() {
+ return new File(
+ new File(new File("."), "debug"),
+ "chunks-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt"
+ );
+ }
+
+ public static void dumpAllChunkLoadInfo(final MinecraftServer server, final boolean writeDebugInfo) {
+ final ChunkInfo[] chunkInfos = getChunkInfos();
+ if (chunkInfos.length > 0) {
+ LOGGER.error("Chunk wait task info below: ");
+ for (final ChunkInfo chunkInfo : chunkInfos) {
+ final NewChunkHolder holder = ((ChunkSystemServerLevel)chunkInfo.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkInfo.chunkX, chunkInfo.chunkZ);
+ LOGGER.error("Chunk wait: " + chunkInfo);
+ LOGGER.error("Chunk holder: " + holder);
+ }
+
+ if (writeDebugInfo) {
+ final File file = getChunkDebugFile();
+ LOGGER.error("Writing chunk information dump to " + file);
+ try {
+ JsonUtil.writeJson(ChunkTaskScheduler.debugAllWorlds(server), file);
+ LOGGER.error("Successfully written chunk information!");
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to dump chunk information to file " + file.toString(), thr);
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..d5fc5756ea960096ff23376a6b7ac68a2a462d22
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
@@ -0,0 +1,2034 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
+import ca.spottedleaf.concurrentutil.completable.Completable;
+import ca.spottedleaf.concurrentutil.executor.Cancellable;
+import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem;
+import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemFeatures;
+import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData;
+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.GenericDataLoadTask;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.ChunkLevel;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.ImposterProtoChunk;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.chunk.storage.ChunkSerializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+public final class NewChunkHolder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(NewChunkHolder.class);
+
+ public final ServerLevel world;
+ public final int chunkX;
+ public final int chunkZ;
+
+ public final ChunkTaskScheduler scheduler;
+
+ // load/unload state
+
+ // chunk data state
+
+ private ChunkEntitySlices entityChunk;
+ // entity chunk that is loaded, but not yet deserialized
+ private CompoundTag pendingEntityChunk;
+
+ ChunkEntitySlices loadInEntityChunk(final boolean transientChunk) {
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot sync load entity data off-main");
+ final CompoundTag entityChunk;
+ final ChunkEntitySlices ret;
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ if (this.entityChunk != null && (transientChunk || !this.entityChunk.isTransient())) {
+ return this.entityChunk;
+ }
+ final CompoundTag pendingEntityChunk = this.pendingEntityChunk;
+ if (!transientChunk && pendingEntityChunk == null) {
+ throw new IllegalStateException("Must load entity data from disk before loading in the entity chunk!");
+ }
+
+ if (this.entityChunk == null) {
+ ret = this.entityChunk = new ChunkEntitySlices(
+ this.world, this.chunkX, this.chunkZ, this.getChunkStatus(),
+ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
+ );
+
+ ret.setTransient(transientChunk);
+
+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionLoad(this.chunkX, this.chunkZ, ret);
+ } else {
+ // transientChunk = false here
+ ret = this.entityChunk;
+ this.entityChunk.setTransient(false);
+ }
+
+ if (!transientChunk) {
+ this.pendingEntityChunk = null;
+ entityChunk = pendingEntityChunk == EMPTY_ENTITY_CHUNK ? null : pendingEntityChunk;
+ } else {
+ entityChunk = null;
+ }
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+
+ if (!transientChunk) {
+ if (entityChunk != null) {
+ final List<Entity> entities = ChunkEntitySlices.readEntities(this.world, entityChunk);
+
+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().addEntityChunkEntities(entities, new ChunkPos(this.chunkX, this.chunkZ));
+ }
+ }
+
+ return ret;
+ }
+
+ // needed to distinguish whether the entity chunk has been read from disk but is empty or whether it has _not_
+ // been read from disk
+ private static final CompoundTag EMPTY_ENTITY_CHUNK = new CompoundTag();
+
+ private ChunkLoadTask.EntityDataLoadTask entityDataLoadTask;
+ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0,
+ // then the task is rescheduled
+ private List<GenericDataLoadTaskCallback> entityDataLoadTaskWaiters;
+
+ public ChunkLoadTask.EntityDataLoadTask getEntityDataLoadTask() {
+ return this.entityDataLoadTask;
+ }
+
+ // must hold schedule lock for the two below functions
+
+ // returns only if the data has been loaded from disk, DOES NOT relate to whether it has been deserialized
+ // or added into the world (or even into entityChunk)
+ public boolean isEntityChunkNBTLoaded() {
+ return (this.entityChunk != null && !this.entityChunk.isTransient()) || this.pendingEntityChunk != null;
+ }
+
+ private void completeEntityLoad(final GenericDataLoadTask.TaskResult<CompoundTag, Throwable> result) {
+ final List<GenericDataLoadTaskCallback> completeWaiters;
+ ChunkLoadTask.EntityDataLoadTask entityDataLoadTask = null;
+ boolean scheduleEntityTask = false;
+ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ final List<GenericDataLoadTaskCallback> waiters = this.entityDataLoadTaskWaiters;
+ this.entityDataLoadTask = null;
+ if (result != null) {
+ this.entityDataLoadTaskWaiters = null;
+ this.pendingEntityChunk = result.left() == null ? EMPTY_ENTITY_CHUNK : result.left();
+ if (result.right() != null) {
+ LOGGER.error("Unhandled entity data load exception, data data will be lost: ", result.right());
+ }
+
+ for (final GenericDataLoadTaskCallback callback : waiters) {
+ callback.markCompleted();
+ }
+
+ completeWaiters = waiters;
+ } else {
+ // cancelled
+ completeWaiters = null;
+
+ // need to re-schedule?
+ if (waiters.isEmpty()) {
+ this.entityDataLoadTaskWaiters = null;
+ // no tasks to schedule _for_
+ } else {
+ entityDataLoadTask = this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask(
+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
+ );
+ entityDataLoadTask.addCallback(this::completeEntityLoad);
+ // need one schedule() per waiter
+ for (final GenericDataLoadTaskCallback callback : waiters) {
+ scheduleEntityTask |= entityDataLoadTask.schedule(true);
+ }
+ }
+ }
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+
+ if (scheduleEntityTask) {
+ entityDataLoadTask.scheduleNow();
+ }
+
+ // avoid holding the scheduling lock while completing
+ if (completeWaiters != null) {
+ for (final GenericDataLoadTaskCallback callback : completeWaiters) {
+ callback.acceptCompleted(result);
+ }
+ }
+
+ schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ this.checkUnload();
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ }
+
+ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held
+ // however, when the consumer is invoked, it will hold the schedule lock
+ public GenericDataLoadTaskCallback getOrLoadEntityData(final Consumer<GenericDataLoadTask.TaskResult<CompoundTag, Throwable>> consumer) {
+ if (this.isEntityChunkNBTLoaded()) {
+ throw new IllegalStateException("Cannot load entity data, it is already loaded");
+ }
+ // why not just acquire the lock? because the caller NEEDS to call isEntityChunkNBTLoaded before this!
+ if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) {
+ throw new IllegalStateException("Must hold scheduling lock");
+ }
+
+ final GenericDataLoadTaskCallback ret = new EntityDataLoadTaskCallback((Consumer)consumer, this);
+
+ if (this.entityDataLoadTask == null) {
+ this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask(
+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
+ );
+ this.entityDataLoadTask.addCallback(this::completeEntityLoad);
+ this.entityDataLoadTaskWaiters = new ArrayList<>();
+ }
+ this.entityDataLoadTaskWaiters.add(ret);
+ if (this.entityDataLoadTask.schedule(true)) {
+ ret.schedule = this.entityDataLoadTask;
+ }
+ this.checkUnload();
+
+ return ret;
+ }
+
+ private static final class EntityDataLoadTaskCallback extends GenericDataLoadTaskCallback {
+
+ public EntityDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer, final NewChunkHolder chunkHolder) {
+ super(consumer, chunkHolder);
+ }
+
+ @Override
+ void internalCancel() {
+ this.chunkHolder.entityDataLoadTaskWaiters.remove(this);
+ this.chunkHolder.entityDataLoadTask.cancel();
+ }
+ }
+
+ private PoiChunk poiChunk;
+
+ private ChunkLoadTask.PoiDataLoadTask poiDataLoadTask;
+ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0,
+ // then the task is rescheduled
+ private List<GenericDataLoadTaskCallback> poiDataLoadTaskWaiters;
+
+ public ChunkLoadTask.PoiDataLoadTask getPoiDataLoadTask() {
+ return this.poiDataLoadTask;
+ }
+
+ // must hold schedule lock for the two below functions
+
+ public boolean isPoiChunkLoaded() {
+ return this.poiChunk != null;
+ }
+
+ private void completePoiLoad(final GenericDataLoadTask.TaskResult<PoiChunk, Throwable> result) {
+ final List<GenericDataLoadTaskCallback> completeWaiters;
+ ChunkLoadTask.PoiDataLoadTask poiDataLoadTask = null;
+ boolean schedulePoiTask = false;
+ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ final List<GenericDataLoadTaskCallback> waiters = this.poiDataLoadTaskWaiters;
+ this.poiDataLoadTask = null;
+ if (result != null) {
+ this.poiDataLoadTaskWaiters = null;
+ this.poiChunk = result.left();
+ if (result.right() != null) {
+ LOGGER.error("Unhandled poi load exception, poi data will be lost: ", result.right());
+ }
+
+ for (final GenericDataLoadTaskCallback callback : waiters) {
+ callback.markCompleted();
+ }
+
+ completeWaiters = waiters;
+ } else {
+ // cancelled
+ completeWaiters = null;
+
+ // need to re-schedule?
+ if (waiters.isEmpty()) {
+ this.poiDataLoadTaskWaiters = null;
+ // no tasks to schedule _for_
+ } else {
+ poiDataLoadTask = this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask(
+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
+ );
+ poiDataLoadTask.addCallback(this::completePoiLoad);
+ // need one schedule() per waiter
+ for (final GenericDataLoadTaskCallback callback : waiters) {
+ schedulePoiTask |= poiDataLoadTask.schedule(true);
+ }
+ }
+ }
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+
+ if (schedulePoiTask) {
+ poiDataLoadTask.scheduleNow();
+ }
+
+ // avoid holding the scheduling lock while completing
+ if (completeWaiters != null) {
+ for (final GenericDataLoadTaskCallback callback : completeWaiters) {
+ callback.acceptCompleted(result);
+ }
+ }
+ schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ this.checkUnload();
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ }
+
+ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held
+ // however, when the consumer is invoked, it will hold the schedule lock
+ public GenericDataLoadTaskCallback getOrLoadPoiData(final Consumer<GenericDataLoadTask.TaskResult<PoiChunk, Throwable>> consumer) {
+ if (this.isPoiChunkLoaded()) {
+ throw new IllegalStateException("Cannot load poi data, it is already loaded");
+ }
+ // why not just acquire the lock? because the caller NEEDS to call isPoiChunkLoaded before this!
+ if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) {
+ throw new IllegalStateException("Must hold scheduling lock");
+ }
+
+ final GenericDataLoadTaskCallback ret = new PoiDataLoadTaskCallback((Consumer)consumer, this);
+
+ if (this.poiDataLoadTask == null) {
+ this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask(
+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
+ );
+ this.poiDataLoadTask.addCallback(this::completePoiLoad);
+ this.poiDataLoadTaskWaiters = new ArrayList<>();
+ }
+ this.poiDataLoadTaskWaiters.add(ret);
+ if (this.poiDataLoadTask.schedule(true)) {
+ ret.schedule = this.poiDataLoadTask;
+ }
+ this.checkUnload();
+
+ return ret;
+ }
+
+ private static final class PoiDataLoadTaskCallback extends GenericDataLoadTaskCallback {
+
+ public PoiDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer, final NewChunkHolder chunkHolder) {
+ super(consumer, chunkHolder);
+ }
+
+ @Override
+ void internalCancel() {
+ this.chunkHolder.poiDataLoadTaskWaiters.remove(this);
+ this.chunkHolder.poiDataLoadTask.cancel();
+ }
+ }
+
+ public static abstract class GenericDataLoadTaskCallback implements Cancellable {
+
+ protected final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer;
+ protected final NewChunkHolder chunkHolder;
+ protected boolean completed;
+ protected GenericDataLoadTask<?, ?> schedule;
+ protected final AtomicBoolean scheduled = new AtomicBoolean();
+
+ public GenericDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer,
+ final NewChunkHolder chunkHolder) {
+ this.consumer = consumer;
+ this.chunkHolder = chunkHolder;
+ }
+
+ public void schedule() {
+ if (this.scheduled.getAndSet(true)) {
+ throw new IllegalStateException("Double calling schedule()");
+ }
+ if (this.schedule != null) {
+ this.schedule.scheduleNow();
+ this.schedule = null;
+ }
+ }
+
+ boolean isCompleted() {
+ return this.completed;
+ }
+
+ // must hold scheduling lock
+ private boolean setCompleted() {
+ if (this.completed) {
+ return false;
+ }
+ return this.completed = true;
+ }
+
+ // must hold scheduling lock
+ void markCompleted() {
+ if (this.completed) {
+ throw new IllegalStateException("May not be completed here");
+ }
+ this.completed = true;
+ }
+
+ void acceptCompleted(final GenericDataLoadTask.TaskResult<?, Throwable> result) {
+ if (result != null) {
+ if (this.completed) {
+ this.consumer.accept(result);
+ } else {
+ throw new IllegalStateException("Cannot be uncompleted at this point");
+ }
+ } else {
+ throw new NullPointerException("Result cannot be null (cancelled)");
+ }
+ }
+
+ // holds scheduling lock
+ abstract void internalCancel();
+
+ @Override
+ public boolean cancel() {
+ final NewChunkHolder holder = this.chunkHolder;
+ final ReentrantAreaLock.Node schedulingLock = holder.scheduler.schedulingLockArea.lock(holder.chunkX, holder.chunkZ);
+ try {
+ if (!this.completed) {
+ this.completed = true;
+ this.internalCancel();
+ return true;
+ }
+ return false;
+ } finally {
+ holder.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ }
+ }
+
+ private ChunkAccess currentChunk;
+
+ // generation status state
+
+ /**
+ * Current status the chunk has been brought up to by the chunk system. null indicates no work at all
+ */
+ private ChunkStatus currentGenStatus;
+
+ // This allows lockless access to the chunk and last gen status
+ private static final ChunkStatus[] ALL_STATUSES = ChunkStatus.getStatusList().toArray(new ChunkStatus[0]);
+
+ public static final record ChunkCompletion(ChunkAccess chunk, ChunkStatus genStatus) {};
+ private static final VarHandle CHUNK_COMPLETION_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(ChunkCompletion[].class);
+ private final ChunkCompletion[] chunkCompletions = new ChunkCompletion[ALL_STATUSES.length];
+
+ private volatile ChunkCompletion lastChunkCompletion;
+
+ public ChunkCompletion getLastChunkCompletion() {
+ return this.lastChunkCompletion;
+ }
+
+ public ChunkAccess getChunkIfPresentUnchecked(final ChunkStatus status) {
+ final ChunkCompletion completion = (ChunkCompletion)CHUNK_COMPLETION_ARRAY_HANDLE.getVolatile(this.chunkCompletions, status.getIndex());
+ return completion == null ? null : completion.chunk;
+ }
+
+ public ChunkAccess getChunkIfPresent(final ChunkStatus status) {
+ final ChunkStatus maxStatus = ChunkLevel.generationStatus(this.getTicketLevel());
+
+ if (maxStatus == null || status.isAfter(maxStatus)) {
+ return null;
+ }
+
+ return this.getChunkIfPresentUnchecked(status);
+ }
+
+ public void replaceProtoChunk(final ImposterProtoChunk imposterProtoChunk) {
+ for (int i = 0, max = ChunkStatus.FULL.getIndex(); i < max; ++i) {
+ CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, i, new ChunkCompletion(imposterProtoChunk, ALL_STATUSES[i]));
+ }
+ }
+
+ /**
+ * The target final chunk status the chunk system will bring the chunk to.
+ */
+ private ChunkStatus requestedGenStatus;
+
+ private ChunkProgressionTask generationTask;
+ private ChunkStatus generationTaskStatus;
+
+ /**
+ * contains the neighbours that this chunk generation is blocking on
+ */
+ private final ReferenceLinkedOpenHashSet<NewChunkHolder> neighboursBlockingGenTask = new ReferenceLinkedOpenHashSet<>(4);
+
+ /**
+ * map of ChunkHolder -> Required Status for this chunk
+ */
+ private final Reference2ObjectLinkedOpenHashMap<NewChunkHolder, ChunkStatus> neighboursWaitingForUs = new Reference2ObjectLinkedOpenHashMap<>();
+
+ public void addGenerationBlockingNeighbour(final NewChunkHolder neighbour) {
+ this.neighboursBlockingGenTask.add(neighbour);
+ }
+
+ public void addWaitingNeighbour(final NewChunkHolder neighbour, final ChunkStatus requiredStatus) {
+ final boolean wasEmpty = this.neighboursWaitingForUs.isEmpty();
+ this.neighboursWaitingForUs.put(neighbour, requiredStatus);
+ if (wasEmpty) {
+ this.checkUnload();
+ }
+ }
+
+ // priority state
+
+ // the target priority for this chunk to generate at
+ private PrioritisedExecutor.Priority priority = null;
+ private boolean priorityLocked;
+
+ // the priority neighbouring chunks have requested this chunk generate at
+ private PrioritisedExecutor.Priority neighbourRequestedPriority = null;
+
+ public PrioritisedExecutor.Priority getEffectivePriority(final PrioritisedExecutor.Priority dfl) {
+ final PrioritisedExecutor.Priority neighbour = this.neighbourRequestedPriority;
+ final PrioritisedExecutor.Priority us = this.priority;
+
+ if (neighbour == null) {
+ return us == null ? dfl : us;
+ }
+ if (us == null) {
+ return dfl;
+ }
+
+ return PrioritisedExecutor.Priority.max(us, neighbour);
+ }
+
+ private void recalculateNeighbourRequestedPriority() {
+ if (this.neighboursWaitingForUs.isEmpty()) {
+ this.neighbourRequestedPriority = null;
+ return;
+ }
+
+ PrioritisedExecutor.Priority max = null;
+
+ for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) {
+ final PrioritisedExecutor.Priority neighbourPriority = holder.getEffectivePriority(null);
+ if (neighbourPriority != null && (max == null || neighbourPriority.isHigherPriority(max))) {
+ max = neighbourPriority;
+ }
+ }
+
+ final PrioritisedExecutor.Priority current = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
+ this.neighbourRequestedPriority = max;
+ final PrioritisedExecutor.Priority next = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
+
+ if (current == next) {
+ return;
+ }
+
+ // our effective priority has changed, so change our task
+ if (this.generationTask != null) {
+ this.generationTask.setPriority(next);
+ }
+
+ // now propagate this to our neighbours
+ this.recalculateNeighbourPriorities();
+ }
+
+ public void recalculateNeighbourPriorities() {
+ for (final NewChunkHolder holder : this.neighboursBlockingGenTask) {
+ holder.recalculateNeighbourRequestedPriority();
+ }
+ }
+
+ // must hold scheduling lock
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ if (this.priority == null || this.priority.isHigherOrEqualPriority(priority)) {
+ return;
+ }
+ this.setPriority(priority);
+ }
+
+ private void lockPriority() {
+ this.priority = null;
+ this.priorityLocked = true;
+ }
+
+ // must hold scheduling lock
+ public void setPriority(final PrioritisedExecutor.Priority priority) {
+ if (this.priorityLocked) {
+ return;
+ }
+ final PrioritisedExecutor.Priority old = this.getEffectivePriority(null);
+ this.priority = priority;
+ final PrioritisedExecutor.Priority newPriority = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
+
+ if (old != newPriority) {
+ if (this.generationTask != null) {
+ this.generationTask.setPriority(newPriority);
+ }
+ }
+
+ this.recalculateNeighbourPriorities();
+ }
+
+ // must hold scheduling lock
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ if (this.priority == null || this.priority.isLowerOrEqualPriority(priority)) {
+ return;
+ }
+ this.setPriority(priority);
+ }
+
+ // error handling state
+ private ChunkStatus failedGenStatus;
+ private Throwable genTaskException;
+ private Thread genTaskFailedThread;
+
+ private boolean failedLightUpdate;
+
+ public void failedLightUpdate() {
+ this.failedLightUpdate = true;
+ }
+
+ public boolean hasFailedGeneration() {
+ return this.genTaskException != null;
+ }
+
+ // ticket level state
+ public int oldTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1;
+ private int currentTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1;
+
+ public int getTicketLevel() {
+ return this.currentTicketLevel;
+ }
+
+ public final ChunkHolder vanillaChunkHolder;
+
+ public NewChunkHolder(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkTaskScheduler scheduler) {
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.scheduler = scheduler;
+ this.vanillaChunkHolder = new ChunkHolder(
+ new ChunkPos(chunkX, chunkZ), ChunkHolderManager.MAX_TICKET_LEVEL, world,
+ world.getLightEngine(), null, world.getChunkSource().chunkMap
+ );
+ ((ChunkSystemChunkHolder)this.vanillaChunkHolder).moonrise$setRealChunkHolder(this);
+ }
+
+ public ChunkAccess getCurrentChunk() {
+ return this.currentChunk;
+ }
+
+ int getCurrentTicketLevel() {
+ return this.currentTicketLevel;
+ }
+
+ void updateTicketLevel(final int toLevel) {
+ this.currentTicketLevel = toLevel;
+ }
+
+ private int totalNeighboursUsingThisChunk = 0;
+
+ // holds schedule lock
+ public void addNeighbourUsingChunk() {
+ final int now = ++this.totalNeighboursUsingThisChunk;
+
+ if (now == 1) {
+ this.checkUnload();
+ }
+ }
+
+ // holds schedule lock
+ public void removeNeighbourUsingChunk() {
+ final int now = --this.totalNeighboursUsingThisChunk;
+
+ if (now == 0) {
+ this.checkUnload();
+ }
+
+ if (now < 0) {
+ throw new IllegalStateException("Neighbours using this chunk cannot be negative");
+ }
+ }
+
+ // must hold scheduling lock
+ // returns string reason for why chunk should remain loaded, null otherwise
+ public final String isSafeToUnload() {
+ // is ticket level below threshold?
+ if (this.oldTicketLevel <= ChunkHolderManager.MAX_TICKET_LEVEL) {
+ return "ticket_level";
+ }
+
+ // are we being used by another chunk for generation?
+ if (this.totalNeighboursUsingThisChunk != 0) {
+ return "neighbours_generating";
+ }
+
+ // are we going to be used by another chunk for generation?
+ if (!this.neighboursWaitingForUs.isEmpty()) {
+ return "neighbours_waiting";
+ }
+
+ // chunk must be marked inaccessible (i.e. unloaded to plugins)
+ if (this.getChunkStatus() != FullChunkStatus.INACCESSIBLE) {
+ return "fullchunkstatus";
+ }
+
+ // are we currently generating anything, or have requested generation?
+ if (this.generationTask != null) {
+ return "generating";
+ }
+ if (this.requestedGenStatus != null) {
+ return "requested_generation";
+ }
+
+ // entity data requested?
+ if (this.entityDataLoadTask != null) {
+ return "entity_data_requested";
+ }
+
+ // poi data requested?
+ if (this.poiDataLoadTask != null) {
+ return "poi_data_requested";
+ }
+
+ // are we pending serialization?
+ if (this.entityDataUnload != null) {
+ return "entity_serialization";
+ }
+ if (this.poiDataUnload != null) {
+ return "poi_serialization";
+ }
+ if (this.chunkDataUnload != null) {
+ return "chunk_serialization";
+ }
+
+ // Note: light tasks do not need a check, as they add a ticket.
+
+ // nothing is using this chunk, so it should be unloaded
+ return null;
+ }
+
+ /** Unloaded from chunk map */
+ private boolean unloaded;
+
+ void markUnloaded() {
+ this.unloaded = true;
+ }
+
+ private boolean inUnloadQueue = false;
+
+ void removeFromUnloadQueue() {
+ this.inUnloadQueue = false;
+ }
+
+ // must hold scheduling lock
+ private void checkUnload() {
+ if (this.unloaded) {
+ return;
+ }
+ if (this.isSafeToUnload() == null) {
+ // ensure in unload queue
+ if (!this.inUnloadQueue) {
+ this.inUnloadQueue = true;
+ this.scheduler.chunkHolderManager.unloadQueue.addChunk(this.chunkX, this.chunkZ);
+ }
+ } else {
+ // ensure not in unload queue
+ if (this.inUnloadQueue) {
+ this.inUnloadQueue = false;
+ this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ);
+ }
+ }
+ }
+
+ static final record UnloadState(NewChunkHolder holder, ChunkAccess chunk, ChunkEntitySlices entityChunk, PoiChunk poiChunk) {};
+
+ // note: these are completed with null to indicate that no write occurred
+ // they are also completed with null to indicate a null write occurred
+ private UnloadTask chunkDataUnload;
+ private UnloadTask entityDataUnload;
+ private UnloadTask poiDataUnload;
+
+ public static final record UnloadTask(Completable<CompoundTag> completable, DelayedPrioritisedTask task) {}
+
+ public UnloadTask getUnloadTask(final RegionFileIOThread.RegionFileType type) {
+ switch (type) {
+ case CHUNK_DATA:
+ return this.chunkDataUnload;
+ case ENTITY_DATA:
+ return this.entityDataUnload;
+ case POI_DATA:
+ return this.poiDataUnload;
+ default:
+ throw new IllegalStateException("Unknown regionfile type " + type);
+ }
+ }
+
+ private void removeUnloadTask(final RegionFileIOThread.RegionFileType type) {
+ switch (type) {
+ case CHUNK_DATA: {
+ this.chunkDataUnload = null;
+ return;
+ }
+ case ENTITY_DATA: {
+ this.entityDataUnload = null;
+ return;
+ }
+ case POI_DATA: {
+ this.poiDataUnload = null;
+ return;
+ }
+ default:
+ throw new IllegalStateException("Unknown regionfile type " + type);
+ }
+ }
+
+ private UnloadState unloadState;
+
+ // holds schedule lock
+ UnloadState unloadStage1() {
+ // because we hold the scheduling lock, we cannot actually unload anything
+ // so, what we do here instead is to null this chunk's state and setup the unload tasks
+ // the unload tasks will ensure that any loads that take place after stage1 (i.e during stage2, in which
+ // we do not hold the lock) c
+ final ChunkAccess chunk = this.currentChunk;
+ final ChunkEntitySlices entityChunk = this.entityChunk;
+ final PoiChunk poiChunk = this.poiChunk;
+ // chunk state
+ this.currentChunk = null;
+ this.currentGenStatus = null;
+ this.lastChunkCompletion = null;
+ for (int i = 0; i < this.chunkCompletions.length; ++i) {
+ CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, i, (ChunkCompletion)null);
+ }
+ // entity chunk state
+ this.entityChunk = null;
+ this.pendingEntityChunk = null;
+
+ // poi chunk state
+ this.poiChunk = null;
+
+ // priority state
+ this.priorityLocked = false;
+
+ if (chunk != null) {
+ this.chunkDataUnload = new UnloadTask(new Completable<>(), new DelayedPrioritisedTask(PrioritisedExecutor.Priority.NORMAL));
+ }
+ if (poiChunk != null) {
+ this.poiDataUnload = new UnloadTask(new Completable<>(), null);
+ }
+ if (entityChunk != null) {
+ this.entityDataUnload = new UnloadTask(new Completable<>(), null);
+ }
+
+ return this.unloadState = (chunk != null || entityChunk != null || poiChunk != null) ? new UnloadState(this, chunk, entityChunk, poiChunk) : null;
+ }
+
+ // data is null if failed or does not need to be saved
+ void completeAsyncUnloadDataSave(final RegionFileIOThread.RegionFileType type, final CompoundTag data) {
+ if (data != null) {
+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, data, type);
+ }
+
+ this.getUnloadTask(type).completable().complete(data);
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ // can only write to these fields while holding the schedule lock
+ this.removeUnloadTask(type);
+ this.checkUnload();
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ }
+
+ void unloadStage2(final UnloadState state) {
+ this.unloadState = null;
+ final ChunkAccess chunk = state.chunk();
+ final ChunkEntitySlices entityChunk = state.entityChunk();
+ final PoiChunk poiChunk = state.poiChunk();
+
+ final boolean shouldLevelChunkNotSave = ChunkSystemFeatures.forceNoSave(chunk);
+
+ // unload chunk data
+ if (chunk != null) {
+ if (chunk instanceof LevelChunk levelChunk) {
+ levelChunk.setLoaded(false);
+ }
+
+ if (!shouldLevelChunkNotSave) {
+ this.saveChunk(chunk, true);
+ } else {
+ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
+ }
+
+ if (chunk instanceof LevelChunk levelChunk) {
+ this.world.unload(levelChunk);
+ }
+ }
+
+ // unload entity data
+ if (entityChunk != null) {
+ this.saveEntities(entityChunk, true);
+ // yes this is a hack to pass the compound tag through...
+ final CompoundTag lastEntityUnload = this.lastEntityUnload;
+ this.lastEntityUnload = null;
+
+ if (entityChunk.unload()) {
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ entityChunk.setTransient(true);
+ this.entityChunk = entityChunk;
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ } else {
+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionUnload(this.chunkX, this.chunkZ);
+ }
+ // we need to delay the callback until after determining transience, otherwise a potential loader could
+ // set entityChunk before we do
+ this.entityDataUnload.completable().complete(lastEntityUnload);
+ }
+
+ // unload poi data
+ if (poiChunk != null) {
+ if (poiChunk.isDirty() && !shouldLevelChunkNotSave) {
+ this.savePOI(poiChunk, true);
+ } else {
+ this.poiDataUnload.completable().complete(null);
+ }
+
+ if (poiChunk.isLoaded()) {
+ ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$onUnload(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ));
+ }
+ }
+ }
+
+ boolean unloadStage3() {
+ // can only write to these while holding the schedule lock, and we instantly complete them in stage2
+ this.poiDataUnload = null;
+ this.entityDataUnload = null;
+
+ // we need to check if anything has been loaded in the meantime (or if we have transient entities)
+ if (this.entityChunk != null || this.poiChunk != null || this.currentChunk != null) {
+ return false;
+ }
+
+ return this.isSafeToUnload() == null;
+ }
+
+ private void cancelGenTask() {
+ if (this.generationTask != null) {
+ this.generationTask.cancel();
+ } else {
+ // otherwise, we are blocking on neighbours, so remove them
+ if (!this.neighboursBlockingGenTask.isEmpty()) {
+ for (final NewChunkHolder neighbour : this.neighboursBlockingGenTask) {
+ if (neighbour.neighboursWaitingForUs.remove(this) == null) {
+ throw new IllegalStateException("Corrupt state");
+ }
+ if (neighbour.neighboursWaitingForUs.isEmpty()) {
+ neighbour.checkUnload();
+ }
+ }
+ this.neighboursBlockingGenTask.clear();
+ this.checkUnload();
+ }
+ }
+ }
+
+ // holds: ticket level update lock
+ // holds: schedule lock
+ public void processTicketLevelUpdate(final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedLoadStatus) {
+ final int oldLevel = this.oldTicketLevel;
+ final int newLevel = this.currentTicketLevel;
+
+ if (oldLevel == newLevel) {
+ return;
+ }
+
+ this.oldTicketLevel = newLevel;
+
+ final FullChunkStatus oldState = ChunkLevel.fullStatus(oldLevel);
+ final FullChunkStatus newState = ChunkLevel.fullStatus(newLevel);
+ final boolean oldUnloaded = oldLevel > ChunkHolderManager.MAX_TICKET_LEVEL;
+ final boolean newUnloaded = newLevel > ChunkHolderManager.MAX_TICKET_LEVEL;
+
+ final ChunkStatus maxGenerationStatusOld = ChunkLevel.generationStatus(oldLevel);
+ final ChunkStatus maxGenerationStatusNew = ChunkLevel.generationStatus(newLevel);
+
+ // check for cancellations from downgrading ticket level
+ if (this.requestedGenStatus != null && !newState.isOrAfter(FullChunkStatus.FULL) && newLevel > oldLevel) {
+ // note: cancel() may invoke onChunkGenComplete synchronously here
+ if (newUnloaded) {
+ // need to cancel all tasks
+ // note: requested status must be set to null here before cancellation, to indicate to the
+ // completion logic that we do not want rescheduling to occur
+ this.requestedGenStatus = null;
+ this.cancelGenTask();
+ } else {
+ final ChunkStatus toCancel = ((ChunkSystemChunkStatus)maxGenerationStatusNew).moonrise$getNextStatus();
+ final ChunkStatus currentRequestedStatus = this.requestedGenStatus;
+
+ if (currentRequestedStatus.isOrAfter(toCancel)) {
+ // we do have to cancel something here
+ // clamp requested status to the maximum
+ if (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(maxGenerationStatusNew)) {
+ // already generated to status, so we must cancel
+ this.requestedGenStatus = null;
+ this.cancelGenTask();
+ } else {
+ // not generated to status, so we may have to cancel
+ // note: gen task is always 1 status above current gen status if not null
+ this.requestedGenStatus = maxGenerationStatusNew;
+ if (this.generationTaskStatus != null && this.generationTaskStatus.isOrAfter(toCancel)) {
+ // TOOD is this even possible? i don't think so
+ throw new IllegalStateException("?????");
+ }
+ }
+ }
+ }
+ }
+
+ if (oldState != newState) {
+ if (newState.isOrAfter(oldState)) {
+ // status upgrade
+ if (!oldState.isOrAfter(FullChunkStatus.FULL) && newState.isOrAfter(FullChunkStatus.FULL)) {
+ // may need to schedule full load
+ if (this.currentGenStatus != ChunkStatus.FULL) {
+ if (this.requestedGenStatus != null) {
+ this.requestedGenStatus = ChunkStatus.FULL;
+ } else {
+ this.scheduler.schedule(
+ this.chunkX, this.chunkZ, ChunkStatus.FULL, this, scheduledTasks
+ );
+ }
+ }
+ }
+ } else {
+ // status downgrade
+ if (!newState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && oldState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
+ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, null);
+ }
+
+ if (!newState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && oldState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
+ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, null);
+ }
+
+ if (!newState.isOrAfter(FullChunkStatus.FULL) && oldState.isOrAfter(FullChunkStatus.FULL)) {
+ this.completeFullStatusConsumers(FullChunkStatus.FULL, null);
+ }
+ }
+
+ if (this.updatePendingStatus()) {
+ changedLoadStatus.add(this);
+ }
+ }
+
+ if (oldUnloaded != newUnloaded) {
+ this.checkUnload();
+ }
+ }
+
+ static final int NEIGHBOUR_RADIUS = 2;
+ private long fullNeighbourChunksLoadedBitset;
+
+ private static int getFullNeighbourIndex(final int relativeX, final int relativeZ) {
+ // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)
+ // optimised variant of the above by moving some of the ops to compile time
+ return relativeX + (relativeZ * (NEIGHBOUR_RADIUS * 2 + 1)) + (NEIGHBOUR_RADIUS + NEIGHBOUR_RADIUS * ((NEIGHBOUR_RADIUS * 2 + 1)));
+ }
+ public final boolean isNeighbourFullLoaded(final int relativeX, final int relativeZ) {
+ return (this.fullNeighbourChunksLoadedBitset & (1L << getFullNeighbourIndex(relativeX, relativeZ))) != 0;
+ }
+
+ // returns true if this chunk changed pending full status
+ // must hold scheduling lock
+ public final boolean setNeighbourFullLoaded(final int relativeX, final int relativeZ) {
+ final int index = getFullNeighbourIndex(relativeX, relativeZ);
+ this.fullNeighbourChunksLoadedBitset |= (1L << index);
+ return this.updatePendingStatus();
+ }
+
+ // returns true if this chunk changed pending full status
+ // must hold scheduling lock
+ public final boolean setNeighbourFullUnloaded(final int relativeX, final int relativeZ) {
+ final int index = getFullNeighbourIndex(relativeX, relativeZ);
+ this.fullNeighbourChunksLoadedBitset &= ~(1L << index);
+ return this.updatePendingStatus();
+ }
+
+ private static long getLoadedMask(final int radius) {
+ long mask = 0L;
+ for (int dx = -radius; dx <= radius; ++dx) {
+ for (int dz = -radius; dz <= radius; ++dz) {
+ mask |= (1L << getFullNeighbourIndex(dx, dz));
+ }
+ }
+
+ return mask;
+ }
+
+ private static final long CHUNK_LOADED_MASK_RAD0 = getLoadedMask(0);
+ private static final long CHUNK_LOADED_MASK_RAD1 = getLoadedMask(1);
+ private static final long CHUNK_LOADED_MASK_RAD2 = getLoadedMask(2);
+
+ public static boolean areNeighboursFullLoaded(final long bitset, final int radius) {
+ switch (radius) {
+ case 0: {
+ return (bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0;
+ }
+ case 1: {
+ return (bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1;
+ }
+ case 2: {
+ return (bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2;
+ }
+
+ default: {
+ throw new IllegalArgumentException("Radius not recognized: " + radius);
+ }
+ }
+ }
+
+ // only updated while holding scheduling lock
+ private FullChunkStatus pendingFullChunkStatus = FullChunkStatus.INACCESSIBLE;
+ // updated while holding no locks, but adds a ticket before to prevent pending status from dropping
+ // so, current will never update to a value higher than pending
+ private FullChunkStatus currentFullChunkStatus = FullChunkStatus.INACCESSIBLE;
+
+ public FullChunkStatus getChunkStatus() {
+ // no volatile access, access off-main is considered racey anyways
+ return this.currentFullChunkStatus;
+ }
+
+ public boolean isEntityTickingReady() {
+ return this.getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING);
+ }
+
+ public boolean isTickingReady() {
+ return this.getChunkStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING);
+ }
+
+ public boolean isFullChunkReady() {
+ return this.getChunkStatus().isOrAfter(FullChunkStatus.FULL);
+ }
+
+ private static FullChunkStatus getStatusForBitset(final long bitset) {
+ if ((bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2) {
+ return FullChunkStatus.ENTITY_TICKING;
+ } else if ((bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1) {
+ return FullChunkStatus.BLOCK_TICKING;
+ } else if ((bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0) {
+ return FullChunkStatus.FULL;
+ } else {
+ return FullChunkStatus.INACCESSIBLE;
+ }
+ }
+
+ // must hold scheduling lock
+ // returns whether the pending status was changed
+ private boolean updatePendingStatus() {
+ final FullChunkStatus byTicketLevel = ChunkLevel.fullStatus(this.oldTicketLevel); // oldTicketLevel is controlled by scheduling lock
+
+ FullChunkStatus pending = getStatusForBitset(this.fullNeighbourChunksLoadedBitset);
+ if (pending == FullChunkStatus.INACCESSIBLE && byTicketLevel.isOrAfter(FullChunkStatus.FULL) && this.currentGenStatus == ChunkStatus.FULL) {
+ // the bitset is only for chunks that have gone through the status updater
+ // but here we are ready to go to FULL
+ pending = FullChunkStatus.FULL;
+ }
+
+ if (pending.isOrAfter(byTicketLevel)) { // pending >= byTicketLevel
+ // cannot set above ticket level
+ pending = byTicketLevel;
+ }
+
+ if (this.pendingFullChunkStatus == pending) {
+ return false;
+ }
+
+ this.pendingFullChunkStatus = pending;
+
+ return true;
+ }
+
+ private void onFullChunkLoadChange(final boolean loaded, final List<NewChunkHolder> changedFullStatus) {
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, NEIGHBOUR_RADIUS);
+ try {
+ for (int dz = -NEIGHBOUR_RADIUS; dz <= NEIGHBOUR_RADIUS; ++dz) {
+ for (int dx = -NEIGHBOUR_RADIUS; dx <= NEIGHBOUR_RADIUS; ++dx) {
+ final NewChunkHolder holder = (dx | dz) == 0 ? this : this.scheduler.chunkHolderManager.getChunkHolder(dx + this.chunkX, dz + this.chunkZ);
+ if (loaded) {
+ if (holder.setNeighbourFullLoaded(-dx, -dz)) {
+ changedFullStatus.add(holder);
+ }
+ } else {
+ if (holder != null && holder.setNeighbourFullUnloaded(-dx, -dz)) {
+ changedFullStatus.add(holder);
+ }
+ }
+ }
+ }
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ }
+
+ private void changeEntityChunkStatus(final FullChunkStatus toStatus) {
+ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().chunkStatusChange(this.chunkX, this.chunkZ, toStatus);
+ }
+
+ private boolean processingFullStatus = false;
+
+ private void updateCurrentState(final FullChunkStatus to) {
+ this.currentFullChunkStatus = to;
+ }
+
+ // only to be called on the main thread, no locks need to be held
+ public boolean handleFullStatusChange(final List<NewChunkHolder> changedFullStatus) {
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot update full status thread off-main");
+
+ boolean ret = false;
+
+ if (this.processingFullStatus) {
+ // we cannot process updates recursively, as we may be in the middle of logic to upgrade/downgrade status
+ return ret;
+ }
+
+ this.processingFullStatus = true;
+ try {
+ for (;;) {
+ // check if we have any remaining work to do
+
+ // we do not need to hold the scheduling lock to read pending, as changes to pending
+ // will queue a status update
+
+ final FullChunkStatus pending = this.pendingFullChunkStatus;
+ FullChunkStatus current = this.currentFullChunkStatus;
+
+ if (pending == current) {
+ if (pending == FullChunkStatus.INACCESSIBLE) {
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ this.checkUnload();
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ }
+ return ret;
+ }
+
+ ret = true;
+
+ // note: because the chunk system delays any ticket downgrade to the chunk holder manager tick, we
+ // do not need to consider cases where the ticket level may decrease during this call by asynchronous
+ // ticket changes
+
+ // chunks cannot downgrade state while status is pending a change
+ // note: currentChunk must be LevelChunk, as current != pending which means that at least one is not ACCESSIBLE
+ final LevelChunk chunk = (LevelChunk)this.currentChunk;
+
+ // Note: we assume that only load/unload contain plugin logic
+ // plugin logic is anything stupid enough to possibly change the chunk status while it is already
+ // being changed (i.e during load it is possible it will try to set to full ticking)
+ // in order to allow this change, we also need this plugin logic to be contained strictly after all
+ // of the chunk system load callbacks are invoked
+ if (pending.isOrAfter(current)) {
+ // state upgrade
+ if (!current.isOrAfter(FullChunkStatus.FULL) && pending.isOrAfter(FullChunkStatus.FULL)) {
+ this.updateCurrentState(FullChunkStatus.FULL);
+ ChunkSystem.onChunkPreBorder(chunk, this.vanillaChunkHolder);
+ this.scheduler.chunkHolderManager.ensureInAutosave(this);
+ this.changeEntityChunkStatus(FullChunkStatus.FULL);
+ ChunkSystem.onChunkBorder(chunk, this.vanillaChunkHolder);
+ this.onFullChunkLoadChange(true, changedFullStatus);
+ this.completeFullStatusConsumers(FullChunkStatus.FULL, chunk);
+ }
+
+ if (!current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
+ this.updateCurrentState(FullChunkStatus.BLOCK_TICKING);
+ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING);
+ ChunkSystem.onChunkTicking(chunk, this.vanillaChunkHolder);
+ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, chunk);
+ }
+
+ if (!current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
+ this.updateCurrentState(FullChunkStatus.ENTITY_TICKING);
+ this.changeEntityChunkStatus(FullChunkStatus.ENTITY_TICKING);
+ ChunkSystem.onChunkEntityTicking(chunk, this.vanillaChunkHolder);
+ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, chunk);
+ }
+ } else {
+ if (current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && !pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
+ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING);
+ ChunkSystem.onChunkNotEntityTicking(chunk, this.vanillaChunkHolder);
+ this.updateCurrentState(FullChunkStatus.BLOCK_TICKING);
+ }
+
+ if (current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && !pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) {
+ this.changeEntityChunkStatus(FullChunkStatus.FULL);
+ ChunkSystem.onChunkNotTicking(chunk, this.vanillaChunkHolder);
+ this.updateCurrentState(FullChunkStatus.FULL);
+ }
+
+ if (current.isOrAfter(FullChunkStatus.FULL) && !pending.isOrAfter(FullChunkStatus.FULL)) {
+ this.onFullChunkLoadChange(false, changedFullStatus);
+ this.changeEntityChunkStatus(FullChunkStatus.INACCESSIBLE);
+ ChunkSystem.onChunkNotBorder(chunk, this.vanillaChunkHolder);
+ ChunkSystem.onChunkPostNotBorder(chunk, this.vanillaChunkHolder);
+ this.updateCurrentState(FullChunkStatus.INACCESSIBLE);
+ }
+ }
+ }
+ } finally {
+ this.processingFullStatus = false;
+ }
+ }
+
+ // note: must hold scheduling lock
+ // rets true if the current requested gen status is not null (effectively, whether further scheduling is not needed)
+ boolean upgradeGenTarget(final ChunkStatus toStatus) {
+ if (toStatus == null) {
+ throw new NullPointerException("toStatus cannot be null");
+ }
+ if (this.requestedGenStatus == null && this.generationTask == null) {
+ return false;
+ }
+ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(toStatus)) {
+ this.requestedGenStatus = toStatus;
+ }
+ return true;
+ }
+
+ public void setGenerationTarget(final ChunkStatus toStatus) {
+ this.requestedGenStatus = toStatus;
+ }
+
+ public boolean hasGenerationTask() {
+ return this.generationTask != null;
+ }
+
+ public ChunkStatus getCurrentGenStatus() {
+ return this.currentGenStatus;
+ }
+
+ public ChunkStatus getRequestedGenStatus() {
+ return this.requestedGenStatus;
+ }
+
+ private final Reference2ObjectOpenHashMap<ChunkStatus, List<Consumer<ChunkAccess>>> statusWaiters = new Reference2ObjectOpenHashMap<>();
+
+ void addStatusConsumer(final ChunkStatus status, final Consumer<ChunkAccess> consumer) {
+ this.statusWaiters.computeIfAbsent(status, (final ChunkStatus keyInMap) -> {
+ return new ArrayList<>(4);
+ }).add(consumer);
+ }
+
+ private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) {
+ // need to tell future statuses to complete if cancelled
+ do {
+ this.completeStatusConsumers0(status, chunk);
+ } while (chunk == null && status != (status = ((ChunkSystemChunkStatus)status).moonrise$getNextStatus()));
+ }
+
+ private void completeStatusConsumers0(final ChunkStatus status, final ChunkAccess chunk) {
+ final List<Consumer<ChunkAccess>> consumers;
+ consumers = this.statusWaiters.remove(status);
+
+ if (consumers == null) {
+ return;
+ }
+
+ // must be scheduled to main, we do not trust the callback to not do anything stupid
+ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
+ for (final Consumer<ChunkAccess> consumer : consumers) {
+ try {
+ consumer.accept(chunk);
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to process chunk status callback", thr);
+ }
+ }
+ }, PrioritisedExecutor.Priority.HIGHEST);
+ }
+
+ private final Reference2ObjectOpenHashMap<FullChunkStatus, List<Consumer<LevelChunk>>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>();
+
+ void addFullStatusConsumer(final FullChunkStatus status, final Consumer<LevelChunk> consumer) {
+ this.fullStatusWaiters.computeIfAbsent(status, (final FullChunkStatus keyInMap) -> {
+ return new ArrayList<>(4);
+ }).add(consumer);
+ }
+
+ private void completeFullStatusConsumers(FullChunkStatus status, final LevelChunk chunk) {
+ final List<Consumer<LevelChunk>> consumers;
+ consumers = this.fullStatusWaiters.remove(status);
+
+ if (consumers == null) {
+ return;
+ }
+
+ // must be scheduled to main, we do not trust the callback to not do anything stupid
+ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
+ for (final Consumer<LevelChunk> consumer : consumers) {
+ try {
+ consumer.accept(chunk);
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to process chunk status callback", thr);
+ }
+ }
+ }, PrioritisedExecutor.Priority.HIGHEST);
+ }
+
+ // note: must hold scheduling lock
+ private void onChunkGenComplete(final ChunkAccess newChunk, final ChunkStatus newStatus,
+ final List<ChunkProgressionTask> scheduleList, final List<NewChunkHolder> changedLoadStatus) {
+ if (!this.neighboursBlockingGenTask.isEmpty()) {
+ throw new IllegalStateException("Cannot have neighbours blocking this gen task");
+ }
+ if (newChunk != null || (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(newStatus))) {
+ this.completeStatusConsumers(newStatus, newChunk);
+ }
+ // done now, clear state (must be done before scheduling new tasks)
+ this.generationTask = null;
+ this.generationTaskStatus = null;
+ if (newChunk == null) {
+ // task was cancelled
+ // should be careful as this could be called while holding the schedule lock and/or inside the
+ // ticket level update
+ // while a task may be cancelled, it is possible for it to be later re-scheduled
+ // however, because generationTask is only set to null on _completion_, the scheduler leaves
+ // the rescheduling logic to us here
+ final ChunkStatus requestedGenStatus = this.requestedGenStatus;
+ this.requestedGenStatus = null;
+ if (requestedGenStatus != null) {
+ // it looks like it has been requested, so we must reschedule
+ if (!this.neighboursWaitingForUs.isEmpty()) {
+ for (final Iterator<Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus>> iterator = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
+ final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry = iterator.next();
+
+ final NewChunkHolder chunkHolder = entry.getKey();
+ final ChunkStatus toStatus = entry.getValue();
+
+ if (!requestedGenStatus.isOrAfter(toStatus)) {
+ // if we were cancelled, we are responsible for removing the waiter
+ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) {
+ throw new IllegalStateException("Corrupt state");
+ }
+ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) {
+ chunkHolder.checkUnload();
+ }
+ iterator.remove();
+ continue;
+ }
+ }
+ }
+
+ // note: only after generationTask -> null, generationTaskStatus -> null, and requestedGenStatus -> null
+ this.scheduler.schedule(
+ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList
+ );
+
+ // return, can't do anything further
+ return;
+ }
+
+ if (!this.neighboursWaitingForUs.isEmpty()) {
+ for (final NewChunkHolder chunkHolder : this.neighboursWaitingForUs.keySet()) {
+ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) {
+ throw new IllegalStateException("Corrupt state");
+ }
+ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) {
+ chunkHolder.checkUnload();
+ }
+ }
+ this.neighboursWaitingForUs.clear();
+ }
+ // reset priority, we have nothing left to generate to
+ this.setPriority(null);
+ this.checkUnload();
+ return;
+ }
+
+ this.currentChunk = newChunk;
+ this.currentGenStatus = newStatus;
+ final ChunkCompletion completion = new ChunkCompletion(newChunk, newStatus);
+ CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, newStatus.getIndex(), completion);
+ this.lastChunkCompletion = completion;
+
+ final ChunkStatus requestedGenStatus = this.requestedGenStatus;
+
+ List<NewChunkHolder> needsScheduling = null;
+ boolean recalculatePriority = false;
+ for (final Iterator<Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus>> iterator
+ = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
+ final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry = iterator.next();
+ final NewChunkHolder neighbour = entry.getKey();
+ final ChunkStatus requiredStatus = entry.getValue();
+
+ if (!newStatus.isOrAfter(requiredStatus)) {
+ if (requestedGenStatus == null || !requestedGenStatus.isOrAfter(requiredStatus)) {
+ // if we're cancelled, still need to clear this map
+ if (!neighbour.neighboursBlockingGenTask.remove(this)) {
+ throw new IllegalStateException("Neighbour is not waiting for us?");
+ }
+ if (neighbour.neighboursBlockingGenTask.isEmpty()) {
+ neighbour.checkUnload();
+ }
+
+ iterator.remove();
+ }
+ continue;
+ }
+
+ // doesn't matter what isCancelled is here, we need to schedule if we can
+
+ recalculatePriority = true;
+ if (!neighbour.neighboursBlockingGenTask.remove(this)) {
+ throw new IllegalStateException("Neighbour is not waiting for us?");
+ }
+
+ if (neighbour.neighboursBlockingGenTask.isEmpty()) {
+ if (neighbour.requestedGenStatus != null) {
+ if (needsScheduling == null) {
+ needsScheduling = new ArrayList<>();
+ }
+ needsScheduling.add(neighbour);
+ } else {
+ neighbour.checkUnload();
+ }
+ }
+
+ // remove last; access to entry will throw if removed
+ iterator.remove();
+ }
+
+ if (newStatus == ChunkStatus.FULL) {
+ this.lockPriority();
+ // try to push pending to FULL
+ if (this.updatePendingStatus()) {
+ changedLoadStatus.add(this);
+ }
+ }
+
+ if (recalculatePriority) {
+ this.recalculateNeighbourRequestedPriority();
+ }
+
+ if (requestedGenStatus != null && !newStatus.isOrAfter(requestedGenStatus)) {
+ this.scheduleNeighbours(needsScheduling, scheduleList);
+
+ // we need to schedule more tasks now
+ this.scheduler.schedule(
+ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList
+ );
+ } else {
+ // we're done now
+ if (requestedGenStatus != null) {
+ this.requestedGenStatus = null;
+ }
+ // reached final stage, so stop scheduling now
+ this.setPriority(null);
+ this.checkUnload();
+
+ this.scheduleNeighbours(needsScheduling, scheduleList);
+ }
+ }
+
+ private void scheduleNeighbours(final List<NewChunkHolder> needsScheduling, final List<ChunkProgressionTask> scheduleList) {
+ if (needsScheduling != null) {
+ for (int i = 0, len = needsScheduling.size(); i < len; ++i) {
+ final NewChunkHolder neighbour = needsScheduling.get(i);
+
+ this.scheduler.schedule(
+ neighbour.chunkX, neighbour.chunkZ, neighbour.requestedGenStatus, neighbour, scheduleList
+ );
+ }
+ }
+ }
+
+ public void setGenerationTask(final ChunkProgressionTask generationTask, final ChunkStatus taskStatus,
+ final List<NewChunkHolder> neighbours) {
+ if (this.generationTask != null || (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(taskStatus))) {
+ throw new IllegalStateException("Currently generating or provided task is trying to generate to a level we are already at!");
+ }
+ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(taskStatus)) {
+ throw new IllegalStateException("Cannot schedule generation task when not requested");
+ }
+ this.generationTask = generationTask;
+ this.generationTaskStatus = taskStatus;
+
+ for (int i = 0, len = neighbours.size(); i < len; ++i) {
+ neighbours.get(i).addNeighbourUsingChunk();
+ }
+
+ this.checkUnload();
+
+ generationTask.onComplete((final ChunkAccess access, final Throwable thr) -> {
+ if (generationTask != this.generationTask) {
+ throw new IllegalStateException(
+ "Cannot complete generation task '" + generationTask + "' because we are waiting on '" + this.generationTask + "' instead!"
+ );
+ }
+ if (thr != null) {
+ if (this.genTaskException != null) {
+ LOGGER.warn("Ignoring exception for " + this.toString(), thr);
+ return;
+ }
+ // don't set generation task to null, so that scheduling will not attempt to create another task and it
+ // will automatically block any further scheduling usage of this chunk as it will wait forever for a failed
+ // task to complete
+ this.genTaskException = thr;
+ this.failedGenStatus = taskStatus;
+ this.genTaskFailedThread = Thread.currentThread();
+
+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
+ "Generation task", ChunkTaskScheduler.stringIfNull(generationTask),
+ "Task to status", ChunkTaskScheduler.stringIfNull(taskStatus)
+ ), thr);
+ return;
+ }
+
+ final boolean scheduleTasks;
+ List<ChunkProgressionTask> tasks = ChunkHolderManager.getCurrentTicketUpdateScheduling();
+ if (tasks == null) {
+ scheduleTasks = true;
+ tasks = new ArrayList<>();
+ } else {
+ scheduleTasks = false;
+ // we are currently updating ticket levels, so we already hold the schedule lock
+ // this means we have to leave the ticket level update to handle the scheduling
+ }
+ final List<NewChunkHolder> changedLoadStatus = new ArrayList<>();
+ // theoretically, we could schedule a chunk at the max radius which performs another max radius access. So we need to double the radius.
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, 2 * ChunkTaskScheduler.getMaxAccessRadius());
+ try {
+ for (int i = 0, len = neighbours.size(); i < len; ++i) {
+ neighbours.get(i).removeNeighbourUsingChunk();
+ }
+ this.onChunkGenComplete(access, taskStatus, tasks, changedLoadStatus);
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+ this.scheduler.chunkHolderManager.addChangedStatuses(changedLoadStatus);
+
+ if (scheduleTasks) {
+ // can't hold the lock while scheduling, so we have to build the tasks and then schedule after
+ for (int i = 0, len = tasks.size(); i < len; ++i) {
+ tasks.get(i).schedule();
+ }
+ }
+ });
+ }
+
+ public PoiChunk getPoiChunk() {
+ return this.poiChunk;
+ }
+
+ public ChunkEntitySlices getEntityChunk() {
+ return this.entityChunk;
+ }
+
+ public long lastAutoSave;
+
+ public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {}
+
+ public SaveStat save(final boolean shutdown) {
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main");
+
+ ChunkAccess chunk = this.getCurrentChunk();
+ PoiChunk poi = this.getPoiChunk();
+ ChunkEntitySlices entities = this.getEntityChunk();
+ boolean executedUnloadTask = false;
+
+ if (shutdown) {
+ // make sure that the async unloads complete
+ if (this.unloadState != null) {
+ // must have errored during unload
+ chunk = this.unloadState.chunk();
+ poi = this.unloadState.poiChunk();
+ entities = this.unloadState.entityChunk();
+ }
+ final UnloadTask chunkUnloadTask = this.chunkDataUnload;
+ final DelayedPrioritisedTask chunkDataUnloadTask = chunkUnloadTask == null ? null : chunkUnloadTask.task();
+ if (chunkDataUnloadTask != null) {
+ final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnloadTask.getTask();
+ if (unloadTask != null) {
+ executedUnloadTask = unloadTask.execute();
+ }
+ }
+ }
+
+ final boolean forceNoSaveChunk = ChunkSystemFeatures.forceNoSave(chunk);
+
+ // can only synchronously save worldgen chunks during shutdown
+ boolean canSaveChunk = !forceNoSaveChunk && (chunk != null && ((shutdown || chunk instanceof LevelChunk) && chunk.isUnsaved()));
+ boolean canSavePOI = !forceNoSaveChunk && (poi != null && poi.isDirty());
+ boolean canSaveEntities = entities != null;
+
+ if (canSaveChunk) {
+ canSaveChunk = this.saveChunk(chunk, false);
+ }
+ if (canSavePOI) {
+ canSavePOI = this.savePOI(poi, false);
+ }
+ if (canSaveEntities) {
+ // on shutdown, we need to force transient entity chunks to save
+ canSaveEntities = this.saveEntities(entities, shutdown);
+ if (shutdown) {
+ this.lastEntityUnload = null;
+ }
+ }
+
+ return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ? new SaveStat(executedUnloadTask || canSaveChunk, canSaveEntities, canSavePOI): null;
+ }
+
+ static final class AsyncChunkSerializeTask implements Runnable {
+
+ private final ServerLevel world;
+ private final ChunkAccess chunk;
+ private final AsyncChunkSaveData asyncSaveData;
+ private final NewChunkHolder toComplete;
+
+ public AsyncChunkSerializeTask(final ServerLevel world, final ChunkAccess chunk, final AsyncChunkSaveData asyncSaveData,
+ final NewChunkHolder toComplete) {
+ this.world = world;
+ this.chunk = chunk;
+ this.asyncSaveData = asyncSaveData;
+ this.toComplete = toComplete;
+ }
+
+ @Override
+ public void run() {
+ final CompoundTag toSerialize;
+ try {
+ toSerialize = ChunkSystemFeatures.saveChunkAsync(this.world, this.chunk, this.asyncSaveData);
+ } catch (final Throwable throwable) {
+ LOGGER.error("Failed to asynchronously save chunk " + this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(this.world) + "', falling back to synchronous save", throwable);
+ final ChunkPos pos = this.chunk.getPos();
+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkTask(pos.x, pos.z, () -> {
+ final CompoundTag synchronousSave;
+ try {
+ synchronousSave = ChunkSystemFeatures.saveChunkAsync(AsyncChunkSerializeTask.this.world, AsyncChunkSerializeTask.this.chunk, AsyncChunkSerializeTask.this.asyncSaveData);
+ } catch (final Throwable throwable2) {
+ LOGGER.error("Failed to synchronously save chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(AsyncChunkSerializeTask.this.world) + "', chunk data will be lost", throwable2);
+ AsyncChunkSerializeTask.this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
+ return;
+ }
+
+ AsyncChunkSerializeTask.this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, synchronousSave);
+ LOGGER.info("Successfully serialized chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(AsyncChunkSerializeTask.this.world) + "' synchronously");
+
+ }, PrioritisedExecutor.Priority.HIGHEST);
+ return;
+ }
+ this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, toSerialize);
+ }
+
+ @Override
+ public String toString() {
+ return "AsyncChunkSerializeTask{" +
+ "chunk={pos=" + this.chunk.getPos() + ",world=\"" + WorldUtil.getWorldName(this.world) + "\"}" +
+ "}";
+ }
+ }
+
+ private boolean saveChunk(final ChunkAccess chunk, final boolean unloading) {
+ if (!chunk.isUnsaved()) {
+ if (unloading) {
+ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
+ }
+ return false;
+ }
+ boolean completing = false;
+ boolean failedAsyncPrepare = false;
+ try {
+ if (unloading && ChunkSystemFeatures.supportsAsyncChunkSave()) {
+ try {
+ final AsyncChunkSaveData asyncSaveData = ChunkSystemFeatures.getAsyncSaveData(this.world, chunk);
+
+ final PrioritisedExecutor.PrioritisedTask task = this.scheduler.loadExecutor.createTask(new AsyncChunkSerializeTask(this.world, chunk, asyncSaveData, this));
+
+ this.chunkDataUnload.task().setTask(task);
+
+ chunk.setUnsaved(false);
+
+ task.queue();
+
+ return true;
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to prepare async chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', falling back to synchronous save", thr);
+ failedAsyncPrepare = true;
+ // fall through to synchronous save
+ }
+ }
+
+ final CompoundTag save = ChunkSerializer.write(this.world, chunk);
+
+ if (unloading) {
+ completing = true;
+ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, save);
+ if (failedAsyncPrepare) {
+ LOGGER.info("Successfully serialized chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "' synchronously");
+ }
+ } else {
+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.CHUNK_DATA);
+ }
+ chunk.setUnsaved(false);
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'");
+ if (unloading && !completing) {
+ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
+ }
+ }
+
+ return true;
+ }
+
+ private boolean lastEntitySaveNull;
+ private CompoundTag lastEntityUnload;
+ private boolean saveEntities(final ChunkEntitySlices entities, final boolean unloading) {
+ try {
+ CompoundTag mergeFrom = null;
+ if (entities.isTransient()) {
+ if (!unloading) {
+ // if we're a transient chunk, we cannot save until unloading because otherwise a double save will
+ // result in double adding the entities
+ return false;
+ }
+ try {
+ mergeFrom = RegionFileIOThread.loadData(this.world, this.chunkX, this.chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, PrioritisedExecutor.Priority.BLOCKING);
+ } catch (final Exception ex) {
+ LOGGER.error("Cannot merge transient entities for chunk (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', data on disk will be replaced", ex);
+ }
+ }
+
+ final CompoundTag save = entities.save();
+ if (mergeFrom != null) {
+ if (save == null) {
+ // don't override the data on disk with nothing
+ return false;
+ } else {
+ ChunkEntitySlices.copyEntities(mergeFrom, save);
+ }
+ }
+ if (save == null && this.lastEntitySaveNull) {
+ return false;
+ }
+
+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.ENTITY_DATA);
+ this.lastEntitySaveNull = save == null;
+ if (unloading) {
+ this.lastEntityUnload = save;
+ }
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'");
+ }
+
+ return true;
+ }
+
+ private boolean lastPoiSaveNull;
+ private boolean savePOI(final PoiChunk poi, final boolean unloading) {
+ try {
+ final CompoundTag save = poi.save();
+ poi.setDirty(false);
+ if (save == null && this.lastPoiSaveNull) {
+ if (unloading) {
+ this.poiDataUnload.completable().complete(null);
+ }
+ return false;
+ }
+
+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.POI_DATA);
+ this.lastPoiSaveNull = save == null;
+ if (unloading) {
+ this.poiDataUnload.completable().complete(save);
+ }
+ } catch (final Throwable thr) {
+ LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'");
+ }
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ final ChunkCompletion lastCompletion = this.lastChunkCompletion;
+ final ChunkEntitySlices entityChunk = this.entityChunk;
+ final FullChunkStatus pendingFullStatus = this.pendingFullChunkStatus;
+ final FullChunkStatus currentFullStatus = this.currentFullChunkStatus;
+ return "NewChunkHolder{" +
+ "world=" + WorldUtil.getWorldName(this.world) +
+ ", chunkX=" + this.chunkX +
+ ", chunkZ=" + this.chunkZ +
+ ", entityChunkFromDisk=" + (entityChunk != null && !entityChunk.isTransient()) +
+ ", lastChunkCompletion={chunk_class=" + (lastCompletion == null || lastCompletion.chunk() == null ? "null" : lastCompletion.chunk().getClass().getName()) + ",status=" + (lastCompletion == null ? "null" : lastCompletion.genStatus()) + "}" +
+ ", currentGenStatus=" + this.currentGenStatus +
+ ", requestedGenStatus=" + this.requestedGenStatus +
+ ", generationTask=" + this.generationTask +
+ ", generationTaskStatus=" + this.generationTaskStatus +
+ ", priority=" + this.priority +
+ ", priorityLocked=" + this.priorityLocked +
+ ", neighbourRequestedPriority=" + this.neighbourRequestedPriority +
+ ", effective_priority=" + this.getEffectivePriority(null) +
+ ", oldTicketLevel=" + this.oldTicketLevel +
+ ", currentTicketLevel=" + this.currentTicketLevel +
+ ", totalNeighboursUsingThisChunk=" + this.totalNeighboursUsingThisChunk +
+ ", fullNeighbourChunksLoadedBitset=" + this.fullNeighbourChunksLoadedBitset +
+ ", currentChunkStatus=" + currentFullStatus +
+ ", pendingChunkStatus=" + pendingFullStatus +
+ ", is_unload_safe=" + this.isSafeToUnload() +
+ ", killed=" + this.unloaded +
+ '}';
+ }
+
+ private static JsonElement serializeStacktraceElement(final StackTraceElement element) {
+ return element == null ? JsonNull.INSTANCE : new JsonPrimitive(element.toString());
+ }
+
+ private static JsonObject serializeCompletable(final Completable<?> completable) {
+ final JsonObject ret = new JsonObject();
+
+ if (completable == null) {
+ return ret;
+ }
+
+ ret.addProperty("valid", Boolean.TRUE);
+
+ final boolean isCompleted = completable.isCompleted();
+ ret.addProperty("completed", Boolean.valueOf(isCompleted));
+
+ if (isCompleted) {
+ final Throwable throwable = completable.getThrowable();
+ if (throwable != null) {
+ final JsonArray throwableJson = new JsonArray();
+ ret.add("throwable", throwableJson);
+
+ for (final StackTraceElement element : throwable.getStackTrace()) {
+ throwableJson.add(serializeStacktraceElement(element));
+ }
+ } else {
+ final Object result = completable.getResult();
+ ret.add("result_class", result == null ? JsonNull.INSTANCE : new JsonPrimitive(result.getClass().getName()));
+ }
+ }
+
+ return ret;
+ }
+
+ // (probably) holds ticket and scheduling lock
+ public JsonObject getDebugJson() {
+ final JsonObject ret = new JsonObject();
+
+ final ChunkCompletion lastCompletion = this.lastChunkCompletion;
+ final ChunkEntitySlices slices = this.entityChunk;
+ final PoiChunk poiChunk = this.poiChunk;
+
+ ret.addProperty("chunkX", Integer.valueOf(this.chunkX));
+ ret.addProperty("chunkZ", Integer.valueOf(this.chunkZ));
+ ret.addProperty("entity_chunk", slices == null ? "null" : "transient=" + slices.isTransient());
+ ret.addProperty("poi_chunk", "null=" + (poiChunk == null));
+ ret.addProperty("completed_chunk_class", lastCompletion == null ? "null" : lastCompletion.chunk().getClass().getName());
+ ret.addProperty("completed_gen_status", lastCompletion == null ? "null" : lastCompletion.genStatus().toString());
+ ret.addProperty("priority", Objects.toString(this.priority));
+ ret.addProperty("neighbour_requested_priority", Objects.toString(this.neighbourRequestedPriority));
+ ret.addProperty("generation_task", Objects.toString(this.generationTask));
+ ret.addProperty("is_safe_unload", Objects.toString(this.isSafeToUnload()));
+ ret.addProperty("old_ticket_level", Integer.valueOf(this.oldTicketLevel));
+ ret.addProperty("current_ticket_level", Integer.valueOf(this.currentTicketLevel));
+ ret.addProperty("neighbours_using_chunk", Integer.valueOf(this.totalNeighboursUsingThisChunk));
+
+ final JsonObject neighbourWaitState = new JsonObject();
+ ret.add("neighbour_state", neighbourWaitState);
+
+ final JsonArray blockingGenNeighbours = new JsonArray();
+ neighbourWaitState.add("blocking_gen_task", blockingGenNeighbours);
+ for (final NewChunkHolder blockingGenNeighbour : this.neighboursBlockingGenTask) {
+ final JsonObject neighbour = new JsonObject();
+ blockingGenNeighbours.add(neighbour);
+
+ neighbour.addProperty("chunkX", Integer.valueOf(blockingGenNeighbour.chunkX));
+ neighbour.addProperty("chunkZ", Integer.valueOf(blockingGenNeighbour.chunkZ));
+ }
+
+ final JsonArray neighboursWaitingForUs = new JsonArray();
+ neighbourWaitState.add("neighbours_waiting_on_us", neighboursWaitingForUs);
+ for (final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry : this.neighboursWaitingForUs.reference2ObjectEntrySet()) {
+ final NewChunkHolder holder = entry.getKey();
+ final ChunkStatus status = entry.getValue();
+
+ final JsonObject neighbour = new JsonObject();
+ neighboursWaitingForUs.add(neighbour);
+
+
+ neighbour.addProperty("chunkX", Integer.valueOf(holder.chunkX));
+ neighbour.addProperty("chunkZ", Integer.valueOf(holder.chunkZ));
+ neighbour.addProperty("waiting_for", Objects.toString(status));
+ }
+
+ ret.addProperty("pending_chunk_full_status", Objects.toString(this.pendingFullChunkStatus));
+ ret.addProperty("current_chunk_full_status", Objects.toString(this.currentFullChunkStatus));
+ ret.addProperty("generation_task", Objects.toString(this.generationTask));
+ ret.addProperty("requested_generation", Objects.toString(this.requestedGenStatus));
+ ret.addProperty("has_entity_load_task", Boolean.valueOf(this.entityDataLoadTask != null));
+ ret.addProperty("has_poi_load_task", Boolean.valueOf(this.poiDataLoadTask != null));
+
+ final UnloadTask entityDataUnload = this.entityDataUnload;
+ final UnloadTask poiDataUnload = this.poiDataUnload;
+ final UnloadTask chunkDataUnload = this.chunkDataUnload;
+
+ ret.add("entity_unload_completable", serializeCompletable(entityDataUnload == null ? null : entityDataUnload.completable()));
+ ret.add("poi_unload_completable", serializeCompletable(poiDataUnload == null ? null : poiDataUnload.completable()));
+ ret.add("chunk_unload_completable", serializeCompletable(chunkDataUnload == null ? null : chunkDataUnload.completable()));
+
+ final DelayedPrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task();
+ if (unloadTask == null) {
+ ret.addProperty("unload_task_priority", "null");
+ ret.addProperty("unload_task_priority_raw", "null");
+ } else {
+ ret.addProperty("unload_task_priority", Objects.toString(unloadTask.getPriority()));
+ ret.addProperty("unload_task_priority_raw", Integer.valueOf(unloadTask.getPriorityInternal()));
+ }
+
+ ret.addProperty("killed", Boolean.valueOf(this.unloaded));
+
+ return ret;
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..261e09454f49d04eb159c984ec695d7c7aa6a3a8
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
@@ -0,0 +1,215 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import java.lang.invoke.VarHandle;
+
+public abstract class PriorityHolder {
+
+ protected volatile int priority;
+ protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(PriorityHolder.class, "priority", int.class);
+
+ protected static final int PRIORITY_SCHEDULED = Integer.MIN_VALUE >>> 0;
+ protected static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 1;
+
+ protected final int getPriorityVolatile() {
+ return (int)PRIORITY_HANDLE.getVolatile((PriorityHolder)this);
+ }
+
+ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) {
+ return (int)PRIORITY_HANDLE.compareAndExchange((PriorityHolder)this, (int)expect, (int)update);
+ }
+
+ protected final int getAndOrPriorityVolatile(final int val) {
+ return (int)PRIORITY_HANDLE.getAndBitwiseOr((PriorityHolder)this, (int)val);
+ }
+
+ protected final void setPriorityPlain(final int val) {
+ PRIORITY_HANDLE.set((PriorityHolder)this, (int)val);
+ }
+
+ protected PriorityHolder(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.setPriorityPlain(priority.priority);
+ }
+
+ // used only for debug json
+ public boolean isScheduled() {
+ return (this.getPriorityVolatile() & PRIORITY_SCHEDULED) != 0;
+ }
+
+ // returns false if cancelled
+ public boolean markExecuting() {
+ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0;
+ }
+
+ public boolean isMarkedExecuted() {
+ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0;
+ }
+
+ public void cancel() {
+ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) {
+ // cancelled already
+ return;
+ }
+ this.cancelScheduled();
+ }
+
+ public void schedule() {
+ int priority = this.getPriorityVolatile();
+
+ if ((priority & PRIORITY_SCHEDULED) != 0) {
+ throw new IllegalStateException("schedule() called twice");
+ }
+
+ if ((priority & PRIORITY_EXECUTED) != 0) {
+ // cancelled
+ return;
+ }
+
+ this.scheduleTask(PrioritisedExecutor.Priority.getPriority(priority));
+
+ int failures = 0;
+ for (;;) {
+ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SCHEDULED))) {
+ return;
+ }
+
+ if ((priority & PRIORITY_SCHEDULED) != 0) {
+ throw new IllegalStateException("schedule() called twice");
+ }
+
+ if ((priority & PRIORITY_EXECUTED) != 0) {
+ // cancelled or executed
+ return;
+ }
+
+ this.setPriorityScheduled(PrioritisedExecutor.Priority.getPriority(priority));
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ public final PrioritisedExecutor.Priority getPriority() {
+ final int ret = this.getPriorityVolatile();
+ if ((ret & PRIORITY_EXECUTED) != 0) {
+ return PrioritisedExecutor.Priority.COMPLETING;
+ }
+ if ((ret & PRIORITY_SCHEDULED) != 0) {
+ return this.getScheduledPriority();
+ }
+ return PrioritisedExecutor.Priority.getPriority(ret);
+ }
+
+ public final void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
+ int failures = 0;
+ for (int curr = this.getPriorityVolatile();;) {
+ if ((curr & PRIORITY_EXECUTED) != 0) {
+ return;
+ }
+
+ if ((curr & PRIORITY_SCHEDULED) != 0) {
+ this.lowerPriorityScheduled(priority);
+ return;
+ }
+
+ if (!priority.isLowerPriority(curr)) {
+ return;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
+ return;
+ }
+
+ // failed, retry
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ public final void setPriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
+ int failures = 0;
+ for (int curr = this.getPriorityVolatile();;) {
+ if ((curr & PRIORITY_EXECUTED) != 0) {
+ return;
+ }
+
+ if ((curr & PRIORITY_SCHEDULED) != 0) {
+ this.setPriorityScheduled(priority);
+ return;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
+ return;
+ }
+
+ // failed, retry
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ public final void raisePriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
+ int failures = 0;
+ for (int curr = this.getPriorityVolatile();;) {
+ if ((curr & PRIORITY_EXECUTED) != 0) {
+ return;
+ }
+
+ if ((curr & PRIORITY_SCHEDULED) != 0) {
+ this.raisePriorityScheduled(priority);
+ return;
+ }
+
+ if (!priority.isHigherPriority(curr)) {
+ return;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
+ return;
+ }
+
+ // failed, retry
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ protected abstract void cancelScheduled();
+
+ protected abstract PrioritisedExecutor.Priority getScheduledPriority();
+
+ protected abstract void scheduleTask(final PrioritisedExecutor.Priority priority);
+
+ protected abstract void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority);
+
+ protected abstract void setPriorityScheduled(final PrioritisedExecutor.Priority priority);
+
+ protected abstract void raisePriorityScheduled(final PrioritisedExecutor.Priority priority);
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java
new file mode 100644
index 0000000000000000000000000000000000000000..310a8f80debadd64c2d962ebf83b7d0505ce6e42
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java
@@ -0,0 +1,1457 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask;
+import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.shorts.Short2ByteLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.shorts.Short2ByteMap;
+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.locks.LockSupport;
+
+public abstract class ThreadedTicketLevelPropagator {
+
+ // sections are 64 in length
+ public static final int SECTION_SHIFT = 6;
+ public static final int SECTION_SIZE = 1 << SECTION_SHIFT;
+ private static final int LEVEL_BITS = SECTION_SHIFT;
+ private static final int LEVEL_COUNT = 1 << LEVEL_BITS;
+ private static final int MIN_SOURCE_LEVEL = 1;
+ // we limit the max source to 62 because the de-propagation code _must_ attempt to de-propagate
+ // a 1 level to 0; and if a source was 63 then it may cross more than 2 sections in de-propagation
+ private static final int MAX_SOURCE_LEVEL = 62;
+
+ private static int getMaxSchedulingRadius() {
+ return 2 * ChunkTaskScheduler.getMaxAccessRadius();
+ }
+
+ private final UpdateQueue updateQueue;
+ private final ConcurrentLong2ReferenceChainedHashTable<Section> sections;
+
+ public ThreadedTicketLevelPropagator() {
+ this.updateQueue = new UpdateQueue();
+ this.sections = new ConcurrentLong2ReferenceChainedHashTable<>();
+ }
+
+ // must hold ticket lock for:
+ // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1))
+ public void setSource(final int posX, final int posZ, final int to) {
+ if (to < 1 || to > MAX_SOURCE_LEVEL) {
+ throw new IllegalArgumentException("Source: " + to);
+ }
+
+ final int sectionX = posX >> SECTION_SHIFT;
+ final int sectionZ = posZ >> SECTION_SHIFT;
+
+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ Section section = this.sections.get(coordinate);
+ if (section == null) {
+ if (null != this.sections.putIfAbsent(coordinate, section = new Section(sectionX, sectionZ))) {
+ throw new IllegalStateException("Race condition while creating new section");
+ }
+ }
+
+ final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
+ final short sLocalIdx = (short)localIdx;
+
+ final short sourceAndLevel = section.levels[localIdx];
+ final int currentSource = (sourceAndLevel >>> 8) & 0xFF;
+
+ if (currentSource == to) {
+ // nothing to do
+ // make sure to kill the current update, if any
+ section.queuedSources.replace(sLocalIdx, (byte)to);
+ return;
+ }
+
+ if (section.queuedSources.put(sLocalIdx, (byte)to) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) {
+ this.queueSectionUpdate(section);
+ }
+ }
+
+ // must hold ticket lock for:
+ // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1))
+ public void removeSource(final int posX, final int posZ) {
+ final int sectionX = posX >> SECTION_SHIFT;
+ final int sectionZ = posZ >> SECTION_SHIFT;
+
+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ final Section section = this.sections.get(coordinate);
+
+ if (section == null) {
+ return;
+ }
+
+ final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
+ final short sLocalIdx = (short)localIdx;
+
+ final int currentSource = (section.levels[localIdx] >>> 8) & 0xFF;
+
+ if (currentSource == 0) {
+ // we use replace here so that we do not possibly multi-queue a section for an update
+ section.queuedSources.replace(sLocalIdx, (byte)0);
+ return;
+ }
+
+ if (section.queuedSources.put(sLocalIdx, (byte)0) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) {
+ this.queueSectionUpdate(section);
+ }
+ }
+
+ private void queueSectionUpdate(final Section section) {
+ this.updateQueue.append(new UpdateQueue.UpdateQueueNode(section, null));
+ }
+
+ public boolean hasPendingUpdates() {
+ return !this.updateQueue.isEmpty();
+ }
+
+ // holds ticket lock for every chunk section represented by any position in the key set
+ // updates is modifiable and passed to processSchedulingUpdates after this call
+ protected abstract void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates);
+
+ // holds ticket lock for every chunk section represented by any position in the key set
+ // holds scheduling lock in max access radius for every position held by the ticket lock
+ // updates is cleared after this call
+ protected abstract void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List<ChunkProgressionTask> scheduledTasks,
+ final List<NewChunkHolder> changedFullStatus);
+
+ // must hold ticket lock for every position in the sections in one radius around sectionX,sectionZ
+ public boolean performUpdate(final int sectionX, final int sectionZ, final ReentrantAreaLock schedulingLock,
+ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) {
+ if (!this.hasPendingUpdates()) {
+ return false;
+ }
+
+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ final Section section = this.sections.get(coordinate);
+
+ if (section == null || section.queuedSources.isEmpty()) {
+ // no section or no updates
+ return false;
+ }
+
+ final Propagator propagator = Propagator.acquirePropagator();
+ final boolean ret = this.performUpdate(section, null, propagator,
+ null, schedulingLock, scheduledTasks, changedFullStatus
+ );
+ Propagator.returnPropagator(propagator);
+ return ret;
+ }
+
+ private boolean performUpdate(final Section section, final UpdateQueue.UpdateQueueNode node, final Propagator propagator,
+ final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock,
+ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) {
+ final int sectionX = section.sectionX;
+ final int sectionZ = section.sectionZ;
+
+ final int rad1MinX = (sectionX - 1) << SECTION_SHIFT;
+ final int rad1MinZ = (sectionZ - 1) << SECTION_SHIFT;
+ final int rad1MaxX = ((sectionX + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1);
+ final int rad1MaxZ = ((sectionZ + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1);
+
+ // set up encode offset first as we need to queue level changes _before_
+ propagator.setupEncodeOffset(sectionX, sectionZ);
+
+ final int coordinateOffset = propagator.coordinateOffset;
+
+ final ReentrantAreaLock.Node ticketNode = ticketLock == null ? null : ticketLock.lock(rad1MinX, rad1MinZ, rad1MaxX, rad1MaxZ);
+ final boolean ret;
+ try {
+ // first, check if this update was stolen
+ if (section != this.sections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ))) {
+ // occurs when a stolen update deletes this section
+ // it is possible that another update is scheduled, but that one will have the correct section
+ if (node != null) {
+ this.updateQueue.remove(node);
+ }
+ return false;
+ }
+
+ final int oldSourceSize = section.sources.size();
+
+ // process pending sources
+ for (final Iterator<Short2ByteMap.Entry> iterator = section.queuedSources.short2ByteEntrySet().fastIterator(); iterator.hasNext();) {
+ final Short2ByteMap.Entry entry = iterator.next();
+ final int pos = (int)entry.getShortKey();
+ final int posX = (pos & (SECTION_SIZE - 1)) | (sectionX << SECTION_SHIFT);
+ final int posZ = ((pos >> SECTION_SHIFT) & (SECTION_SIZE - 1)) | (sectionZ << SECTION_SHIFT);
+ final int newSource = (int)entry.getByteValue();
+
+ final short currentEncoded = section.levels[pos];
+ final int currLevel = currentEncoded & 0xFF;
+ final int prevSource = (currentEncoded >>> 8) & 0xFF;
+
+ if (prevSource == newSource) {
+ // nothing changed
+ continue;
+ }
+
+ if ((prevSource < currLevel && newSource <= currLevel) || newSource == currLevel) {
+ // just update the source, don't need to propagate change
+ section.levels[pos] = (short)(currLevel | (newSource << 8));
+ // level is unchanged, don't add to changed positions
+ } else {
+ // set current level and current source to new source
+ section.levels[pos] = (short)(newSource | (newSource << 8));
+ // must add to updated positions in case this is final
+ propagator.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)newSource);
+ if (newSource != 0) {
+ // queue increase with new source level
+ propagator.appendToIncreaseQueue(
+ ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) |
+ ((newSource & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) |
+ (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS))
+ );
+ }
+ // queue decrease with previous level
+ if (newSource < currLevel) {
+ propagator.appendToDecreaseQueue(
+ ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) |
+ ((currLevel & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) |
+ (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS))
+ );
+ }
+ }
+
+ if (newSource == 0) {
+ // prevSource != newSource, so we are removing this source
+ section.sources.remove((short)pos);
+ } else if (prevSource == 0) {
+ // prevSource != newSource, so we are adding this source
+ section.sources.add((short)pos);
+ }
+ }
+
+ section.queuedSources.clear();
+
+ final int newSourceSize = section.sources.size();
+
+ if (oldSourceSize == 0 && newSourceSize != 0) {
+ // need to make sure the sections in 1 radius are initialised
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ if ((dx | dz) == 0) {
+ continue;
+ }
+ final int offX = dx + sectionX;
+ final int offZ = dz + sectionZ;
+ final long coordinate = CoordinateUtils.getChunkKey(offX, offZ);
+ final Section neighbour = this.sections.computeIfAbsent(coordinate, (final long keyInMap) -> {
+ return new Section(CoordinateUtils.getChunkX(keyInMap), CoordinateUtils.getChunkZ(keyInMap));
+ });
+
+ // increase ref count
+ ++neighbour.oneRadNeighboursWithSources;
+ if (neighbour.oneRadNeighboursWithSources <= 0 || neighbour.oneRadNeighboursWithSources > 8) {
+ throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources));
+ }
+ }
+ }
+ }
+
+ if (propagator.hasUpdates()) {
+ propagator.setupCaches(this, sectionX, sectionZ, 1);
+ propagator.performDecrease();
+ // don't need try-finally, as any exception will cause the propagator to not be returned
+ propagator.destroyCaches();
+ }
+
+ if (newSourceSize == 0) {
+ final boolean decrementRef = oldSourceSize != 0;
+ // check for section de-init
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ final int offX = dx + sectionX;
+ final int offZ = dz + sectionZ;
+ final long coordinate = CoordinateUtils.getChunkKey(offX, offZ);
+ final Section neighbour = this.sections.get(coordinate);
+
+ if (neighbour == null) {
+ if (oldSourceSize == 0 && (dx | dz) != 0) {
+ // since we don't have sources, this section is allowed to be null
+ continue;
+ }
+ throw new IllegalStateException("??");
+ }
+
+ if (decrementRef && (dx | dz) != 0) {
+ // decrease ref count, but only for neighbours
+ --neighbour.oneRadNeighboursWithSources;
+ }
+
+ // we need to check the current section for de-init as well
+ if (neighbour.oneRadNeighboursWithSources == 0) {
+ if (neighbour.queuedSources.isEmpty() && neighbour.sources.isEmpty()) {
+ // need to de-init
+ this.sections.remove(coordinate);
+ } // else: neighbour is queued for an update, and it will de-init itself
+ } else if (neighbour.oneRadNeighboursWithSources < 0 || neighbour.oneRadNeighboursWithSources > 8) {
+ throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources));
+ }
+ }
+ }
+ }
+
+
+ ret = !propagator.updatedPositions.isEmpty();
+
+ if (ret) {
+ this.processLevelUpdates(propagator.updatedPositions);
+
+ if (!propagator.updatedPositions.isEmpty()) {
+ // now we can actually update the ticket levels in the chunk holders
+ final int maxScheduleRadius = getMaxSchedulingRadius();
+
+ // allow the chunkholders to process ticket level updates without needing to acquire the schedule lock every time
+ final ReentrantAreaLock.Node schedulingNode = schedulingLock.lock(
+ rad1MinX - maxScheduleRadius, rad1MinZ - maxScheduleRadius,
+ rad1MaxX + maxScheduleRadius, rad1MaxZ + maxScheduleRadius
+ );
+ try {
+ this.processSchedulingUpdates(propagator.updatedPositions, scheduledTasks, changedFullStatus);
+ } finally {
+ schedulingLock.unlock(schedulingNode);
+ }
+ }
+
+ propagator.updatedPositions.clear();
+ }
+ } finally {
+ if (ticketLock != null) {
+ ticketLock.unlock(ticketNode);
+ }
+ }
+
+ // finished
+ if (node != null) {
+ this.updateQueue.remove(node);
+ }
+
+ return ret;
+ }
+
+ public boolean performUpdates(final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock,
+ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) {
+ if (this.updateQueue.isEmpty()) {
+ return false;
+ }
+
+ final long maxOrder = this.updateQueue.getLastOrder();
+
+ boolean updated = false;
+ Propagator propagator = null;
+
+ for (;;) {
+ final UpdateQueue.UpdateQueueNode toUpdate = this.updateQueue.acquireNextOrWait(maxOrder);
+ if (toUpdate == null) {
+ if (!this.updateQueue.hasRemainingUpdates(maxOrder)) {
+ if (propagator != null) {
+ Propagator.returnPropagator(propagator);
+ }
+ return updated;
+ }
+
+ continue;
+ }
+
+ if (propagator == null) {
+ propagator = Propagator.acquirePropagator();
+ }
+
+ updated |= this.performUpdate(toUpdate.section, toUpdate, propagator, ticketLock, schedulingLock, scheduledTasks, changedFullStatus);
+ }
+ }
+
+ // Similar implementation of concurrent FIFO queue (See MTQ in ConcurrentUtil) which has an additional node pointer
+ // for the last update node being handled
+ private static final class UpdateQueue {
+
+ private volatile UpdateQueueNode head;
+ private volatile UpdateQueueNode tail;
+
+ private static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "head", UpdateQueueNode.class);
+ private static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "tail", UpdateQueueNode.class);
+
+ /* head */
+
+ private final void setHeadPlain(final UpdateQueueNode newHead) {
+ HEAD_HANDLE.set(this, newHead);
+ }
+
+ private final void setHeadOpaque(final UpdateQueueNode newHead) {
+ HEAD_HANDLE.setOpaque(this, newHead);
+ }
+
+ private final UpdateQueueNode getHeadPlain() {
+ return (UpdateQueueNode)HEAD_HANDLE.get(this);
+ }
+
+ private final UpdateQueueNode getHeadOpaque() {
+ return (UpdateQueueNode)HEAD_HANDLE.getOpaque(this);
+ }
+
+ private final UpdateQueueNode getHeadAcquire() {
+ return (UpdateQueueNode)HEAD_HANDLE.getAcquire(this);
+ }
+
+ /* tail */
+
+ private final void setTailPlain(final UpdateQueueNode newTail) {
+ TAIL_HANDLE.set(this, newTail);
+ }
+
+ private final void setTailOpaque(final UpdateQueueNode newTail) {
+ TAIL_HANDLE.setOpaque(this, newTail);
+ }
+
+ private final UpdateQueueNode getTailPlain() {
+ return (UpdateQueueNode)TAIL_HANDLE.get(this);
+ }
+
+ private final UpdateQueueNode getTailOpaque() {
+ return (UpdateQueueNode)TAIL_HANDLE.getOpaque(this);
+ }
+
+ public UpdateQueue() {
+ final UpdateQueueNode dummy = new UpdateQueueNode(null, null);
+ dummy.order = -1L;
+ dummy.preventAdds();
+
+ this.setHeadPlain(dummy);
+ this.setTailPlain(dummy);
+ }
+
+ public boolean isEmpty() {
+ return this.peek() == null;
+ }
+
+ public boolean hasRemainingUpdates(final long maxUpdate) {
+ final UpdateQueueNode node = this.peek();
+ return node != null && node.order <= maxUpdate;
+ }
+
+ public long getLastOrder() {
+ for (UpdateQueueNode tail = this.getTailOpaque(), curr = tail;;) {
+ final UpdateQueueNode next = curr.getNextVolatile();
+ if (next == null) {
+ // try to update stale tail
+ if (this.getTailOpaque() == tail && curr != tail) {
+ this.setTailOpaque(curr);
+ }
+ return curr.order;
+ }
+ curr = next;
+ }
+ }
+
+ private static void await(final UpdateQueueNode node) {
+ final Thread currThread = Thread.currentThread();
+ // we do not use add-blocking because we use the nullability of the section to block
+ // remove() does not begin to poll from the wait queue until the section is null'd,
+ // and so provided we check the nullability before parking there is no ordering of these operations
+ // such that remove() finishes polling from the wait queue while section is not null
+ node.add(currThread);
+
+ // wait until completed
+ while (node.getSectionVolatile() != null) {
+ LockSupport.park();
+ }
+ }
+
+ public UpdateQueueNode acquireNextOrWait(final long maxOrder) {
+ final List<UpdateQueueNode> blocking = new ArrayList<>();
+
+ node_search:
+ for (UpdateQueueNode curr = this.peek(); curr != null && curr.order <= maxOrder; curr = curr.getNextVolatile()) {
+ if (curr.getSectionVolatile() == null) {
+ continue;
+ }
+
+ if (curr.getUpdatingVolatile()) {
+ blocking.add(curr);
+ continue;
+ }
+
+ for (int i = 0, len = blocking.size(); i < len; ++i) {
+ final UpdateQueueNode node = blocking.get(i);
+
+ if (node.intersects(curr)) {
+ continue node_search;
+ }
+ }
+
+ if (curr.getAndSetUpdatingVolatile(true)) {
+ blocking.add(curr);
+ continue;
+ }
+
+ return curr;
+ }
+
+ if (!blocking.isEmpty()) {
+ await(blocking.get(0));
+ }
+
+ return null;
+ }
+
+ public UpdateQueueNode peek() {
+ for (UpdateQueueNode head = this.getHeadOpaque(), curr = head;;) {
+ final UpdateQueueNode next = curr.getNextVolatile();
+ final Section element = curr.getSectionVolatile(); /* Likely in sync */
+
+ if (element != null) {
+ if (this.getHeadOpaque() == head && curr != head) {
+ this.setHeadOpaque(curr);
+ }
+ return curr;
+ }
+
+ if (next == null) {
+ if (this.getHeadOpaque() == head && curr != head) {
+ this.setHeadOpaque(curr);
+ }
+ return null;
+ }
+ curr = next;
+ }
+ }
+
+ public void remove(final UpdateQueueNode node) {
+ // mark as removed
+ node.setSectionVolatile(null);
+
+ // use peek to advance head
+ this.peek();
+
+ // unpark any waiters / block the wait queue
+ Thread unpark;
+ while ((unpark = node.poll()) != null) {
+ LockSupport.unpark(unpark);
+ }
+ }
+
+ public void append(final UpdateQueueNode node) {
+ int failures = 0;
+
+ for (UpdateQueueNode currTail = this.getTailOpaque(), curr = currTail;;) {
+ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
+ /* It is likely due to a cache miss caused by another write to the next field */
+ final UpdateQueueNode next = curr.getNextVolatile();
+
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (next == null) {
+ node.order = curr.order + 1L;
+ final UpdateQueueNode compared = curr.compareExchangeNextVolatile(null, node);
+
+ if (compared == null) {
+ /* Added */
+ /* Avoid CASing on tail more than we need to */
+ /* CAS to avoid setting an out-of-date tail */
+ if (this.getTailOpaque() == currTail) {
+ this.setTailOpaque(node);
+ }
+ return;
+ }
+
+ ++failures;
+ curr = compared;
+ continue;
+ }
+
+ if (curr == currTail) {
+ /* Tail is likely not up-to-date */
+ curr = next;
+ } else {
+ /* Try to update to tail */
+ if (currTail == (currTail = this.getTailOpaque())) {
+ curr = next;
+ } else {
+ curr = currTail;
+ }
+ }
+ }
+ }
+
+ // each node also represents a set of waiters, represented by the MTQ
+ // if the queue is add-blocked, then the update is complete
+ private static final class UpdateQueueNode extends MultiThreadedQueue<Thread> {
+ private final int sectionX;
+ private final int sectionZ;
+
+ private long order;
+ private volatile Section section;
+ private volatile UpdateQueueNode next;
+ private volatile boolean updating;
+
+ private static final VarHandle SECTION_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "section", Section.class);
+ private static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "next", UpdateQueueNode.class);
+ private static final VarHandle UPDATING_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "updating", boolean.class);
+
+ public UpdateQueueNode(final Section section, final UpdateQueueNode next) {
+ if (section == null) {
+ this.sectionX = this.sectionZ = 0;
+ } else {
+ this.sectionX = section.sectionX;
+ this.sectionZ = section.sectionZ;
+ }
+
+ SECTION_HANDLE.set(this, section);
+ NEXT_HANDLE.set(this, next);
+ }
+
+ public boolean intersects(final UpdateQueueNode other) {
+ final int dist = Math.max(Math.abs(this.sectionX - other.sectionX), Math.abs(this.sectionZ - other.sectionZ));
+
+ // intersection radius is ticket update radius (1) + scheduling radius
+ return dist <= (1 + ((getMaxSchedulingRadius() + (SECTION_SIZE - 1)) >> SECTION_SHIFT));
+ }
+
+ /* section */
+
+ private final Section getSectionPlain() {
+ return (Section)SECTION_HANDLE.get(this);
+ }
+
+ private final Section getSectionVolatile() {
+ return (Section)SECTION_HANDLE.getVolatile(this);
+ }
+
+ private final void setSectionPlain(final Section update) {
+ SECTION_HANDLE.set(this, update);
+ }
+
+ private final void setSectionOpaque(final Section update) {
+ SECTION_HANDLE.setOpaque(this, update);
+ }
+
+ private final void setSectionVolatile(final Section update) {
+ SECTION_HANDLE.setVolatile(this, update);
+ }
+
+ private final Section getAndSetSectionVolatile(final Section update) {
+ return (Section)SECTION_HANDLE.getAndSet(this, update);
+ }
+
+ private final Section compareExchangeSectionVolatile(final Section expect, final Section update) {
+ return (Section)SECTION_HANDLE.compareAndExchange(this, expect, update);
+ }
+
+ /* next */
+
+ private final UpdateQueueNode getNextPlain() {
+ return (UpdateQueueNode)NEXT_HANDLE.get(this);
+ }
+
+ private final UpdateQueueNode getNextOpaque() {
+ return (UpdateQueueNode)NEXT_HANDLE.getOpaque(this);
+ }
+
+ private final UpdateQueueNode getNextAcquire() {
+ return (UpdateQueueNode)NEXT_HANDLE.getAcquire(this);
+ }
+
+ private final UpdateQueueNode getNextVolatile() {
+ return (UpdateQueueNode)NEXT_HANDLE.getVolatile(this);
+ }
+
+ private final void setNextPlain(final UpdateQueueNode next) {
+ NEXT_HANDLE.set(this, next);
+ }
+
+ private final void setNextVolatile(final UpdateQueueNode next) {
+ NEXT_HANDLE.setVolatile(this, next);
+ }
+
+ private final UpdateQueueNode compareExchangeNextVolatile(final UpdateQueueNode expect, final UpdateQueueNode set) {
+ return (UpdateQueueNode)NEXT_HANDLE.compareAndExchange(this, expect, set);
+ }
+
+ /* updating */
+
+ private final boolean getUpdatingVolatile() {
+ return (boolean)UPDATING_HANDLE.getVolatile(this);
+ }
+
+ private final boolean getAndSetUpdatingVolatile(final boolean value) {
+ return (boolean)UPDATING_HANDLE.getAndSet(this, value);
+ }
+ }
+ }
+
+ private static final class Section {
+
+ // upper 8 bits: sources, lower 8 bits: level
+ // if we REALLY wanted to get crazy, we could make the increase propagator use MethodHandles#byteArrayViewVarHandle
+ // to read and write the lower 8 bits of this array directly rather than reading, updating the bits, then writing back.
+ private final short[] levels = new short[SECTION_SIZE * SECTION_SIZE];
+ // set of local positions that represent sources
+ private final ShortOpenHashSet sources = new ShortOpenHashSet();
+ // map of local index to new source level
+ // the source level _cannot_ be updated in the backing storage immediately since the update
+ private static final byte NO_QUEUED_UPDATE = (byte)-1;
+ private final Short2ByteLinkedOpenHashMap queuedSources = new Short2ByteLinkedOpenHashMap();
+ {
+ this.queuedSources.defaultReturnValue(NO_QUEUED_UPDATE);
+ }
+ private int oneRadNeighboursWithSources = 0;
+
+ public final int sectionX;
+ public final int sectionZ;
+
+ public Section(final int sectionX, final int sectionZ) {
+ this.sectionX = sectionX;
+ this.sectionZ = sectionZ;
+ }
+
+ public boolean isZero() {
+ for (final short val : this.levels) {
+ if (val != 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder ret = new StringBuilder();
+
+ for (int x = 0; x < SECTION_SIZE; ++x) {
+ ret.append("levels x=").append(x).append("\n");
+ for (int z = 0; z < SECTION_SIZE; ++z) {
+ final short v = this.levels[x | (z << SECTION_SHIFT)];
+ ret.append(v & 0xFF).append(".");
+ }
+ ret.append("\n");
+ ret.append("sources x=").append(x).append("\n");
+ for (int z = 0; z < SECTION_SIZE; ++z) {
+ final short v = this.levels[x | (z << SECTION_SHIFT)];
+ ret.append((v >>> 8) & 0xFF).append(".");
+ }
+ ret.append("\n\n");
+ }
+
+ return ret.toString();
+ }
+ }
+
+
+ private static final class Propagator {
+
+ private static final ArrayDeque<Propagator> CACHED_PROPAGATORS = new ArrayDeque<>();
+ private static final int MAX_PROPAGATORS = Runtime.getRuntime().availableProcessors() * 2;
+
+ private static Propagator acquirePropagator() {
+ synchronized (CACHED_PROPAGATORS) {
+ final Propagator ret = CACHED_PROPAGATORS.pollFirst();
+ if (ret != null) {
+ return ret;
+ }
+ }
+ return new Propagator();
+ }
+
+ private static void returnPropagator(final Propagator propagator) {
+ synchronized (CACHED_PROPAGATORS) {
+ if (CACHED_PROPAGATORS.size() < MAX_PROPAGATORS) {
+ CACHED_PROPAGATORS.add(propagator);
+ }
+ }
+ }
+
+ private static final int SECTION_RADIUS = 2;
+ private static final int SECTION_CACHE_WIDTH = 2 * SECTION_RADIUS + 1;
+ // minimum number of bits to represent [0, SECTION_SIZE * SECTION_CACHE_WIDTH)
+ private static final int COORDINATE_BITS = 9;
+ private static final int COORDINATE_SIZE = 1 << COORDINATE_BITS;
+ static {
+ if ((SECTION_SIZE * SECTION_CACHE_WIDTH) > (1 << COORDINATE_BITS)) {
+ throw new IllegalStateException("Adjust COORDINATE_BITS");
+ }
+ }
+ // index = x + (z * SECTION_CACHE_WIDTH)
+ // (this requires x >= 0 and z >= 0)
+ private final Section[] sections = new Section[SECTION_CACHE_WIDTH * SECTION_CACHE_WIDTH];
+
+ private int encodeOffsetX;
+ private int encodeOffsetZ;
+
+ private int coordinateOffset;
+
+ private int encodeSectionOffsetX;
+ private int encodeSectionOffsetZ;
+
+ private int sectionIndexOffset;
+
+ public final boolean hasUpdates() {
+ return this.decreaseQueueInitialLength != 0 || this.increaseQueueInitialLength != 0;
+ }
+
+ private final void setupEncodeOffset(final int centerSectionX, final int centerSectionZ) {
+ final int maxCoordinate = (SECTION_RADIUS * SECTION_SIZE - 1);
+ // must have that encoded >= 0
+ // coordinates can range from [-maxCoordinate + centerSection*SECTION_SIZE, maxCoordinate + centerSection*SECTION_SIZE]
+ // we want a range of [0, maxCoordinate*2]
+ // so, 0 = -maxCoordinate + centerSection*SECTION_SIZE + offset
+ this.encodeOffsetX = maxCoordinate - (centerSectionX << SECTION_SHIFT);
+ this.encodeOffsetZ = maxCoordinate - (centerSectionZ << SECTION_SHIFT);
+
+ // encoded coordinates range from [0, SECTION_SIZE * SECTION_CACHE_WIDTH)
+ // coordinate index = (x + encodeOffsetX) + ((z + encodeOffsetZ) << COORDINATE_BITS)
+ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << COORDINATE_BITS);
+
+ // need encoded values to be >= 0
+ // so, 0 = (-SECTION_RADIUS + centerSectionX) + encodeOffset
+ this.encodeSectionOffsetX = SECTION_RADIUS - centerSectionX;
+ this.encodeSectionOffsetZ = SECTION_RADIUS - centerSectionZ;
+
+ // section index = (secX + encodeSectionOffsetX) + ((secZ + encodeSectionOffsetZ) * SECTION_CACHE_WIDTH)
+ this.sectionIndexOffset = this.encodeSectionOffsetX + (this.encodeSectionOffsetZ * SECTION_CACHE_WIDTH);
+ }
+
+ // must hold ticket lock for (centerSectionX,centerSectionZ) in radius rad
+ // must call setupEncodeOffset
+ private final void setupCaches(final ThreadedTicketLevelPropagator propagator,
+ final int centerSectionX, final int centerSectionZ,
+ final int rad) {
+ for (int dz = -rad; dz <= rad; ++dz) {
+ for (int dx = -rad; dx <= rad; ++dx) {
+ final int sectionX = centerSectionX + dx;
+ final int sectionZ = centerSectionZ + dz;
+ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ);
+ final Section section = propagator.sections.get(coordinate);
+
+ if (section == null) {
+ throw new IllegalStateException("Section at " + coordinate + " should not be null");
+ }
+
+ this.setSectionInCache(sectionX, sectionZ, section);
+ }
+ }
+ }
+
+ private final void setSectionInCache(final int sectionX, final int sectionZ, final Section section) {
+ this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset] = section;
+ }
+
+ private final Section getSection(final int sectionX, final int sectionZ) {
+ return this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset];
+ }
+
+ private final int getLevel(final int posX, final int posZ) {
+ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset];
+ if (section != null) {
+ return (int)section.levels[(posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT)] & 0xFF;
+ }
+
+ return 0;
+ }
+
+ private final void setLevel(final int posX, final int posZ, final int to) {
+ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset];
+ if (section != null) {
+ final int index = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
+ final short level = section.levels[index];
+ section.levels[index] = (short)((level & ~0xFF) | (to & 0xFF));
+ this.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)to);
+ }
+ }
+
+ private final void destroyCaches() {
+ Arrays.fill(this.sections, null);
+ }
+
+ // contains:
+ // lower (COORDINATE_BITS(9) + COORDINATE_BITS(9) = 18) bits encoded position: (x | (z << COORDINATE_BITS))
+ // next LEVEL_BITS (6) bits: propagated level [0, 63]
+ // propagation directions bitset (16 bits):
+ private static final long ALL_DIRECTIONS_BITSET = (
+ // z = -1
+ (1L << ((1 - 1) | ((1 - 1) << 2))) |
+ (1L << ((1 + 0) | ((1 - 1) << 2))) |
+ (1L << ((1 + 1) | ((1 - 1) << 2))) |
+
+ // z = 0
+ (1L << ((1 - 1) | ((1 + 0) << 2))) |
+ //(1L << ((1 + 0) | ((1 + 0) << 2))) | // exclude (0,0)
+ (1L << ((1 + 1) | ((1 + 0) << 2))) |
+
+ // z = 1
+ (1L << ((1 - 1) | ((1 + 1) << 2))) |
+ (1L << ((1 + 0) | ((1 + 1) << 2))) |
+ (1L << ((1 + 1) | ((1 + 1) << 2)))
+ );
+
+ private void ex(int bitset) {
+ for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) {
+ final int set = Integer.numberOfTrailingZeros(bitset);
+ final int tailingBit = (-bitset) & bitset;
+ // XOR to remove the trailing bit
+ bitset ^= tailingBit;
+
+ // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits
+ // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the
+ // index of the set bit is the encoded value
+ // the encoded coordinate has 3 valid states:
+ // 0b00 (0) -> -1
+ // 0b01 (1) -> 0
+ // 0b10 (2) -> 1
+ // the decode operation then is val - 1, and the encode operation is val + 1
+ final int xOff = (set & 3) - 1;
+ final int zOff = ((set >>> 2) & 3) - 1;
+ System.out.println("Encoded: (" + xOff + "," + zOff + ")");
+ }
+ }
+
+ private void ch(long bs, int shift) {
+ int bitset = (int)(bs >>> shift);
+ for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) {
+ final int set = Integer.numberOfTrailingZeros(bitset);
+ final int tailingBit = (-bitset) & bitset;
+ // XOR to remove the trailing bit
+ bitset ^= tailingBit;
+
+ // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits
+ // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the
+ // index of the set bit is the encoded value
+ // the encoded coordinate has 3 valid states:
+ // 0b00 (0) -> -1
+ // 0b01 (1) -> 0
+ // 0b10 (2) -> 1
+ // the decode operation then is val - 1, and the encode operation is val + 1
+ final int xOff = (set & 3) - 1;
+ final int zOff = ((set >>> 2) & 3) - 1;
+ if (Math.abs(xOff) > 1 || Math.abs(zOff) > 1 || (xOff | zOff) == 0) {
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading
+ // updates for sources
+ private static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 1;
+ // whether the propagation needs to check if its current level is equal to the expected level
+ // used only in increase propagation
+ private static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 0;
+
+ private long[] increaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2];
+ private int increaseQueueInitialLength;
+ private long[] decreaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2];
+ private int decreaseQueueInitialLength;
+
+ private final Long2ByteLinkedOpenHashMap updatedPositions = new Long2ByteLinkedOpenHashMap();
+
+ private final long[] resizeIncreaseQueue() {
+ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2);
+ }
+
+ private final long[] resizeDecreaseQueue() {
+ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2);
+ }
+
+ private final void appendToIncreaseQueue(final long value) {
+ final int idx = this.increaseQueueInitialLength++;
+ long[] queue = this.increaseQueue;
+ if (idx >= queue.length) {
+ queue = this.resizeIncreaseQueue();
+ queue[idx] = value;
+ return;
+ } else {
+ queue[idx] = value;
+ return;
+ }
+ }
+
+ private final void appendToDecreaseQueue(final long value) {
+ final int idx = this.decreaseQueueInitialLength++;
+ long[] queue = this.decreaseQueue;
+ if (idx >= queue.length) {
+ queue = this.resizeDecreaseQueue();
+ queue[idx] = value;
+ return;
+ } else {
+ queue[idx] = value;
+ return;
+ }
+ }
+
+ private final void performIncrease() {
+ long[] queue = this.increaseQueue;
+ int queueReadIndex = 0;
+ int queueLength = this.increaseQueueInitialLength;
+ this.increaseQueueInitialLength = 0;
+ final int decodeOffsetX = -this.encodeOffsetX;
+ final int decodeOffsetZ = -this.encodeOffsetZ;
+ final int encodeOffset = this.coordinateOffset;
+ final int sectionOffset = this.sectionIndexOffset;
+
+ final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions;
+
+ while (queueReadIndex < queueLength) {
+ final long queueValue = queue[queueReadIndex++];
+
+ final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX;
+ final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ;
+ final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1);
+ // note: the above code requires coordinate bits * 2 < 32
+ // bitset is 16 bits
+ int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1);
+
+ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) {
+ if (this.getLevel(posX, posZ) != propagatedLevel) {
+ // not at the level we expect, so something changed.
+ continue;
+ }
+ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) {
+ // these are used to restore sources after a propagation decrease
+ this.setLevel(posX, posZ, propagatedLevel);
+ }
+
+ // this bitset represents the values that we have not propagated to
+ // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases
+ // significantly reducing the total number of ops
+ // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need
+ // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead
+ // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits)
+ // to make things easy, we use positions [0, 4] in the bitset, with current position being 2
+ // index = x | (z << 3)
+
+ // to start, we eliminate everything 1 radius from the current position as the previous propagator
+ // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius
+ // but the rest not propagated are already handled
+ long currentPropagation = ~(
+ // z = -1
+ (1L << ((2 - 1) | ((2 - 1) << 3))) |
+ (1L << ((2 + 0) | ((2 - 1) << 3))) |
+ (1L << ((2 + 1) | ((2 - 1) << 3))) |
+
+ // z = 0
+ (1L << ((2 - 1) | ((2 + 0) << 3))) |
+ (1L << ((2 + 0) | ((2 + 0) << 3))) |
+ (1L << ((2 + 1) | ((2 + 0) << 3))) |
+
+ // z = 1
+ (1L << ((2 - 1) | ((2 + 1) << 3))) |
+ (1L << ((2 + 0) | ((2 + 1) << 3))) |
+ (1L << ((2 + 1) | ((2 + 1) << 3)))
+ );
+
+ final int toPropagate = propagatedLevel - 1;
+
+ // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting
+ // the bits, the cpu loop predictor should perfectly predict the loop.
+ for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) {
+ final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset);
+ final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset;
+ propagateDirectionBitset ^= tailingBit;
+
+ // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset
+ // it has been split to save some cycles via parallelism
+ final int pDecodeX = (set & 3);
+ final int pDecodeZ = ((set >>> 2) & 3);
+
+ // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX
+ final int offX = (posX - 1) + pDecodeX;
+ final int offZ = (posZ - 1) + pDecodeZ;
+
+ final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset;
+ final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
+
+ // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset
+ // bitset idx = x | (z << 3)
+
+ // read three bits, so we need 7L
+ // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1
+ // nstartidx1 = x rel -1 for z rel -1
+ // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3)
+ // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3)
+ // = pDecodeX | (pDecodeZ << 3) = start
+ final int start = pDecodeX | (pDecodeZ << 3);
+ final long bitsetLine1 = currentPropagation & (7L << (start));
+
+ // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset)
+ final long bitsetLine2 = currentPropagation & (7L << (start + 8));
+
+ // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset)
+ final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8)));
+
+ // remove ("take") lines from bitset
+ currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3);
+
+ // now try to propagate
+ final Section section = this.sections[sectionIndex];
+
+ // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag
+ final short currentStoredLevel = section.levels[localIndex];
+ final int currentLevel = currentStoredLevel & 0xFF;
+
+ if (currentLevel >= toPropagate) {
+ continue; // already at the level we want
+ }
+
+ // update level
+ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF) | (toPropagate & 0xFF));
+ updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)toPropagate);
+
+ // queue next
+ if (toPropagate > 1) {
+ // now combine into one bitset to pass to child
+ // the child bitset is 4x4, so we just shift each line by 4
+ // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value
+ final long childPropagation =
+ ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1
+ ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0
+ ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1
+
+ // don't queue update if toPropagate cannot propagate anything to neighbours
+ // (for increase, propagating 0 to neighbours is useless)
+ if (queueLength >= queue.length) {
+ queue = this.resizeIncreaseQueue();
+ }
+ queue[queueLength++] =
+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) |
+ ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) |
+ childPropagation; //(ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS));
+ continue;
+ }
+ continue;
+ }
+ }
+ }
+
+ private final void performDecrease() {
+ long[] queue = this.decreaseQueue;
+ long[] increaseQueue = this.increaseQueue;
+ int queueReadIndex = 0;
+ int queueLength = this.decreaseQueueInitialLength;
+ this.decreaseQueueInitialLength = 0;
+ int increaseQueueLength = this.increaseQueueInitialLength;
+ final int decodeOffsetX = -this.encodeOffsetX;
+ final int decodeOffsetZ = -this.encodeOffsetZ;
+ final int encodeOffset = this.coordinateOffset;
+ final int sectionOffset = this.sectionIndexOffset;
+
+ final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions;
+
+ while (queueReadIndex < queueLength) {
+ final long queueValue = queue[queueReadIndex++];
+
+ final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX;
+ final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ;
+ final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1);
+ // note: the above code requires coordinate bits * 2 < 32
+ // bitset is 16 bits
+ int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1);
+
+ // this bitset represents the values that we have not propagated to
+ // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases
+ // significantly reducing the total number of ops
+ // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need
+ // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead
+ // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits)
+ // to make things easy, we use positions [0, 4] in the bitset, with current position being 2
+ // index = x | (z << 3)
+
+ // to start, we eliminate everything 1 radius from the current position as the previous propagator
+ // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius
+ // but the rest not propagated are already handled
+ long currentPropagation = ~(
+ // z = -1
+ (1L << ((2 - 1) | ((2 - 1) << 3))) |
+ (1L << ((2 + 0) | ((2 - 1) << 3))) |
+ (1L << ((2 + 1) | ((2 - 1) << 3))) |
+
+ // z = 0
+ (1L << ((2 - 1) | ((2 + 0) << 3))) |
+ (1L << ((2 + 0) | ((2 + 0) << 3))) |
+ (1L << ((2 + 1) | ((2 + 0) << 3))) |
+
+ // z = 1
+ (1L << ((2 - 1) | ((2 + 1) << 3))) |
+ (1L << ((2 + 0) | ((2 + 1) << 3))) |
+ (1L << ((2 + 1) | ((2 + 1) << 3)))
+ );
+
+ final int toPropagate = propagatedLevel - 1;
+
+ // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting
+ // the bits, the cpu loop predictor should perfectly predict the loop.
+ for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) {
+ final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset);
+ final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset;
+ propagateDirectionBitset ^= tailingBit;
+
+
+ // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset
+ // it has been split to save some cycles via parallelism
+ final int pDecodeX = (set & 3);
+ final int pDecodeZ = ((set >>> 2) & 3);
+
+ // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX
+ final int offX = (posX - 1) + pDecodeX;
+ final int offZ = (posZ - 1) + pDecodeZ;
+
+ final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset;
+ final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT);
+
+ // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset
+ // bitset idx = x | (z << 3)
+
+ // read three bits, so we need 7L
+ // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1
+ // nstartidx1 = x rel -1 for z rel -1
+ // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3)
+ // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3)
+ // = pDecodeX | (pDecodeZ << 3) = start
+ final int start = pDecodeX | (pDecodeZ << 3);
+ final long bitsetLine1 = currentPropagation & (7L << (start));
+
+ // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset)
+ final long bitsetLine2 = currentPropagation & (7L << (start + 8));
+
+ // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset)
+ final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8)));
+
+ // now try to propagate
+ final Section section = this.sections[sectionIndex];
+
+ // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag
+ final short currentStoredLevel = section.levels[localIndex];
+ final int currentLevel = currentStoredLevel & 0xFF;
+ final int sourceLevel = (currentStoredLevel >>> 8) & 0xFF;
+
+ if (currentLevel == 0) {
+ continue; // already at the level we want
+ }
+
+ if (currentLevel > toPropagate) {
+ // it looks like another source propagated here, so re-propagate it
+ if (increaseQueueLength >= increaseQueue.length) {
+ increaseQueue = this.resizeIncreaseQueue();
+ }
+ increaseQueue[increaseQueueLength++] =
+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) |
+ ((currentLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) |
+ (FLAG_RECHECK_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)));
+ continue;
+ }
+
+ // remove ("take") lines from bitset
+ // can't do this during decrease, TODO WHY?
+ //currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3);
+
+ // update level
+ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF));
+ updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)0);
+
+ if (sourceLevel != 0) {
+ // re-propagate source
+ // note: do not set recheck level, or else the propagation will fail
+ if (increaseQueueLength >= increaseQueue.length) {
+ increaseQueue = this.resizeIncreaseQueue();
+ }
+ increaseQueue[increaseQueueLength++] =
+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) |
+ ((sourceLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) |
+ (FLAG_WRITE_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)));
+ }
+
+ // queue next
+ // note: targetLevel > 0 here, since toPropagate >= currentLevel and currentLevel > 0
+ // now combine into one bitset to pass to child
+ // the child bitset is 4x4, so we just shift each line by 4
+ // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value
+ final long childPropagation =
+ ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1
+ ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0
+ ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1
+
+ // don't queue update if toPropagate cannot propagate anything to neighbours
+ // (for increase, propagating 0 to neighbours is useless)
+ if (queueLength >= queue.length) {
+ queue = this.resizeDecreaseQueue();
+ }
+ queue[queueLength++] =
+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) |
+ ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) |
+ (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); //childPropagation;
+ continue;
+ }
+ }
+
+ // propagate sources we clobbered
+ this.increaseQueueInitialLength = increaseQueueLength;
+ this.performIncrease();
+ }
+ }
+
+ /*
+ private static final java.util.Random random = new java.util.Random(4L);
+ private static final List<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void>> walkers =
+ new java.util.ArrayList<>();
+ static final int PLAYERS = 0;
+ static final int RAD_BLOCKS = 10000;
+ static final int RAD = RAD_BLOCKS >> 4;
+ static final int RAD_BIG_BLOCKS = 100_000;
+ static final int RAD_BIG = RAD_BIG_BLOCKS >> 4;
+ static final int VD = 4;
+ static final int BIG_PLAYERS = 50;
+ static final double WALK_CHANCE = 0.10;
+ static final double TP_CHANCE = 0.01;
+ static final int TP_BACK_PLAYERS = 200;
+ static final double TP_BACK_CHANCE = 0.25;
+ static final double TP_STEAL_CHANCE = 0.25;
+ private static final List<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void>> tpBack =
+ new java.util.ArrayList<>();
+
+ public static void main(final String[] args) {
+ final ReentrantAreaLock ticketLock = new ReentrantAreaLock(SECTION_SHIFT);
+ final ReentrantAreaLock schedulingLock = new ReentrantAreaLock(SECTION_SHIFT);
+ final Long2ByteLinkedOpenHashMap levelMap = new Long2ByteLinkedOpenHashMap();
+ final Long2ByteLinkedOpenHashMap refMap = new Long2ByteLinkedOpenHashMap();
+ final io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D ref = new io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D((final long coordinate, final byte oldLevel, final byte newLevel) -> {
+ if (newLevel == 0) {
+ refMap.remove(coordinate);
+ } else {
+ refMap.put(coordinate, newLevel);
+ }
+ });
+ final ThreadedTicketLevelPropagator propagator = new ThreadedTicketLevelPropagator() {
+ @Override
+ protected void processLevelUpdates(Long2ByteLinkedOpenHashMap updates) {
+ for (final long key : updates.keySet()) {
+ final byte val = updates.get(key);
+ if (val == 0) {
+ levelMap.remove(key);
+ } else {
+ levelMap.put(key, val);
+ }
+ }
+ }
+
+ @Override
+ protected void processSchedulingUpdates(Long2ByteLinkedOpenHashMap updates, List<ChunkProgressionTask> scheduledTasks, List<NewChunkHolder> changedFullStatus) {}
+ };
+
+ for (;;) {
+ if (walkers.isEmpty() && tpBack.isEmpty()) {
+ for (int i = 0; i < PLAYERS; ++i) {
+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD;
+ int posX = random.nextInt(-rad, rad + 1);
+ int posZ = random.nextInt(-rad, rad + 1);
+
+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) {
+ @Override
+ protected void addCallback(Void parameter, int chunkX, int chunkZ) {
+ int src = 45 - 31 + 1;
+ ref.setSource(chunkX, chunkZ, src);
+ propagator.setSource(chunkX, chunkZ, src);
+ }
+
+ @Override
+ protected void removeCallback(Void parameter, int chunkX, int chunkZ) {
+ ref.removeSource(chunkX, chunkZ);
+ propagator.removeSource(chunkX, chunkZ);
+ }
+ };
+
+ map.add(posX, posZ, VD);
+
+ walkers.add(map);
+ }
+ for (int i = 0; i < TP_BACK_PLAYERS; ++i) {
+ int rad = RAD_BIG;
+ int posX = random.nextInt(-rad, rad + 1);
+ int posZ = random.nextInt(-rad, rad + 1);
+
+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) {
+ @Override
+ protected void addCallback(Void parameter, int chunkX, int chunkZ) {
+ int src = 45 - 31 + 1;
+ ref.setSource(chunkX, chunkZ, src);
+ propagator.setSource(chunkX, chunkZ, src);
+ }
+
+ @Override
+ protected void removeCallback(Void parameter, int chunkX, int chunkZ) {
+ ref.removeSource(chunkX, chunkZ);
+ propagator.removeSource(chunkX, chunkZ);
+ }
+ };
+
+ map.add(posX, posZ, random.nextInt(1, 63));
+
+ tpBack.add(map);
+ }
+ } else {
+ for (int i = 0; i < PLAYERS; ++i) {
+ if (random.nextDouble() > WALK_CHANCE) {
+ continue;
+ }
+
+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = walkers.get(i);
+
+ int updateX = random.nextInt(-1, 2);
+ int updateZ = random.nextInt(-1, 2);
+
+ map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD);
+ }
+
+ for (int i = 0; i < PLAYERS; ++i) {
+ if (random.nextDouble() > TP_CHANCE) {
+ continue;
+ }
+
+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD;
+ int posX = random.nextInt(-rad, rad + 1);
+ int posZ = random.nextInt(-rad, rad + 1);
+
+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = walkers.get(i);
+
+ map.update(posX, posZ, VD);
+ }
+
+ for (int i = 0; i < TP_BACK_PLAYERS; ++i) {
+ if (random.nextDouble() > TP_BACK_CHANCE) {
+ continue;
+ }
+
+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = tpBack.get(i);
+
+ map.update(-map.lastChunkX, -map.lastChunkZ, random.nextInt(1, 63));
+
+ if (random.nextDouble() > TP_STEAL_CHANCE) {
+ propagator.performUpdate(
+ map.lastChunkX >> SECTION_SHIFT, map.lastChunkZ >> SECTION_SHIFT, schedulingLock, null, null
+ );
+ propagator.performUpdate(
+ (-map.lastChunkX >> SECTION_SHIFT), (-map.lastChunkZ >> SECTION_SHIFT), schedulingLock, null, null
+ );
+ }
+ }
+ }
+
+ ref.propagateUpdates();
+ propagator.performUpdates(ticketLock, schedulingLock, null, null);
+
+ if (!refMap.equals(levelMap)) {
+ throw new IllegalStateException("Error!");
+ }
+ }
+ }
+ */
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373ba811ba8b
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
@@ -0,0 +1,668 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.PriorityQueue;
+
+public class RadiusAwarePrioritisedExecutor {
+
+ private static final Comparator<DependencyNode> DEPENDENCY_NODE_COMPARATOR = (final DependencyNode t1, final DependencyNode t2) -> {
+ return Long.compare(t1.id, t2.id);
+ };
+
+ private final DependencyTree[] queues = new DependencyTree[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES];
+ private static final int NO_TASKS_QUEUED = -1;
+ private int selectedQueue = NO_TASKS_QUEUED;
+ private boolean canQueueTasks = true;
+
+ public RadiusAwarePrioritisedExecutor(final PrioritisedExecutor executor, final int maxToSchedule) {
+ for (int i = 0; i < this.queues.length; ++i) {
+ this.queues[i] = new DependencyTree(this, executor, maxToSchedule, i);
+ }
+ }
+
+ private boolean canQueueTasks() {
+ return this.canQueueTasks;
+ }
+
+ private List<PrioritisedExecutor.PrioritisedTask> treeFinished() {
+ this.canQueueTasks = true;
+ for (int priority = 0; priority < this.queues.length; ++priority) {
+ final DependencyTree queue = this.queues[priority];
+ if (queue.hasWaitingTasks()) {
+ final List<PrioritisedExecutor.PrioritisedTask> ret = queue.tryPushTasks();
+
+ if (ret == null || ret.isEmpty()) {
+ // this happens when the tasks in the wait queue were purged
+ // in this case, the queue was actually empty, we just had to purge it
+ // if we set the selected queue without scheduling any tasks, the queue will never be unselected
+ // as that requires a scheduled task completing...
+ continue;
+ }
+
+ this.selectedQueue = priority;
+ return ret;
+ }
+ }
+
+ this.selectedQueue = NO_TASKS_QUEUED;
+
+ return null;
+ }
+
+ private List<PrioritisedExecutor.PrioritisedTask> queue(final Task task, final PrioritisedExecutor.Priority priority) {
+ final int priorityId = priority.priority;
+ final DependencyTree queue = this.queues[priorityId];
+
+ final DependencyNode node = new DependencyNode(task, queue);
+
+ if (task.dependencyNode != null) {
+ throw new IllegalStateException();
+ }
+ task.dependencyNode = node;
+
+ queue.pushNode(node);
+
+ if (this.selectedQueue == NO_TASKS_QUEUED) {
+ this.canQueueTasks = true;
+ this.selectedQueue = priorityId;
+ return queue.tryPushTasks();
+ }
+
+ if (!this.canQueueTasks) {
+ return null;
+ }
+
+ if (PrioritisedExecutor.Priority.isHigherPriority(priorityId, this.selectedQueue)) {
+ // prevent the lower priority tree from queueing more tasks
+ this.canQueueTasks = false;
+ return null;
+ }
+
+ // priorityId != selectedQueue: lower priority, don't care - treeFinished will pick it up
+ return priorityId == this.selectedQueue ? queue.tryPushTasks() : null;
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius,
+ final Runnable run, final PrioritisedExecutor.Priority priority) {
+ if (radius < 0) {
+ throw new IllegalArgumentException("Radius must be > 0: " + radius);
+ }
+ return new Task(this, chunkX, chunkZ, radius, run, priority);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius,
+ final Runnable run) {
+ return this.createTask(chunkX, chunkZ, radius, run, PrioritisedExecutor.Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius,
+ final Runnable run, final PrioritisedExecutor.Priority priority) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run, priority);
+
+ ret.queue();
+
+ return ret;
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius,
+ final Runnable run) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run);
+
+ ret.queue();
+
+ return ret;
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ return new Task(this, 0, 0, -1, run, priority);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run) {
+ return this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, priority);
+
+ ret.queue();
+
+ return ret;
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL);
+
+ ret.queue();
+
+ return ret;
+ }
+
+ // all accesses must be synchronised by the radius aware object
+ private static final class DependencyTree {
+
+ private final RadiusAwarePrioritisedExecutor scheduler;
+ private final PrioritisedExecutor executor;
+ private final int maxToSchedule;
+ private final int treeIndex;
+
+ private int currentlyExecuting;
+ private long idGenerator;
+
+ private final PriorityQueue<DependencyNode> awaiting = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR);
+
+ private final PriorityQueue<DependencyNode> infiniteRadius = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR);
+ private boolean isInfiniteRadiusScheduled;
+
+ private final Long2ReferenceOpenHashMap<DependencyNode> nodeByPosition = new Long2ReferenceOpenHashMap<>();
+
+ public DependencyTree(final RadiusAwarePrioritisedExecutor scheduler, final PrioritisedExecutor executor,
+ final int maxToSchedule, final int treeIndex) {
+ this.scheduler = scheduler;
+ this.executor = executor;
+ this.maxToSchedule = maxToSchedule;
+ this.treeIndex = treeIndex;
+ }
+
+ public boolean hasWaitingTasks() {
+ return !this.awaiting.isEmpty() || !this.infiniteRadius.isEmpty();
+ }
+
+ private long nextId() {
+ return this.idGenerator++;
+ }
+
+ private boolean isExecutingAnyTasks() {
+ return this.currentlyExecuting != 0;
+ }
+
+ private void pushNode(final DependencyNode node) {
+ if (!node.task.isFiniteRadius()) {
+ this.infiniteRadius.add(node);
+ return;
+ }
+
+ // set up dependency for node
+ final Task task = node.task;
+
+ final int centerX = task.chunkX;
+ final int centerZ = task.chunkZ;
+ final int radius = task.radius;
+
+ final int minX = centerX - radius;
+ final int maxX = centerX + radius;
+
+ final int minZ = centerZ - radius;
+ final int maxZ = centerZ + radius;
+
+ ReferenceOpenHashSet<DependencyNode> parents = null;
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ final DependencyNode dependency = this.nodeByPosition.put(CoordinateUtils.getChunkKey(currX, currZ), node);
+ if (dependency != null) {
+ if (parents == null) {
+ parents = new ReferenceOpenHashSet<>();
+ }
+ if (parents.add(dependency)) {
+ // added a dependency, so we need to add as a child to the dependency
+ if (dependency.children == null) {
+ dependency.children = new ArrayList<>();
+ }
+ dependency.children.add(node);
+ }
+ }
+ }
+ }
+
+ if (parents == null) {
+ // no dependencies, add straight to awaiting
+ this.awaiting.add(node);
+ } else {
+ node.parents = parents.size();
+ // we will be added to awaiting once we have no parents
+ }
+ }
+
+ // called only when a node is returned after being executed
+ private List<PrioritisedExecutor.PrioritisedTask> returnNode(final DependencyNode node) {
+ final Task task = node.task;
+
+ // now that the task is completed, we can push its children to the awaiting queue
+ this.pushChildren(node);
+
+ if (task.isFiniteRadius()) {
+ // remove from dependency map
+ this.removeNodeFromMap(node);
+ } else {
+ // mark as no longer executing infinite radius
+ if (!this.isInfiniteRadiusScheduled) {
+ throw new IllegalStateException();
+ }
+ this.isInfiniteRadiusScheduled = false;
+ }
+
+ // decrement executing count, we are done executing this task
+ --this.currentlyExecuting;
+
+ if (this.currentlyExecuting == 0) {
+ return this.scheduler.treeFinished();
+ }
+
+ return this.scheduler.canQueueTasks() ? this.tryPushTasks() : null;
+ }
+
+ private List<PrioritisedExecutor.PrioritisedTask> tryPushTasks() {
+ // tasks are not queued, but only created here - we do hold the lock for the map
+ List<PrioritisedExecutor.PrioritisedTask> ret = null;
+ PrioritisedExecutor.PrioritisedTask pushedTask;
+ while ((pushedTask = this.tryPushTask()) != null) {
+ if (ret == null) {
+ ret = new ArrayList<>();
+ }
+ ret.add(pushedTask);
+ }
+
+ return ret;
+ }
+
+ private void removeNodeFromMap(final DependencyNode node) {
+ final Task task = node.task;
+
+ final int centerX = task.chunkX;
+ final int centerZ = task.chunkZ;
+ final int radius = task.radius;
+
+ final int minX = centerX - radius;
+ final int maxX = centerX + radius;
+
+ final int minZ = centerZ - radius;
+ final int maxZ = centerZ + radius;
+
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ this.nodeByPosition.remove(CoordinateUtils.getChunkKey(currX, currZ), node);
+ }
+ }
+ }
+
+ private void pushChildren(final DependencyNode node) {
+ // add all the children that we can into awaiting
+ final List<DependencyNode> children = node.children;
+ if (children != null) {
+ for (int i = 0, len = children.size(); i < len; ++i) {
+ final DependencyNode child = children.get(i);
+ int newParents = --child.parents;
+ if (newParents == 0) {
+ // no more dependents, we can push to awaiting
+ // even if the child is purged, we need to push it so that its children will be pushed
+ this.awaiting.add(child);
+ } else if (newParents < 0) {
+ throw new IllegalStateException();
+ }
+ }
+ }
+ }
+
+ private DependencyNode pollAwaiting() {
+ final DependencyNode ret = this.awaiting.poll();
+ if (ret == null) {
+ return ret;
+ }
+
+ if (ret.parents != 0) {
+ throw new IllegalStateException();
+ }
+
+ if (ret.purged) {
+ // need to manually remove from state here
+ this.pushChildren(ret);
+ this.removeNodeFromMap(ret);
+ } // else: delay children push until the task has finished
+
+ return ret;
+ }
+
+ private DependencyNode pollInfinite() {
+ return this.infiniteRadius.poll();
+ }
+
+ public PrioritisedExecutor.PrioritisedTask tryPushTask() {
+ if (this.currentlyExecuting >= this.maxToSchedule || this.isInfiniteRadiusScheduled) {
+ return null;
+ }
+
+ DependencyNode firstInfinite;
+ while ((firstInfinite = this.infiniteRadius.peek()) != null && firstInfinite.purged) {
+ this.pollInfinite();
+ }
+
+ DependencyNode firstAwaiting;
+ while ((firstAwaiting = this.awaiting.peek()) != null && firstAwaiting.purged) {
+ this.pollAwaiting();
+ }
+
+ if (firstInfinite == null && firstAwaiting == null) {
+ return null;
+ }
+
+ // firstAwaiting compared to firstInfinite
+ final int compare;
+
+ if (firstAwaiting == null) {
+ // we choose first infinite, or infinite < awaiting
+ compare = 1;
+ } else if (firstInfinite == null) {
+ // we choose first awaiting, or awaiting < infinite
+ compare = -1;
+ } else {
+ compare = DEPENDENCY_NODE_COMPARATOR.compare(firstAwaiting, firstInfinite);
+ }
+
+ if (compare >= 0) {
+ if (this.currentlyExecuting != 0) {
+ // don't queue infinite task while other tasks are executing in parallel
+ return null;
+ }
+ ++this.currentlyExecuting;
+ this.pollInfinite();
+ this.isInfiniteRadiusScheduled = true;
+ return firstInfinite.task.pushTask(this.executor);
+ } else {
+ ++this.currentlyExecuting;
+ this.pollAwaiting();
+ return firstAwaiting.task.pushTask(this.executor);
+ }
+ }
+ }
+
+ private static final class DependencyNode {
+
+ private final Task task;
+ private final DependencyTree tree;
+
+ // dependency tree fields
+ // (must hold lock on the scheduler to use)
+ // null is the same as empty, we just use it so that we don't allocate the set unless we need to
+ private List<DependencyNode> children;
+ // 0 indicates that this task is considered "awaiting"
+ private int parents;
+ // false -> scheduled and not cancelled
+ // true -> scheduled but cancelled
+ private boolean purged;
+ private final long id;
+
+ public DependencyNode(final Task task, final DependencyTree tree) {
+ this.task = task;
+ this.id = tree.nextId();
+ this.tree = tree;
+ }
+ }
+
+ private static final class Task implements PrioritisedExecutor.PrioritisedTask, Runnable {
+
+ // task specific fields
+ private final RadiusAwarePrioritisedExecutor scheduler;
+ private final int chunkX;
+ private final int chunkZ;
+ private final int radius;
+ private Runnable run;
+ private PrioritisedExecutor.Priority priority;
+
+ private DependencyNode dependencyNode;
+ private PrioritisedExecutor.PrioritisedTask queuedTask;
+
+ private Task(final RadiusAwarePrioritisedExecutor scheduler, final int chunkX, final int chunkZ, final int radius,
+ final Runnable run, final PrioritisedExecutor.Priority priority) {
+ this.scheduler = scheduler;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.radius = radius;
+ this.run = run;
+ this.priority = priority;
+ }
+
+ private boolean isFiniteRadius() {
+ return this.radius >= 0;
+ }
+
+ private PrioritisedExecutor.PrioritisedTask pushTask(final PrioritisedExecutor executor) {
+ return this.queuedTask = executor.createTask(this, this.priority);
+ }
+
+ private void executeTask() {
+ final Runnable run = this.run;
+ this.run = null;
+ run.run();
+ }
+
+ private static void scheduleTasks(final List<PrioritisedExecutor.PrioritisedTask> toSchedule) {
+ if (toSchedule != null) {
+ for (int i = 0, len = toSchedule.size(); i < len; ++i) {
+ toSchedule.get(i).queue();
+ }
+ }
+ }
+
+ private void returnNode() {
+ final List<PrioritisedExecutor.PrioritisedTask> toSchedule;
+ synchronized (this.scheduler) {
+ final DependencyNode node = this.dependencyNode;
+ this.dependencyNode = null;
+ toSchedule = node.tree.returnNode(node);
+ }
+
+ scheduleTasks(toSchedule);
+ }
+
+ @Override
+ public void run() {
+ final Runnable run = this.run;
+ this.run = null;
+ try {
+ run.run();
+ } finally {
+ this.returnNode();
+ }
+ }
+
+ @Override
+ public boolean queue() {
+ final List<PrioritisedExecutor.PrioritisedTask> toSchedule;
+ synchronized (this.scheduler) {
+ if (this.queuedTask != null || this.dependencyNode != null || this.priority == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ toSchedule = this.scheduler.queue(this, this.priority);
+ }
+
+ scheduleTasks(toSchedule);
+ return true;
+ }
+
+ @Override
+ public boolean cancel() {
+ final PrioritisedExecutor.PrioritisedTask task;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ this.priority = PrioritisedExecutor.Priority.COMPLETING;
+ if (this.dependencyNode != null) {
+ this.dependencyNode.purged = true;
+ this.dependencyNode = null;
+ }
+
+ return true;
+ }
+ }
+
+ if (task.cancel()) {
+ // must manually return the node
+ this.run = null;
+ this.returnNode();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean execute() {
+ final PrioritisedExecutor.PrioritisedTask task;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ this.priority = PrioritisedExecutor.Priority.COMPLETING;
+ if (this.dependencyNode != null) {
+ this.dependencyNode.purged = true;
+ this.dependencyNode = null;
+ }
+ // fall through to execution logic
+ }
+ }
+
+ if (task != null) {
+ // will run the return node logic automatically
+ return task.execute();
+ } else {
+ // don't run node removal/insertion logic, we aren't actually removed from the dependency tree
+ this.executeTask();
+ return true;
+ }
+ }
+
+ @Override
+ public PrioritisedExecutor.Priority getPriority() {
+ final PrioritisedExecutor.PrioritisedTask task;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
+ return this.priority;
+ }
+ }
+
+ return task.getPriority();
+ }
+
+ @Override
+ public boolean setPriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
+ final PrioritisedExecutor.PrioritisedTask task;
+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ if (this.priority == priority) {
+ return true;
+ }
+
+ this.priority = priority;
+ if (this.dependencyNode != null) {
+ // need to re-insert node
+ this.dependencyNode.purged = true;
+ this.dependencyNode = null;
+ toSchedule = this.scheduler.queue(this, priority);
+ }
+ }
+ }
+
+ if (task != null) {
+ return task.setPriority(priority);
+ }
+
+ scheduleTasks(toSchedule);
+
+ return true;
+ }
+
+ @Override
+ public boolean raisePriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
+ final PrioritisedExecutor.PrioritisedTask task;
+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ if (this.priority.isHigherOrEqualPriority(priority)) {
+ return true;
+ }
+
+ this.priority = priority;
+ if (this.dependencyNode != null) {
+ // need to re-insert node
+ this.dependencyNode.purged = true;
+ this.dependencyNode = null;
+ toSchedule = this.scheduler.queue(this, priority);
+ }
+ }
+ }
+
+ if (task != null) {
+ return task.raisePriority(priority);
+ }
+
+ scheduleTasks(toSchedule);
+
+ return true;
+ }
+
+ @Override
+ public boolean lowerPriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
+ final PrioritisedExecutor.PrioritisedTask task;
+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
+ return false;
+ }
+
+ if (this.priority.isLowerOrEqualPriority(priority)) {
+ return true;
+ }
+
+ this.priority = priority;
+ if (this.dependencyNode != null) {
+ // need to re-insert node
+ this.dependencyNode.purged = true;
+ this.dependencyNode = null;
+ toSchedule = this.scheduler.queue(this, priority);
+ }
+ }
+ }
+
+ if (task != null) {
+ return task.lowerPriority(priority);
+ }
+
+ scheduleTasks(toSchedule);
+
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..49774d42f35eeeac5e2b334cce40e6dcca6d01ed
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
@@ -0,0 +1,139 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
+import net.minecraft.server.level.ChunkMap;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.ImposterProtoChunk;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.ProtoChunk;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.chunk.status.ChunkStatusTasks;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.lang.invoke.VarHandle;
+
+public final class ChunkFullTask extends ChunkProgressionTask implements Runnable {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkFullTask.class);
+
+ private final NewChunkHolder chunkHolder;
+ private final ChunkAccess fromChunk;
+ private final PrioritisedExecutor.PrioritisedTask convertToFullTask;
+
+ public ChunkFullTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ,
+ final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final PrioritisedExecutor.Priority priority) {
+ super(scheduler, world, chunkX, chunkZ);
+ this.chunkHolder = chunkHolder;
+ this.fromChunk = fromChunk;
+ this.convertToFullTask = scheduler.createChunkTask(chunkX, chunkZ, this, priority);
+ }
+
+ @Override
+ public ChunkStatus getTargetStatus() {
+ return ChunkStatus.FULL;
+ }
+
+ @Override
+ public void run() {
+ // See Vanilla ChunkPyramid#LOADING_PYRAMID.FULL for what this function should be doing
+ final LevelChunk chunk;
+ try {
+ // moved from the load from nbt stage into here
+ final PoiChunk poiChunk = this.chunkHolder.getPoiChunk();
+ if (poiChunk == null) {
+ LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString());
+ } else {
+ poiChunk.load();
+ ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$checkConsistency(this.fromChunk);
+ }
+
+ if (this.fromChunk instanceof ImposterProtoChunk wrappedFull) {
+ chunk = wrappedFull.getWrapped();
+ } else {
+ final ServerLevel world = this.world;
+ final ProtoChunk protoChunk = (ProtoChunk)this.fromChunk;
+ chunk = new LevelChunk(this.world, protoChunk, (final LevelChunk unused) -> {
+ ChunkStatusTasks.postLoadProtoChunk(world, protoChunk.getEntities(), protoChunk.getPos()); // Paper - pass chunk pos
+ });
+ this.chunkHolder.replaceProtoChunk(new ImposterProtoChunk(chunk, false));
+ }
+
+ final NewChunkHolder chunkHolder = this.chunkHolder;
+
+ chunk.setFullStatus(chunkHolder::getChunkStatus);
+ chunk.runPostLoad();
+ // Unlike Vanilla, we load the entity chunk here, as we load the NBT in empty status (unlike Vanilla)
+ // This brings entity addition back in line with older versions of the game
+ // Since we load the NBT in the empty status, this will never block for I/O
+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getOrCreateEntityChunk(this.chunkX, this.chunkZ, false);
+
+ // we don't need the entitiesInLevel, not sure why it's there
+ chunk.setLoaded(true);
+ chunk.registerAllBlockEntitiesAfterLevelLoad();
+ chunk.registerTickContainerInLevel(this.world);
+ } catch (final Throwable throwable) {
+ this.complete(null, throwable);
+ return;
+ }
+ this.complete(chunk, null);
+ }
+
+ protected volatile boolean scheduled;
+ protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkFullTask.class, "scheduled", boolean.class);
+
+ @Override
+ public boolean isScheduled() {
+ return this.scheduled;
+ }
+
+ @Override
+ public void schedule() {
+ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkFullTask)this, true)) {
+ throw new IllegalStateException("Cannot double call schedule()");
+ }
+ this.convertToFullTask.queue();
+ }
+
+ @Override
+ public void cancel() {
+ if (this.convertToFullTask.cancel()) {
+ this.complete(null, null);
+ }
+ }
+
+ @Override
+ public PrioritisedExecutor.Priority getPriority() {
+ return this.convertToFullTask.getPriority();
+ }
+
+ @Override
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.convertToFullTask.lowerPriority(priority);
+ }
+
+ @Override
+ public void setPriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.convertToFullTask.setPriority(priority);
+ }
+
+ @Override
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.convertToFullTask.raisePriority(priority);
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..7c2e6752228fac175c4aa97fa3d817b8a938922f
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
@@ -0,0 +1,181 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.PriorityHolder;
+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine;
+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface;
+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.ProtoChunk;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import java.util.function.BooleanSupplier;
+
+public final class ChunkLightTask extends ChunkProgressionTask {
+
+ private static final Logger LOGGER = LogManager.getLogger();
+
+ private final ChunkAccess fromChunk;
+
+ private final LightTaskPriorityHolder priorityHolder;
+
+ public ChunkLightTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ,
+ final ChunkAccess chunk, final PrioritisedExecutor.Priority priority) {
+ super(scheduler, world, chunkX, chunkZ);
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.priorityHolder = new LightTaskPriorityHolder(priority, this);
+ this.fromChunk = chunk;
+ }
+
+ @Override
+ public boolean isScheduled() {
+ return this.priorityHolder.isScheduled();
+ }
+
+ @Override
+ public ChunkStatus getTargetStatus() {
+ return ChunkStatus.LIGHT;
+ }
+
+ @Override
+ public void schedule() {
+ this.priorityHolder.schedule();
+ }
+
+ @Override
+ public void cancel() {
+ this.priorityHolder.cancel();
+ }
+
+ @Override
+ public PrioritisedExecutor.Priority getPriority() {
+ return this.priorityHolder.getPriority();
+ }
+
+ @Override
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ this.priorityHolder.raisePriority(priority);
+ }
+
+ @Override
+ public void setPriority(final PrioritisedExecutor.Priority priority) {
+ this.priorityHolder.setPriority(priority);
+ }
+
+ @Override
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ this.priorityHolder.raisePriority(priority);
+ }
+
+ private static final class LightTaskPriorityHolder extends PriorityHolder {
+
+ private final ChunkLightTask task;
+
+ private LightTaskPriorityHolder(final PrioritisedExecutor.Priority priority, final ChunkLightTask task) {
+ super(priority);
+ this.task = task;
+ }
+
+ @Override
+ protected void cancelScheduled() {
+ final ChunkLightTask task = this.task;
+ task.complete(null, null);
+ }
+
+ @Override
+ protected PrioritisedExecutor.Priority getScheduledPriority() {
+ final ChunkLightTask task = this.task;
+ return ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine().getServerLightQueue().getPriority(task.chunkX, task.chunkZ);
+ }
+
+ @Override
+ protected void scheduleTask(final PrioritisedExecutor.Priority priority) {
+ final ChunkLightTask task = this.task;
+ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
+ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
+ lightQueue.queueChunkLightTask(new ChunkPos(task.chunkX, task.chunkZ), new LightTask(starLightInterface, task), priority);
+ lightQueue.setPriority(task.chunkX, task.chunkZ, priority);
+ }
+
+ @Override
+ protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) {
+ final ChunkLightTask task = this.task;
+ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
+ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
+ lightQueue.lowerPriority(task.chunkX, task.chunkZ, priority);
+ }
+
+ @Override
+ protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) {
+ final ChunkLightTask task = this.task;
+ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
+ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
+ lightQueue.setPriority(task.chunkX, task.chunkZ, priority);
+ }
+
+ @Override
+ protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) {
+ final ChunkLightTask task = this.task;
+ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine();
+ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue();
+ lightQueue.raisePriority(task.chunkX, task.chunkZ, priority);
+ }
+ }
+
+ private static final class LightTask implements BooleanSupplier {
+
+ private final StarLightInterface lightEngine;
+ private final ChunkLightTask task;
+
+ public LightTask(final StarLightInterface lightEngine, final ChunkLightTask task) {
+ this.lightEngine = lightEngine;
+ this.task = task;
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ final ChunkLightTask task = this.task;
+ // executed on light thread
+ if (!task.priorityHolder.markExecuting()) {
+ // cancelled
+ return false;
+ }
+
+ try {
+ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(task.fromChunk);
+
+ if (task.fromChunk.isLightCorrect() && task.fromChunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) {
+ this.lightEngine.forceLoadInChunk(task.fromChunk, emptySections);
+ this.lightEngine.checkChunkEdges(task.chunkX, task.chunkZ);
+ } else {
+ task.fromChunk.setLightCorrect(false);
+ this.lightEngine.lightChunk(task.fromChunk, emptySections);
+ task.fromChunk.setLightCorrect(true);
+ }
+ // we need to advance status
+ if (task.fromChunk instanceof ProtoChunk chunk && chunk.getPersistedStatus() == ChunkStatus.LIGHT.getParent()) {
+ chunk.setPersistedStatus(ChunkStatus.LIGHT);
+ }
+ } catch (final Throwable thr) {
+ LOGGER.fatal(
+ "Failed to light chunk " + task.fromChunk.getPos().toString()
+ + " in world '" + WorldUtil.getWorldName(this.lightEngine.getWorld()) + "'", thr
+ );
+
+ task.complete(null, thr);
+
+ return true;
+ }
+
+ task.complete(task.fromChunk, null);
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13091425f8
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
@@ -0,0 +1,487 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemConverters;
+import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemFeatures;
+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.ProtoChunk;
+import net.minecraft.world.level.chunk.UpgradeData;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.chunk.storage.ChunkSerializer;
+import net.minecraft.world.level.levelgen.blending.BlendingData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.lang.invoke.VarHandle;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+public final class ChunkLoadTask extends ChunkProgressionTask {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkLoadTask.class);
+
+ private final NewChunkHolder chunkHolder;
+ private final ChunkDataLoadTask loadTask;
+
+ private volatile boolean cancelled;
+ private NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask;
+ private NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask;
+ private GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> loadResult;
+ private final AtomicInteger taskCountToComplete = new AtomicInteger(3); // one for poi, one for entity, and one for chunk data
+
+ public ChunkLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ,
+ final NewChunkHolder chunkHolder, final PrioritisedExecutor.Priority priority) {
+ super(scheduler, world, chunkX, chunkZ);
+ this.chunkHolder = chunkHolder;
+ this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority);
+ this.loadTask.addCallback((final GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> result) -> {
+ ChunkLoadTask.this.loadResult = result; // must be before getAndDecrement
+ ChunkLoadTask.this.tryCompleteLoad();
+ });
+ }
+
+ private void tryCompleteLoad() {
+ final int count = this.taskCountToComplete.decrementAndGet();
+ if (count == 0) {
+ final GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> result = this.cancelled ? null : this.loadResult; // only after the getAndDecrement
+ ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right());
+ } else if (count < 0) {
+ throw new IllegalStateException("Called tryCompleteLoad() too many times");
+ }
+ }
+
+ @Override
+ public ChunkStatus getTargetStatus() {
+ return ChunkStatus.EMPTY;
+ }
+
+ private boolean scheduled;
+
+ @Override
+ public boolean isScheduled() {
+ return this.scheduled;
+ }
+
+ @Override
+ public void schedule() {
+ final NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask;
+ final NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask;
+
+ final Consumer<GenericDataLoadTask.TaskResult<?, ?>> scheduleLoadTask = (final GenericDataLoadTask.TaskResult<?, ?> result) -> {
+ ChunkLoadTask.this.tryCompleteLoad();
+ };
+
+ // NOTE: it is IMPOSSIBLE for getOrLoadEntityData/getOrLoadPoiData to complete synchronously, because
+ // they must schedule a task to off main or to on main to complete
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ if (this.scheduled) {
+ throw new IllegalStateException("schedule() called twice");
+ }
+ this.scheduled = true;
+ if (this.cancelled) {
+ return;
+ }
+ if (!this.chunkHolder.isEntityChunkNBTLoaded()) {
+ entityLoadTask = this.chunkHolder.getOrLoadEntityData((Consumer)scheduleLoadTask);
+ } else {
+ entityLoadTask = null;
+ this.tryCompleteLoad();
+ }
+
+ if (!this.chunkHolder.isPoiChunkLoaded()) {
+ poiLoadTask = this.chunkHolder.getOrLoadPoiData((Consumer)scheduleLoadTask);
+ } else {
+ poiLoadTask = null;
+ this.tryCompleteLoad();
+ }
+
+ this.entityLoadTask = entityLoadTask;
+ this.poiLoadTask = poiLoadTask;
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+
+ if (entityLoadTask != null) {
+ entityLoadTask.schedule();
+ }
+
+ if (poiLoadTask != null) {
+ poiLoadTask.schedule();
+ }
+
+ this.loadTask.schedule(false);
+ }
+
+ @Override
+ public void cancel() {
+ // must be before load task access, so we can synchronise with the writes to the fields
+ final boolean scheduled;
+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ);
+ try {
+ // must read field here, as it may be written later conucrrently -
+ // we need to know if we scheduled _before_ cancellation
+ scheduled = this.scheduled;
+ this.cancelled = true;
+ } finally {
+ this.scheduler.schedulingLockArea.unlock(schedulingLock);
+ }
+
+ /*
+ Note: The entityLoadTask/poiLoadTask do not complete when cancelled,
+ so we need to manually try to complete in those cases
+ It is also important to note that we set the cancelled field first, just in case
+ the chunk load task attempts to complete with a non-null value
+ */
+
+ if (scheduled) {
+ // since we scheduled, we need to cancel the tasks
+ if (this.entityLoadTask != null) {
+ if (this.entityLoadTask.cancel()) {
+ this.tryCompleteLoad();
+ }
+ }
+ if (this.poiLoadTask != null) {
+ if (this.poiLoadTask.cancel()) {
+ this.tryCompleteLoad();
+ }
+ }
+ } else {
+ // since nothing was scheduled, we need to decrement the task count here ourselves
+
+ // for entity load task
+ this.tryCompleteLoad();
+
+ // for poi load task
+ this.tryCompleteLoad();
+ }
+ this.loadTask.cancel();
+ }
+
+ @Override
+ public PrioritisedExecutor.Priority getPriority() {
+ return this.loadTask.getPriority();
+ }
+
+ @Override
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
+ if (entityLoad != null) {
+ entityLoad.lowerPriority(priority);
+ }
+
+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
+
+ if (poiLoad != null) {
+ poiLoad.lowerPriority(priority);
+ }
+
+ this.loadTask.lowerPriority(priority);
+ }
+
+ @Override
+ public void setPriority(final PrioritisedExecutor.Priority priority) {
+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
+ if (entityLoad != null) {
+ entityLoad.setPriority(priority);
+ }
+
+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
+
+ if (poiLoad != null) {
+ poiLoad.setPriority(priority);
+ }
+
+ this.loadTask.setPriority(priority);
+ }
+
+ @Override
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
+ if (entityLoad != null) {
+ entityLoad.raisePriority(priority);
+ }
+
+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask();
+
+ if (poiLoad != null) {
+ poiLoad.raisePriority(priority);
+ }
+
+ this.loadTask.raisePriority(priority);
+ }
+
+ protected static abstract class CallbackDataLoadTask<OnMain,FinalCompletion> extends GenericDataLoadTask<OnMain,FinalCompletion> {
+
+ private TaskResult<FinalCompletion, Throwable> result;
+ private final MultiThreadedQueue<Consumer<TaskResult<FinalCompletion, Throwable>>> waiters = new MultiThreadedQueue<>();
+
+ protected volatile boolean completed;
+ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(CallbackDataLoadTask.class, "completed", boolean.class);
+
+ protected CallbackDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
+ final int chunkZ, final RegionFileIOThread.RegionFileType type,
+ final PrioritisedExecutor.Priority priority) {
+ super(scheduler, world, chunkX, chunkZ, type, priority);
+ }
+
+ public void addCallback(final Consumer<TaskResult<FinalCompletion, Throwable>> consumer) {
+ if (!this.waiters.add(consumer)) {
+ try {
+ consumer.accept(this.result);
+ } catch (final Throwable throwable) {
+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
+ "Consumer", ChunkTaskScheduler.stringIfNull(consumer),
+ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.result.right()),
+ "CallbackDataLoadTask impl", this.getClass().getName()
+ ), throwable);
+ }
+ }
+ }
+
+ @Override
+ protected void onComplete(final TaskResult<FinalCompletion, Throwable> result) {
+ if ((boolean)COMPLETED_HANDLE.getAndSet((CallbackDataLoadTask)this, (boolean)true)) {
+ throw new IllegalStateException("Already completed");
+ }
+ this.result = result;
+ Consumer<TaskResult<FinalCompletion, Throwable>> consumer;
+ while ((consumer = this.waiters.pollOrBlockAdds()) != null) {
+ try {
+ consumer.accept(result);
+ } catch (final Throwable throwable) {
+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
+ "Consumer", ChunkTaskScheduler.stringIfNull(consumer),
+ "Completed throwable", ChunkTaskScheduler.stringIfNull(result.right()),
+ "CallbackDataLoadTask impl", this.getClass().getName()
+ ), throwable);
+ return;
+ }
+ }
+ }
+ }
+
+ private static final class ChunkDataLoadTask extends CallbackDataLoadTask<CompoundTag, ChunkAccess> {
+ private ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
+ final int chunkZ, final PrioritisedExecutor.Priority priority) {
+ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.CHUNK_DATA, priority);
+ }
+
+ @Override
+ protected boolean hasOffMain() {
+ return true;
+ }
+
+ @Override
+ protected boolean hasOnMain() {
+ return true;
+ }
+
+ @Override
+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ return this.scheduler.loadExecutor.createTask(run, priority);
+ }
+
+ @Override
+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ return this.scheduler.createChunkTask(this.chunkX, this.chunkZ, run, priority);
+ }
+
+ @Override
+ protected TaskResult<ChunkAccess, Throwable> completeOnMainOffMain(final CompoundTag data, final Throwable throwable) {
+ if (throwable != null) {
+ return new TaskResult<>(null, throwable);
+ }
+ if (data == null) {
+ return new TaskResult<>(this.getEmptyChunk(), null);
+ }
+
+ if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) {
+ return this.deserialize(data);
+ }
+ // need to deserialize on main thread
+ return null;
+ }
+
+ private ProtoChunk getEmptyChunk() {
+ return new ProtoChunk(
+ new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world,
+ this.world.registryAccess().registryOrThrow(Registries.BIOME), (BlendingData)null
+ );
+ }
+
+ @Override
+ protected TaskResult<CompoundTag, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
+ if (throwable != null) {
+ LOGGER.error("Failed to load chunk data for task: " + this.toString() + ", chunk data will be lost", throwable);
+ return new TaskResult<>(null, null);
+ }
+
+ if (data == null) {
+ return new TaskResult<>(null, null);
+ }
+
+ try {
+ // run converters
+ final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data, new net.minecraft.world.level.ChunkPos(this.chunkX, this.chunkZ));
+
+ return new TaskResult<>(converted, null);
+ } catch (final Throwable thr2) {
+ LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2);
+ return new TaskResult<>(null, null);
+ }
+ }
+
+ private TaskResult<ChunkAccess, Throwable> deserialize(final CompoundTag data) {
+ try {
+ final ChunkAccess deserialized = ChunkSerializer.read(
+ this.world, this.world.getPoiManager(), this.world.getChunkSource().chunkMap.storageInfo(), new ChunkPos(this.chunkX, this.chunkZ), data
+ );
+ return new TaskResult<>(deserialized, null);
+ } catch (final Throwable thr2) {
+ LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2);
+ return new TaskResult<>(this.getEmptyChunk(), null);
+ }
+ }
+
+ @Override
+ protected TaskResult<ChunkAccess, Throwable> runOnMain(final CompoundTag data, final Throwable throwable) {
+ // data != null && throwable == null
+ if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) {
+ throw new UnsupportedOperationException();
+ }
+ return this.deserialize(data);
+ }
+ }
+
+ public static final class PoiDataLoadTask extends CallbackDataLoadTask<PoiChunk, PoiChunk> {
+
+ public PoiDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
+ final int chunkZ, final PrioritisedExecutor.Priority priority) {
+ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.POI_DATA, priority);
+ }
+
+ @Override
+ protected boolean hasOffMain() {
+ return true;
+ }
+
+ @Override
+ protected boolean hasOnMain() {
+ return false;
+ }
+
+ @Override
+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ return this.scheduler.loadExecutor.createTask(run, priority);
+ }
+
+ @Override
+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected TaskResult<PoiChunk, Throwable> completeOnMainOffMain(final PoiChunk data, final Throwable throwable) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected TaskResult<PoiChunk, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
+ if (throwable != null) {
+ LOGGER.error("Failed to load poi data for task: " + this.toString() + ", poi data will be lost", throwable);
+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
+ }
+
+ if (data == null || data.isEmpty()) {
+ // nothing to do
+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
+ }
+
+ try {
+ // run converters
+ final CompoundTag converted = ChunkSystemConverters.convertPoiCompoundTag(data, this.world);
+
+ // now we need to parse it
+ return new TaskResult<>(PoiChunk.parse(this.world, this.chunkX, this.chunkZ, converted), null);
+ } catch (final Throwable thr2) {
+ LOGGER.error("Failed to run parse poi data for task: " + this.toString() + ", poi data will be lost", thr2);
+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null);
+ }
+ }
+
+ @Override
+ protected TaskResult<PoiChunk, Throwable> runOnMain(final PoiChunk data, final Throwable throwable) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ public static final class EntityDataLoadTask extends CallbackDataLoadTask<CompoundTag, CompoundTag> {
+
+ public EntityDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
+ final int chunkZ, final PrioritisedExecutor.Priority priority) {
+ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, priority);
+ }
+
+ @Override
+ protected boolean hasOffMain() {
+ return true;
+ }
+
+ @Override
+ protected boolean hasOnMain() {
+ return false;
+ }
+
+ @Override
+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ return this.scheduler.loadExecutor.createTask(run, priority);
+ }
+
+ @Override
+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected TaskResult<CompoundTag, Throwable> completeOnMainOffMain(final CompoundTag data, final Throwable throwable) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected TaskResult<CompoundTag, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
+ if (throwable != null) {
+ LOGGER.error("Failed to load entity data for task: " + this.toString() + ", entity data will be lost", throwable);
+ return new TaskResult<>(null, null);
+ }
+
+ if (data == null || data.isEmpty()) {
+ // nothing to do
+ return new TaskResult<>(null, null);
+ }
+
+ try {
+ return new TaskResult<>(ChunkSystemConverters.convertEntityChunkCompoundTag(data, this.world), null);
+ } catch (final Throwable thr2) {
+ LOGGER.error("Failed to run converters for entity data for task: " + this.toString() + ", entity data will be lost", thr2);
+ return new TaskResult<>(null, thr2);
+ }
+ }
+
+ @Override
+ protected TaskResult<CompoundTag, Throwable> runOnMain(final CompoundTag data, final Throwable throwable) {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..70e900b0f9c131900bf8b3f3ecbfbd5df5361205
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
@@ -0,0 +1,101 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import java.lang.invoke.VarHandle;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+public abstract class ChunkProgressionTask {
+
+ private final MultiThreadedQueue<BiConsumer<ChunkAccess, Throwable>> waiters = new MultiThreadedQueue<>();
+ private ChunkAccess completedChunk;
+ private Throwable completedThrowable;
+
+ protected final ChunkTaskScheduler scheduler;
+ protected final ServerLevel world;
+ protected final int chunkX;
+ protected final int chunkZ;
+
+ protected volatile boolean completed;
+ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(ChunkProgressionTask.class, "completed", boolean.class);
+
+ protected ChunkProgressionTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ) {
+ this.scheduler = scheduler;
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ }
+
+ // Used only for debug json
+ public abstract boolean isScheduled();
+
+ // Note: It is the responsibility of the task to set the chunk's status once it has completed
+ public abstract ChunkStatus getTargetStatus();
+
+ /* Only executed once */
+ /* Implementations must be prepared to handle cases where cancel() is called before schedule() */
+ public abstract void schedule();
+
+ /* May be called multiple times */
+ public abstract void cancel();
+
+ public abstract PrioritisedExecutor.Priority getPriority();
+
+ /* Schedule lock is always held for the priority update calls */
+
+ public abstract void lowerPriority(final PrioritisedExecutor.Priority priority);
+
+ public abstract void setPriority(final PrioritisedExecutor.Priority priority);
+
+ public abstract void raisePriority(final PrioritisedExecutor.Priority priority);
+
+ public final void onComplete(final BiConsumer<ChunkAccess, Throwable> onComplete) {
+ if (!this.waiters.add(onComplete)) {
+ try {
+ onComplete.accept(this.completedChunk, this.completedThrowable);
+ } catch (final Throwable throwable) {
+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
+ "Consumer", ChunkTaskScheduler.stringIfNull(onComplete),
+ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.completedThrowable)
+ ), throwable);
+ }
+ }
+ }
+
+ protected final void complete(final ChunkAccess chunk, final Throwable throwable) {
+ try {
+ this.complete0(chunk, throwable);
+ } catch (final Throwable thr2) {
+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
+ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable)
+ ), thr2);
+ }
+ }
+
+ private void complete0(final ChunkAccess chunk, final Throwable throwable) {
+ if ((boolean)COMPLETED_HANDLE.getAndSet((ChunkProgressionTask)this, (boolean)true)) {
+ throw new IllegalStateException("Already completed");
+ }
+ this.completedChunk = chunk;
+ this.completedThrowable = throwable;
+
+ BiConsumer<ChunkAccess, Throwable> consumer;
+ while ((consumer = this.waiters.pollOrBlockAdds()) != null) {
+ consumer.accept(chunk, throwable);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ChunkProgressionTask{class: " + this.getClass().getName() + ", for world: " + WorldUtil.getWorldName(this.world) +
+ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() +
+ ", status: " + this.getTargetStatus().toString() + ", scheduled: " + this.isScheduled() + "}";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c17d5589f15f1155be08be670d29acbe954a8fa
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
@@ -0,0 +1,217 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.ChunkMap;
+import net.minecraft.server.level.GenerationChunkHolder;
+import net.minecraft.server.level.ServerChunkCache;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.util.StaticCache2D;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.ProtoChunk;
+import net.minecraft.world.level.chunk.status.ChunkPyramid;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.chunk.status.WorldGenContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.lang.invoke.VarHandle;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask implements Runnable {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkUpgradeGenericStatusTask.class);
+
+ private final ChunkAccess fromChunk;
+ private final ChunkStatus fromStatus;
+ private final ChunkStatus toStatus;
+ private final StaticCache2D<GenerationChunkHolder> neighbours;
+
+ private final PrioritisedExecutor.PrioritisedTask generateTask;
+
+ public ChunkUpgradeGenericStatusTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
+ final int chunkZ, final ChunkAccess chunk, final StaticCache2D<GenerationChunkHolder> neighbours,
+ final ChunkStatus toStatus, final PrioritisedExecutor.Priority priority) {
+ super(scheduler, world, chunkX, chunkZ);
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.fromChunk = chunk;
+ this.fromStatus = chunk.getPersistedStatus();
+ this.toStatus = toStatus;
+ this.neighbours = neighbours;
+ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isParallelCapable()) {
+ this.generateTask = this.scheduler.parallelGenExecutor.createTask(this, priority);
+ } else {
+ final int writeRadius = ((ChunkSystemChunkStatus)this.toStatus).moonrise$getWriteRadius();
+ if (writeRadius < 0) {
+ this.generateTask = this.scheduler.radiusAwareScheduler.createInfiniteRadiusTask(this, priority);
+ } else {
+ this.generateTask = this.scheduler.radiusAwareScheduler.createTask(chunkX, chunkZ, writeRadius, this, priority);
+ }
+ }
+ }
+
+ @Override
+ public ChunkStatus getTargetStatus() {
+ return this.toStatus;
+ }
+
+ private boolean isEmptyTask() {
+ // must use fromStatus here to avoid any race condition with run() overwriting the status
+ final boolean generation = !this.fromStatus.isOrAfter(this.toStatus);
+ return (generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) || (!generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus());
+ }
+
+ @Override
+ public void run() {
+ final ChunkAccess chunk = this.fromChunk;
+
+ final ServerChunkCache serverChunkCache = this.world.getChunkSource();
+ final ChunkMap chunkMap = serverChunkCache.chunkMap;
+
+ final CompletableFuture<ChunkAccess> completeFuture;
+
+ final boolean generation;
+ boolean completing = false;
+
+ // note: should optimise the case where the chunk does not need to execute the status, because
+ // schedule() calls this synchronously if it will run through that path
+
+ final WorldGenContext ctx = chunkMap.worldGenContext;
+ try {
+ generation = !chunk.getPersistedStatus().isOrAfter(this.toStatus);
+ if (generation) {
+ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) {
+ if (chunk instanceof ProtoChunk) {
+ ((ProtoChunk)chunk).setPersistedStatus(this.toStatus);
+ }
+ completing = true;
+ this.complete(chunk, null);
+ return;
+ }
+ completeFuture = ChunkPyramid.GENERATION_PYRAMID.getStepTo(this.toStatus).apply(ctx, this.neighbours, this.fromChunk)
+ .whenComplete((final ChunkAccess either, final Throwable throwable) -> {
+ if (either instanceof ProtoChunk proto) {
+ proto.setPersistedStatus(ChunkUpgradeGenericStatusTask.this.toStatus);
+ }
+ }
+ );
+ } else {
+ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus()) {
+ completing = true;
+ this.complete(chunk, null);
+ return;
+ }
+ completeFuture = ChunkPyramid.LOADING_PYRAMID.getStepTo(this.toStatus).apply(ctx, this.neighbours, this.fromChunk);
+ }
+ } catch (final Throwable throwable) {
+ if (!completing) {
+ this.complete(null, throwable);
+ return;
+ }
+
+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
+ "Target status", ChunkTaskScheduler.stringIfNull(this.toStatus),
+ "From status", ChunkTaskScheduler.stringIfNull(this.fromStatus),
+ "Generation task", this
+ ), throwable);
+
+ LOGGER.error(
+ "Failed to complete status for chunk: status:" + this.toStatus + ", chunk: (" + this.chunkX +
+ "," + this.chunkZ + "), world: " + WorldUtil.getWorldName(this.world),
+ throwable
+ );
+
+ return;
+ }
+
+ if (!completeFuture.isDone() && !((ChunkSystemChunkStatus)this.toStatus).moonrise$getWarnedAboutNoImmediateComplete().getAndSet(true)) {
+ LOGGER.warn("Future status not complete after scheduling: " + this.toStatus.toString() + ", generate: " + generation);
+ }
+
+ final ChunkAccess newChunk;
+
+ try {
+ newChunk = completeFuture.join();
+ } catch (final Throwable throwable) {
+ this.complete(null, throwable);
+ return;
+ }
+
+ if (newChunk == null) {
+ this.complete(null,
+ new IllegalStateException(
+ "Chunk for status: " + ChunkUpgradeGenericStatusTask.this.toStatus.toString()
+ + ", generation: " + generation + " should not be null! Future: " + completeFuture
+ ).fillInStackTrace()
+ );
+ return;
+ }
+
+ this.complete(newChunk, null);
+ }
+
+ private volatile boolean scheduled;
+ private static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkUpgradeGenericStatusTask.class, "scheduled", boolean.class);
+
+ @Override
+ public boolean isScheduled() {
+ return this.scheduled;
+ }
+
+ @Override
+ public void schedule() {
+ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkUpgradeGenericStatusTask)this, true)) {
+ throw new IllegalStateException("Cannot double call schedule()");
+ }
+ if (this.isEmptyTask()) {
+ if (this.generateTask.cancel()) {
+ this.run();
+ }
+ } else {
+ this.generateTask.queue();
+ }
+ }
+
+ @Override
+ public void cancel() {
+ if (this.generateTask.cancel()) {
+ this.complete(null, null);
+ }
+ }
+
+ @Override
+ public PrioritisedExecutor.Priority getPriority() {
+ return this.generateTask.getPriority();
+ }
+
+ @Override
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.generateTask.lowerPriority(priority);
+ }
+
+ @Override
+ public void setPriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.generateTask.setPriority(priority);
+ }
+
+ @Override
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.generateTask.raisePriority(priority);
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be314b876c
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
@@ -0,0 +1,673 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
+import ca.spottedleaf.concurrentutil.completable.Completable;
+import ca.spottedleaf.concurrentutil.executor.Cancellable;
+import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerLevel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.lang.invoke.VarHandle;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BiConsumer;
+
+public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(GenericDataLoadTask.class);
+
+ protected static final CompoundTag CANCELLED_DATA = new CompoundTag();
+
+ // reference count is the upper 32 bits
+ protected final AtomicLong stageAndReferenceCount = new AtomicLong(STAGE_NOT_STARTED);
+
+ protected static final long STAGE_MASK = 0xFFFFFFFFL;
+ protected static final long STAGE_CANCELLED = 0xFFFFFFFFL;
+ protected static final long STAGE_NOT_STARTED = 0L;
+ protected static final long STAGE_LOADING = 1L;
+ protected static final long STAGE_PROCESSING = 2L;
+ protected static final long STAGE_COMPLETED = 3L;
+
+ // for loading data off disk
+ protected final LoadDataFromDiskTask loadDataFromDiskTask;
+ // processing off-main
+ protected final PrioritisedExecutor.PrioritisedTask processOffMain;
+ // processing on-main
+ protected final PrioritisedExecutor.PrioritisedTask processOnMain;
+
+ protected final ChunkTaskScheduler scheduler;
+ protected final ServerLevel world;
+ protected final int chunkX;
+ protected final int chunkZ;
+ protected final RegionFileIOThread.RegionFileType type;
+
+ public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
+ final int chunkZ, final RegionFileIOThread.RegionFileType type,
+ final PrioritisedExecutor.Priority priority) {
+ this.scheduler = scheduler;
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.type = type;
+
+ final ProcessOnMainTask mainTask;
+ if (this.hasOnMain()) {
+ mainTask = new ProcessOnMainTask();
+ this.processOnMain = this.createOnMain(mainTask, priority);
+ } else {
+ mainTask = null;
+ this.processOnMain = null;
+ }
+
+ final ProcessOffMainTask offMainTask;
+ if (this.hasOffMain()) {
+ offMainTask = new ProcessOffMainTask(mainTask);
+ this.processOffMain = this.createOffMain(offMainTask, priority);
+ } else {
+ offMainTask = null;
+ this.processOffMain = null;
+ }
+
+ if (this.processOffMain == null && this.processOnMain == null) {
+ throw new IllegalStateException("Illegal class implementation: " + this.getClass().getName() + ", should be able to schedule at least one task!");
+ }
+
+ this.loadDataFromDiskTask = new LoadDataFromDiskTask(world, chunkX, chunkZ, type, new DataLoadCallback(offMainTask, mainTask), priority);
+ }
+
+ public static final record TaskResult<L, R>(L left, R right) {}
+
+ protected abstract boolean hasOffMain();
+
+ protected abstract boolean hasOnMain();
+
+ protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority);
+
+ protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority);
+
+ protected abstract TaskResult<OnMain, Throwable> runOffMain(final CompoundTag data, final Throwable throwable);
+
+ protected abstract TaskResult<FinalCompletion, Throwable> runOnMain(final OnMain data, final Throwable throwable);
+
+ protected abstract void onComplete(final TaskResult<FinalCompletion,Throwable> result);
+
+ protected abstract TaskResult<FinalCompletion, Throwable> completeOnMainOffMain(final OnMain data, final Throwable throwable);
+
+ @Override
+ public String toString() {
+ return "GenericDataLoadTask{class: " + this.getClass().getName() + ", world: " + WorldUtil.getWorldName(this.world) +
+ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() +
+ ", type: " + this.type.toString() + "}";
+ }
+
+ public PrioritisedExecutor.Priority getPriority() {
+ if (this.processOnMain != null) {
+ return this.processOnMain.getPriority();
+ } else {
+ return this.processOffMain.getPriority();
+ }
+ }
+
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ // can't lower I/O tasks, we don't know what they affect
+ if (this.processOffMain != null) {
+ this.processOffMain.lowerPriority(priority);
+ }
+ if (this.processOnMain != null) {
+ this.processOnMain.lowerPriority(priority);
+ }
+ }
+
+ public void setPriority(final PrioritisedExecutor.Priority priority) {
+ // can't lower I/O tasks, we don't know what they affect
+ this.loadDataFromDiskTask.raisePriority(priority);
+ if (this.processOffMain != null) {
+ this.processOffMain.setPriority(priority);
+ }
+ if (this.processOnMain != null) {
+ this.processOnMain.setPriority(priority);
+ }
+ }
+
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ // can't lower I/O tasks, we don't know what they affect
+ this.loadDataFromDiskTask.raisePriority(priority);
+ if (this.processOffMain != null) {
+ this.processOffMain.raisePriority(priority);
+ }
+ if (this.processOnMain != null) {
+ this.processOnMain.raisePriority(priority);
+ }
+ }
+
+ // returns whether scheduleNow() needs to be called
+ public boolean schedule(final boolean delay) {
+ if (this.stageAndReferenceCount.get() != STAGE_NOT_STARTED ||
+ !this.stageAndReferenceCount.compareAndSet(STAGE_NOT_STARTED, (1L << 32) | STAGE_LOADING)) {
+ // try and increment reference count
+ int failures = 0;
+ for (long curr = this.stageAndReferenceCount.get();;) {
+ if ((curr & STAGE_MASK) == STAGE_CANCELLED || (curr & STAGE_MASK) == STAGE_COMPLETED) {
+ // cancelled or completed, nothing to do here
+ return false;
+ }
+
+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, curr + (1L << 32)))) {
+ // successful
+ return false;
+ }
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ if (!delay) {
+ this.scheduleNow();
+ return false;
+ }
+ return true;
+ }
+
+ public void scheduleNow() {
+ this.loadDataFromDiskTask.schedule(); // will schedule the rest
+ }
+
+ // assumes the current stage cannot be completed
+ // returns false if cancelled, returns true if can proceed
+ private boolean advanceStage(final long expect, final long to) {
+ int failures = 0;
+ for (long curr = this.stageAndReferenceCount.get();;) {
+ if ((curr & STAGE_MASK) != expect) {
+ // must be cancelled
+ return false;
+ }
+
+ final long newVal = (curr & ~STAGE_MASK) | to;
+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
+ return true;
+ }
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ public boolean cancel() {
+ int failures = 0;
+ for (long curr = this.stageAndReferenceCount.get();;) {
+ if ((curr & STAGE_MASK) == STAGE_COMPLETED || (curr & STAGE_MASK) == STAGE_CANCELLED) {
+ return false;
+ }
+
+ if ((curr & STAGE_MASK) == STAGE_NOT_STARTED || (curr & ~STAGE_MASK) == (1L << 32)) {
+ // no other references, so we can cancel
+ final long newVal = STAGE_CANCELLED;
+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
+ this.loadDataFromDiskTask.cancel();
+ if (this.processOffMain != null) {
+ this.processOffMain.cancel();
+ }
+ if (this.processOnMain != null) {
+ this.processOnMain.cancel();
+ }
+ this.onComplete(null);
+ return true;
+ }
+ } else {
+ if ((curr & ~STAGE_MASK) == (0L << 32)) {
+ throw new IllegalStateException("Reference count cannot be zero here");
+ }
+ // just decrease the reference count
+ final long newVal = curr - (1L << 32);
+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) {
+ return false;
+ }
+ }
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ private final class DataLoadCallback implements BiConsumer<CompoundTag, Throwable> {
+
+ private final ProcessOffMainTask offMainTask;
+ private final ProcessOnMainTask onMainTask;
+
+ public DataLoadCallback(final ProcessOffMainTask offMainTask, final ProcessOnMainTask onMainTask) {
+ this.offMainTask = offMainTask;
+ this.onMainTask = onMainTask;
+ }
+
+ @Override
+ public void accept(final CompoundTag compoundTag, final Throwable throwable) {
+ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) {
+ // don't try to schedule further
+ return;
+ }
+
+ try {
+ if (compoundTag == CANCELLED_DATA) {
+ // cancelled, except this isn't possible
+ LOGGER.error("Data callback says cancelled, but stage does not?");
+ return;
+ }
+
+ // get off of the regionfile callback ASAP, no clue what locks are held right now...
+ if (GenericDataLoadTask.this.processOffMain != null) {
+ this.offMainTask.data = compoundTag;
+ this.offMainTask.throwable = throwable;
+ GenericDataLoadTask.this.processOffMain.queue();
+ return;
+ } else {
+ // no off-main task, so go straight to main
+ this.onMainTask.data = (OnMain)compoundTag;
+ this.onMainTask.throwable = throwable;
+ GenericDataLoadTask.this.processOnMain.queue();
+ }
+ } catch (final Throwable thr2) {
+ LOGGER.error("Failed I/O callback for task: " + GenericDataLoadTask.this.toString(), thr2);
+ GenericDataLoadTask.this.scheduler.unrecoverableChunkSystemFailure(
+ GenericDataLoadTask.this.chunkX, GenericDataLoadTask.this.chunkZ, Map.of(
+ "Callback throwable", ChunkTaskScheduler.stringIfNull(throwable)
+ ), thr2
+ );
+ }
+ }
+ }
+
+ private final class ProcessOffMainTask implements Runnable {
+
+ private CompoundTag data;
+ private Throwable throwable;
+ private final ProcessOnMainTask schedule;
+
+ public ProcessOffMainTask(final ProcessOnMainTask schedule) {
+ this.schedule = schedule;
+ }
+
+ @Override
+ public void run() {
+ if (!GenericDataLoadTask.this.advanceStage(STAGE_LOADING, this.schedule == null ? STAGE_COMPLETED : STAGE_PROCESSING)) {
+ // cancelled
+ return;
+ }
+ final TaskResult<OnMain, Throwable> newData = GenericDataLoadTask.this.runOffMain(this.data, this.throwable);
+
+ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) {
+ // don't try to schedule further
+ return;
+ }
+
+ if (this.schedule != null) {
+ final TaskResult<FinalCompletion, Throwable> syncComplete = GenericDataLoadTask.this.completeOnMainOffMain(newData.left, newData.right);
+
+ if (syncComplete != null) {
+ if (GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) {
+ GenericDataLoadTask.this.onComplete(syncComplete);
+ } // else: cancelled
+ return;
+ }
+
+ this.schedule.data = newData.left;
+ this.schedule.throwable = newData.right;
+
+ GenericDataLoadTask.this.processOnMain.queue();
+ } else {
+ GenericDataLoadTask.this.onComplete((TaskResult<FinalCompletion, Throwable>)newData);
+ }
+ }
+ }
+
+ private final class ProcessOnMainTask implements Runnable {
+
+ private OnMain data;
+ private Throwable throwable;
+
+ @Override
+ public void run() {
+ if (!GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) {
+ // cancelled
+ return;
+ }
+ final TaskResult<FinalCompletion, Throwable> result = GenericDataLoadTask.this.runOnMain(this.data, this.throwable);
+
+ GenericDataLoadTask.this.onComplete(result);
+ }
+ }
+
+ protected static final class LoadDataFromDiskTask {
+
+ private volatile int priority;
+ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(LoadDataFromDiskTask.class, "priority", int.class);
+
+ private static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 0;
+ private static final int PRIORITY_LOAD_SCHEDULED = Integer.MIN_VALUE >>> 1;
+ private static final int PRIORITY_UNLOAD_SCHEDULED = Integer.MIN_VALUE >>> 2;
+
+ private static final int PRIORITY_FLAGS = ~Character.MAX_VALUE;
+
+ private final int getPriorityVolatile() {
+ return (int)PRIORITY_HANDLE.getVolatile((LoadDataFromDiskTask)this);
+ }
+
+ private final int compareAndExchangePriorityVolatile(final int expect, final int update) {
+ return (int)PRIORITY_HANDLE.compareAndExchange((LoadDataFromDiskTask)this, (int)expect, (int)update);
+ }
+
+ private final int getAndOrPriorityVolatile(final int val) {
+ return (int)PRIORITY_HANDLE.getAndBitwiseOr((LoadDataFromDiskTask)this, (int)val);
+ }
+
+ private final void setPriorityPlain(final int val) {
+ PRIORITY_HANDLE.set((LoadDataFromDiskTask)this, (int)val);
+ }
+
+ private final ServerLevel world;
+ private final int chunkX;
+ private final int chunkZ;
+
+ private final RegionFileIOThread.RegionFileType type;
+ private Cancellable dataLoadTask;
+ private Cancellable dataUnloadCancellable;
+ private DelayedPrioritisedTask dataUnloadTask;
+
+ private final BiConsumer<CompoundTag, Throwable> onComplete;
+ private final AtomicBoolean scheduled = new AtomicBoolean();
+
+ // onComplete should be caller sensitive, it may complete synchronously with schedule() - which does
+ // hold a priority lock.
+ public LoadDataFromDiskTask(final ServerLevel world, final int chunkX, final int chunkZ,
+ final RegionFileIOThread.RegionFileType type,
+ final BiConsumer<CompoundTag, Throwable> onComplete,
+ final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.type = type;
+ this.onComplete = onComplete;
+ this.setPriorityPlain(priority.priority);
+ }
+
+ private void complete(final CompoundTag data, final Throwable throwable) {
+ try {
+ this.onComplete.accept(data, throwable);
+ } catch (final Throwable thr2) {
+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of(
+ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable),
+ "Regionfile type", ChunkTaskScheduler.stringIfNull(this.type)
+ ), thr2);
+ }
+ }
+
+ private boolean markExecuting() {
+ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0;
+ }
+
+ private boolean isMarkedExecuted() {
+ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0;
+ }
+
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
+ int failures = 0;
+ for (int curr = this.getPriorityVolatile();;) {
+ if ((curr & PRIORITY_EXECUTED) != 0) {
+ // cancelled or executed
+ return;
+ }
+
+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
+ RegionFileIOThread.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
+ return;
+ }
+
+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
+ if (this.dataUnloadTask != null) {
+ this.dataUnloadTask.lowerPriority(priority);
+ }
+ // no return - we need to propagate priority
+ }
+
+ if (!priority.isHigherPriority(curr & ~PRIORITY_FLAGS)) {
+ return;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
+ return;
+ }
+
+ // failed, retry
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ public void setPriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
+ int failures = 0;
+ for (int curr = this.getPriorityVolatile();;) {
+ if ((curr & PRIORITY_EXECUTED) != 0) {
+ // cancelled or executed
+ return;
+ }
+
+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
+ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
+ return;
+ }
+
+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
+ if (this.dataUnloadTask != null) {
+ this.dataUnloadTask.setPriority(priority);
+ }
+ // no return - we need to propagate priority
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
+ return;
+ }
+
+ // failed, retry
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
+ int failures = 0;
+ for (int curr = this.getPriorityVolatile();;) {
+ if ((curr & PRIORITY_EXECUTED) != 0) {
+ // cancelled or executed
+ return;
+ }
+
+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
+ RegionFileIOThread.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
+ return;
+ }
+
+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) {
+ if (this.dataUnloadTask != null) {
+ this.dataUnloadTask.raisePriority(priority);
+ }
+ // no return - we need to propagate priority
+ }
+
+ if (!priority.isLowerPriority(curr & ~PRIORITY_FLAGS)) {
+ return;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) {
+ return;
+ }
+
+ // failed, retry
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+
+ public void cancel() {
+ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) {
+ // cancelled or executed already
+ return;
+ }
+
+ // OK if we miss the field read, the task cannot complete if the cancelled bit is set and
+ // the write to dataLoadTask will check for the cancelled bit
+ if (this.dataUnloadCancellable != null) {
+ this.dataUnloadCancellable.cancel();
+ }
+
+ if (this.dataLoadTask != null) {
+ this.dataLoadTask.cancel();
+ }
+
+ this.complete(CANCELLED_DATA, null);
+ }
+
+ public void schedule() {
+ if (this.scheduled.getAndSet(true)) {
+ throw new IllegalStateException("schedule() called twice");
+ }
+ int priority = this.getPriorityVolatile();
+
+ if ((priority & PRIORITY_EXECUTED) != 0) {
+ // cancelled
+ return;
+ }
+
+ final BiConsumer<CompoundTag, Throwable> consumer = (final CompoundTag data, final Throwable thr) -> {
+ // because cancelScheduled() cannot actually stop this task from executing in every case, we need
+ // to mark complete here to ensure we do not double complete
+ if (LoadDataFromDiskTask.this.markExecuting()) {
+ LoadDataFromDiskTask.this.complete(data, thr);
+ } // else: cancelled
+ };
+
+ final PrioritisedExecutor.Priority initialPriority = PrioritisedExecutor.Priority.getPriority(priority);
+ boolean scheduledUnload = false;
+
+ final NewChunkHolder holder = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ);
+ if (holder != null) {
+ final BiConsumer<CompoundTag, Throwable> unloadConsumer = (final CompoundTag data, final Throwable thr) -> {
+ if (data != null) {
+ consumer.accept(data, null);
+ } else {
+ // need to schedule task
+ LoadDataFromDiskTask.this.schedule(false, consumer, PrioritisedExecutor.Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS));
+ }
+ };
+ Cancellable unloadCancellable = null;
+ CompoundTag syncComplete = null;
+ final NewChunkHolder.UnloadTask unloadTask = holder.getUnloadTask(this.type); // can be null if no task exists
+ final Completable<CompoundTag> unloadCompletable = unloadTask == null ? null : unloadTask.completable();
+ if (unloadCompletable != null) {
+ unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer);
+ if (unloadCancellable == null) {
+ syncComplete = unloadCompletable.getResult();
+ }
+ }
+
+ if (syncComplete != null) {
+ consumer.accept(syncComplete, null);
+ return;
+ }
+
+ if (unloadCancellable != null) {
+ scheduledUnload = true;
+ this.dataUnloadCancellable = unloadCancellable;
+ this.dataUnloadTask = unloadTask.task();
+ }
+ }
+
+ this.schedule(scheduledUnload, consumer, initialPriority);
+ }
+
+ private void schedule(final boolean scheduledUnload, final BiConsumer<CompoundTag, Throwable> consumer, final PrioritisedExecutor.Priority initialPriority) {
+ int priority = this.getPriorityVolatile();
+
+ if ((priority & PRIORITY_EXECUTED) != 0) {
+ // cancelled
+ return;
+ }
+
+ if (!scheduledUnload) {
+ this.dataLoadTask = RegionFileIOThread.loadDataAsync(
+ this.world, this.chunkX, this.chunkZ, this.type, consumer,
+ initialPriority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), initialPriority
+ );
+ }
+
+ int failures = 0;
+ for (;;) {
+ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | (scheduledUnload ? PRIORITY_UNLOAD_SCHEDULED : PRIORITY_LOAD_SCHEDULED)))) {
+ return;
+ }
+
+ if ((priority & PRIORITY_EXECUTED) != 0) {
+ // cancelled or executed
+ if (this.dataUnloadCancellable != null) {
+ this.dataUnloadCancellable.cancel();
+ }
+
+ if (this.dataLoadTask != null) {
+ this.dataLoadTask.cancel();
+ }
+ return;
+ }
+
+ if (scheduledUnload) {
+ if (this.dataUnloadTask != null) {
+ this.dataUnloadTask.setPriority(PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS));
+ }
+ } else {
+ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS));
+ }
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java
new file mode 100644
index 0000000000000000000000000000000000000000..cb6af3712bf9f6f6b8f7a459c309c75dabe83a50
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.server;
+
+public interface ChunkSystemMinecraftServer {
+
+ public void moonrise$setChunkSystemCrash(final Throwable throwable);
+
+ public void moonrise$executeMidTickTasks();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java
new file mode 100644
index 0000000000000000000000000000000000000000..ea759ce6f10f2a5a4e107ab7528030fe931ba223
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.status;
+
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+
+public interface ChunkSystemChunkStep {
+
+ public ChunkStatus moonrise$getRequiredStatusAtRadius(final int radius);
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..129a35ff2db5b3bb6736810fc180796ce55e1875
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.storage;
+
+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
+
+public interface ChunkSystemChunkStorage {
+
+ public RegionFileStorage moonrise$getRegionStorage();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java
new file mode 100644
index 0000000000000000000000000000000000000000..786e6ad17cd6216ef0aadaa7cf10044a0c19c933
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.ticket;
+
+public interface ChunkSystemTicket<T> {
+
+ public long moonrise$getRemoveDelay();
+
+ public void moonrise$setRemoveDelay(final long removeDelay);
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java
new file mode 100644
index 0000000000000000000000000000000000000000..2add7fd15a2210286aeb9af5024263333340d34c
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.ticks;
+
+public interface ChunkSystemLevelChunkTicks {
+
+ public boolean moonrise$isDirty(final long tick);
+
+ public void moonrise$clearDirty();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java
new file mode 100644
index 0000000000000000000000000000000000000000..ce3bb903c9ccb7efa0f004cf79b291dcb1cb7a23
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java
@@ -0,0 +1,15 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.util;
+
+import net.minecraft.util.SortedArraySet;
+
+public interface ChunkSystemSortedArraySet<T> {
+
+ public SortedArraySet<T> moonrise$copy();
+
+ public Object[] moonrise$copyBackingArray();
+
+ public T moonrise$replace(final T object);
+
+ public T moonrise$removeAndGet(final T object);
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
new file mode 100644
index 0000000000000000000000000000000000000000..3a9a564edfdb99e006e4816cb8821bd1e9ecff43
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
@@ -0,0 +1,320 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.util;
+
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import it.unimi.dsi.fastutil.HashCommon;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongIterator;
+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+import java.util.Arrays;
+import java.util.Objects;
+
+public final class ParallelSearchRadiusIteration {
+
+ // expected that this list returns for a given radius, the set of chunks ordered
+ // by manhattan distance
+ private static final long[][] SEARCH_RADIUS_ITERATION_LIST = new long[64+2+1][];
+ static {
+ for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) {
+ // a BFS around -x, -z, +x, +z will give increasing manhatten distance
+ SEARCH_RADIUS_ITERATION_LIST[i] = generateBFSOrder(i);
+ }
+ }
+
+ public static long[] getSearchIteration(final int radius) {
+ return SEARCH_RADIUS_ITERATION_LIST[radius];
+ }
+
+ private static class CustomLongArray extends LongArrayList {
+
+ public CustomLongArray() {
+ super();
+ }
+
+ public CustomLongArray(final int expected) {
+ super(expected);
+ }
+
+ public boolean addAll(final CustomLongArray list) {
+ this.addElements(this.size, list.a, 0, list.size);
+ return list.size != 0;
+ }
+
+ public void addUnchecked(final long value) {
+ this.a[this.size++] = value;
+ }
+
+ public void forceSize(final int to) {
+ this.size = to;
+ }
+
+ @Override
+ public int hashCode() {
+ long h = 1L;
+
+ Objects.checkFromToIndex(0, this.size, this.a.length);
+
+ for (int i = 0; i < this.size; ++i) {
+ h = HashCommon.mix(h + this.a[i]);
+ }
+
+ return (int)h;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (o == this) {
+ return true;
+ }
+
+ if (!(o instanceof CustomLongArray other)) {
+ return false;
+ }
+
+ return this.size == other.size && Arrays.equals(this.a, 0, this.size, other.a, 0, this.size);
+ }
+ }
+
+ private static int getDistanceSize(final int radius, final int max) {
+ if (radius == 0) {
+ return 1;
+ }
+ final int diff = radius - max;
+ if (diff <= 0) {
+ return 4*radius;
+ }
+ return 4*(max - Math.max(0, diff - 1));
+ }
+
+ private static int getQ1DistanceSize(final int radius, final int max) {
+ if (radius == 0) {
+ return 1;
+ }
+ final int diff = radius - max;
+ if (diff <= 0) {
+ return radius+1;
+ }
+ return max - diff + 1;
+ }
+
+ private static final class BasicFIFOLQueue {
+
+ private final long[] values;
+ private int head, tail;
+
+ public BasicFIFOLQueue(final int cap) {
+ if (cap <= 1) {
+ throw new IllegalArgumentException();
+ }
+ this.values = new long[cap];
+ }
+
+ public boolean isEmpty() {
+ return this.head == this.tail;
+ }
+
+ public long removeFirst() {
+ final long ret = this.values[this.head];
+
+ if (this.head == this.tail) {
+ throw new IllegalStateException();
+ }
+
+ ++this.head;
+ if (this.head == this.values.length) {
+ this.head = 0;
+ }
+
+ return ret;
+ }
+
+ public void addLast(final long value) {
+ this.values[this.tail++] = value;
+
+ if (this.tail == this.head) {
+ throw new IllegalStateException();
+ }
+
+ if (this.tail == this.values.length) {
+ this.tail = 0;
+ }
+ }
+ }
+
+ private static CustomLongArray[] makeQ1BFS(final int radius) {
+ final CustomLongArray[] ret = new CustomLongArray[2 * radius + 1];
+ final BasicFIFOLQueue queue = new BasicFIFOLQueue(Math.max(1, 4 * radius) + 1);
+ final LongOpenHashSet seen = new LongOpenHashSet((radius + 1) * (radius + 1));
+
+ seen.add(CoordinateUtils.getChunkKey(0, 0));
+ queue.addLast(CoordinateUtils.getChunkKey(0, 0));
+ while (!queue.isEmpty()) {
+ final long chunk = queue.removeFirst();
+ final int chunkX = CoordinateUtils.getChunkX(chunk);
+ final int chunkZ = CoordinateUtils.getChunkZ(chunk);
+
+ final int index = Math.abs(chunkX) + Math.abs(chunkZ);
+ final CustomLongArray list = ret[index];
+ if (list != null) {
+ list.addUnchecked(chunk);
+ } else {
+ (ret[index] = new CustomLongArray(getQ1DistanceSize(index, radius))).addUnchecked(chunk);
+ }
+
+ for (int i = 0; i < 4; ++i) {
+ // 0 -> -1, 0
+ // 1 -> 0, -1
+ // 2 -> 1, 0
+ // 3 -> 0, 1
+
+ final int signInv = -(i >>> 1); // 2/3 -> -(1), 0/1 -> -(0)
+ // note: -n = (~n) + 1
+ // (n ^ signInv) - signInv = signInv == 0 ? ((n ^ 0) - 0 = n) : ((n ^ -1) - (-1) = ~n + 1)
+
+ final int axis = i & 1; // 0/2 -> 0, 1/3 -> 1
+ final int dx = ((axis - 1) ^ signInv) - signInv; // 0 -> -1, 1 -> 0
+ final int dz = (-axis ^ signInv) - signInv; // 0 -> 0, 1 -> -1
+
+ final int neighbourX = chunkX + dx;
+ final int neighbourZ = chunkZ + dz;
+ final long neighbour = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
+
+ if ((neighbourX | neighbourZ) < 0 || Math.max(Math.abs(neighbourX), Math.abs(neighbourZ)) > radius) {
+ // don't enqueue out of range
+ continue;
+ }
+
+ if (!seen.add(neighbour)) {
+ continue;
+ }
+
+ queue.addLast(neighbour);
+ }
+ }
+
+ return ret;
+ }
+
+ // doesn't appear worth optimising this function now, even though it's 70% of the call
+ private static CustomLongArray spread(final CustomLongArray input, final int size) {
+ final LongLinkedOpenHashSet notAdded = new LongLinkedOpenHashSet(input);
+ final CustomLongArray added = new CustomLongArray(size);
+
+ while (!notAdded.isEmpty()) {
+ if (added.isEmpty()) {
+ added.addUnchecked(notAdded.removeLastLong());
+ continue;
+ }
+
+ long maxChunk = -1L;
+ int maxDist = 0;
+
+ // select the chunk from the not yet added set that has the largest minimum distance from
+ // the current set of added chunks
+
+ for (final LongIterator iterator = notAdded.iterator(); iterator.hasNext();) {
+ final long chunkKey = iterator.nextLong();
+ final int chunkX = CoordinateUtils.getChunkX(chunkKey);
+ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey);
+
+ int minDist = Integer.MAX_VALUE;
+
+ final int len = added.size();
+ final long[] addedArr = added.elements();
+ Objects.checkFromToIndex(0, len, addedArr.length);
+ for (int i = 0; i < len; ++i) {
+ final long addedKey = addedArr[i];
+ final int addedX = CoordinateUtils.getChunkX(addedKey);
+ final int addedZ = CoordinateUtils.getChunkZ(addedKey);
+
+ // here we use square distance because chunk generation uses neighbours in a square radius
+ final int dist = Math.max(Math.abs(addedX - chunkX), Math.abs(addedZ - chunkZ));
+
+ minDist = Math.min(dist, minDist);
+ }
+
+ if (minDist > maxDist) {
+ maxDist = minDist;
+ maxChunk = chunkKey;
+ }
+ }
+
+ // move the selected chunk from the not added set to the added set
+
+ if (!notAdded.remove(maxChunk)) {
+ throw new IllegalStateException();
+ }
+
+ added.addUnchecked(maxChunk);
+ }
+
+ return added;
+ }
+
+ private static void expandQuadrants(final CustomLongArray input, final int size) {
+ final int len = input.size();
+ final long[] array = input.elements();
+
+ int writeIndex = size - 1;
+ for (int i = len - 1; i >= 0; --i) {
+ final long key = array[i];
+ final int chunkX = CoordinateUtils.getChunkX(key);
+ final int chunkZ = CoordinateUtils.getChunkZ(key);
+
+ if ((chunkX | chunkZ) < 0 || (i != 0 && chunkX == 0 && chunkZ == 0)) {
+ throw new IllegalStateException();
+ }
+
+ // Q4
+ if (chunkZ != 0) {
+ array[writeIndex--] = CoordinateUtils.getChunkKey(chunkX, -chunkZ);
+ }
+ // Q3
+ if (chunkX != 0 && chunkZ != 0) {
+ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, -chunkZ);
+ }
+ // Q2
+ if (chunkX != 0) {
+ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, chunkZ);
+ }
+
+ array[writeIndex--] = key;
+ }
+
+ input.forceSize(size);
+
+ if (writeIndex != -1) {
+ throw new IllegalStateException();
+ }
+ }
+
+ private static long[] generateBFSOrder(final int radius) {
+ // by using only the first quadrant, we can reduce the total element size by 4 when spreading
+ final CustomLongArray[] byDistance = makeQ1BFS(radius);
+
+ // to increase generation parallelism, we want to space the chunks out so that they are not nearby when generating
+ // this also means we are minimising locality
+ // but, we need to maintain sorted order by manhatten distance
+
+ // per manhatten distance we transform the chunk list so that each element is maximally spaced out from each other
+ for (int i = 0, len = byDistance.length; i < len; ++i) {
+ final CustomLongArray points = byDistance[i];
+ final int expectedSize = getDistanceSize(i, radius);
+
+ final CustomLongArray spread = spread(points, expectedSize);
+ // add in Q2, Q3, Q4
+ expandQuadrants(spread, expectedSize);
+
+ byDistance[i] = spread;
+ }
+
+ // now, rebuild the list so that it still maintains manhatten distance order
+ final CustomLongArray ret = new CustomLongArray((2 * radius + 1) * (2 * radius + 1));
+
+ for (final CustomLongArray dist : byDistance) {
+ ret.addAll(dist);
+ }
+
+ return ret.elements();
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java
new file mode 100644
index 0000000000000000000000000000000000000000..ea6b6ed27b212719feb31610faac974899688839
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java
@@ -0,0 +1,12 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.world;
+
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.phys.AABB;
+import java.util.List;
+import java.util.function.Predicate;
+
+public interface ChunkSystemEntityGetter {
+
+ public List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate);
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java
new file mode 100644
index 0000000000000000000000000000000000000000..4b9e2fa963c14f65f15407c1814c543c2999ea32
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java
@@ -0,0 +1,11 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.world;
+
+import net.minecraft.world.level.chunk.LevelChunk;
+
+public interface ChunkSystemServerChunkCache {
+
+ public void moonrise$setFullChunk(final int chunkX, final int chunkZ, final LevelChunk chunk);
+
+ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ);
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
new file mode 100644
index 0000000000000000000000000000000000000000..2bfdf3721db9a45e36538d71cbefcb1d339e6c58
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
@@ -0,0 +1,9 @@
+package ca.spottedleaf.moonrise.patches.starlight.blockstate;
+
+public interface StarlightAbstractBlockState {
+
+ public boolean starlight$isConditionallyFullOpaque();
+
+ public int starlight$getOpacityIfCached();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java
new file mode 100644
index 0000000000000000000000000000000000000000..ed80017c8f257b981d626a37ffc5480d9b326558
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java
@@ -0,0 +1,18 @@
+package ca.spottedleaf.moonrise.patches.starlight.chunk;
+
+import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
+
+public interface StarlightChunk {
+
+ public SWMRNibbleArray[] starlight$getBlockNibbles();
+ public void starlight$setBlockNibbles(final SWMRNibbleArray[] nibbles);
+
+ public SWMRNibbleArray[] starlight$getSkyNibbles();
+ public void starlight$setSkyNibbles(final SWMRNibbleArray[] nibbles);
+
+ public boolean[] starlight$getSkyEmptinessMap();
+ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap);
+
+ public boolean[] starlight$getBlockEmptinessMap();
+ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap);
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..154443ac1ee1d6d18b8ff0f40a307d638b213aeb
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
@@ -0,0 +1,277 @@
+package ca.spottedleaf.moonrise.patches.starlight.light;
+
+import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState;
+import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunkSection;
+import net.minecraft.world.level.chunk.LightChunkGetter;
+import net.minecraft.world.level.chunk.PalettedContainer;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.phys.shapes.Shapes;
+import net.minecraft.world.phys.shapes.VoxelShape;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public final class BlockStarLightEngine extends StarLightEngine {
+
+ public BlockStarLightEngine(final Level world) {
+ super(false, world);
+ }
+
+ @Override
+ protected boolean[] getEmptinessMap(final ChunkAccess chunk) {
+ return ((StarlightChunk)chunk).starlight$getBlockEmptinessMap();
+ }
+
+ @Override
+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) {
+ ((StarlightChunk)chunk).starlight$setBlockEmptinessMap(to);
+ }
+
+ @Override
+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) {
+ return ((StarlightChunk)chunk).starlight$getBlockNibbles();
+ }
+
+ @Override
+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) {
+ ((StarlightChunk)chunk).starlight$setBlockNibbles(to);
+ }
+
+ @Override
+ protected boolean canUseChunk(final ChunkAccess chunk) {
+ return chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect());
+ }
+
+ @Override
+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) {
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+ if (nibble != null) {
+ // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically
+ // because a block was removed - which can decrease light. with sky data, block breaking can only result
+ // in increases, and thus the existing sky block check will actually correctly propagate light through
+ // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove
+ // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running
+ // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence
+ // of vanilla data management we "hide" them.
+ nibble.setHidden();
+ }
+ }
+
+ @Override
+ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) {
+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) {
+ return;
+ }
+
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+ if (nibble == null) {
+ if (!initRemovedNibbles) {
+ throw new IllegalStateException();
+ } else {
+ this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray());
+ }
+ } else {
+ nibble.setNonNull();
+ }
+ }
+
+ @Override
+ protected final void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) {
+ // blocks can change opacity
+ // blocks can change emitted light
+ // blocks can change direction of propagation
+
+ final int encodeOffset = this.coordinateOffset;
+ final int emittedMask = this.emittedLightMask;
+
+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
+ final BlockState blockState = this.getBlockState(worldX, worldY, worldZ);
+ final int emittedLevel = blockState.getLightEmission() & emittedMask;
+
+ this.setLightLevel(worldX, worldY, worldZ, emittedLevel);
+ // this accounts for change in emitted light that would cause an increase
+ if (emittedLevel != 0) {
+ this.appendToIncreaseQueue(
+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (emittedLevel & 0xFL) << (6 + 6 + 16)
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0)
+ );
+ }
+ // this also accounts for a change in emitted light that would cause a decrease
+ // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa)
+ // as it checks all neighbours (even if current level is 0)
+ this.appendToDecreaseQueue(
+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (currentLevel & 0xFL) << (6 + 6 + 16)
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ // always keep sided transparent false here, new block might be conditionally transparent which would
+ // prevent us from decreasing sources in the directions where the new block is opaque
+ // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always
+ // catch that and fix it.
+ );
+ // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block
+ }
+
+ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
+ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos();
+
+ @Override
+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
+ final int expect) {
+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
+ int level = centerState.getLightEmission() & 0xF;
+
+ if (level >= (15 - 1) || level > expect) {
+ return level;
+ }
+
+ final int sectionOffset = this.chunkSectionIndexOffset;
+ final BlockState conditionallyOpaqueState;
+ int opacity = ((StarlightAbstractBlockState)centerState).starlight$getOpacityIfCached();
+
+ if (opacity == -1) {
+ this.recalcCenterPos.set(worldX, worldY, worldZ);
+ opacity = centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos);
+ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) {
+ conditionallyOpaqueState = centerState;
+ } else {
+ conditionallyOpaqueState = null;
+ }
+ } else if (opacity >= 15) {
+ return level;
+ } else {
+ conditionallyOpaqueState = null;
+ }
+ opacity = Math.max(1, opacity);
+
+ for (final AxisDirection direction : AXIS_DIRECTIONS) {
+ final int offX = worldX + direction.x;
+ final int offY = worldY + direction.y;
+ final int offZ = worldZ + direction.z;
+
+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
+
+ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8));
+
+ if ((neighbourLevel - 1) <= level) {
+ // don't need to test transparency, we know it wont affect the result.
+ continue;
+ }
+
+ final BlockState neighbourState = this.getBlockState(offX, offY, offZ);
+ if (((StarlightAbstractBlockState)neighbourState).starlight$isConditionallyFullOpaque()) {
+ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
+ // we don't read the blockstate because most of the time this is false, so using the faster
+ // known transparency lookup results in a net win
+ this.recalcNeighbourPos.set(offX, offY, offZ);
+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms);
+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms);
+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) {
+ // not allowed to propagate
+ continue;
+ }
+ }
+
+ // passed transparency,
+
+ final int calculated = neighbourLevel - opacity;
+ level = Math.max(calculated, level);
+ if (level > expect) {
+ return level;
+ }
+ }
+
+ return level;
+ }
+
+ @Override
+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions) {
+ for (final BlockPos pos : positions) {
+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
+ }
+
+ this.performLightDecrease(lightAccess);
+ }
+
+ protected List<BlockPos> getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) {
+ final List<BlockPos> sources = new ArrayList<>();
+
+ final int offX = chunk.getPos().x << 4;
+ final int offZ = chunk.getPos().z << 4;
+
+ final LevelChunkSection[] sections = chunk.getSections();
+ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) {
+ final LevelChunkSection section = sections[sectionY - this.minSection];
+ if (section == null || section.hasOnlyAir()) {
+ // no sources in empty sections
+ continue;
+ }
+ if (!section.maybeHas((final BlockState state) -> {
+ return state.getLightEmission() > 0;
+ })) {
+ // no light sources in palette
+ continue;
+ }
+ final PalettedContainer<BlockState> states = section.states;
+ final int offY = sectionY << 4;
+
+ for (int index = 0; index < (16 * 16 * 16); ++index) {
+ final BlockState state = states.get(index);
+ if (state.getLightEmission() <= 0) {
+ continue;
+ }
+
+ // index = x | (z << 4) | (y << 8)
+ sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15)));
+ }
+ }
+
+ return sources;
+ }
+
+ @Override
+ public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
+ // setup sources
+ final int emittedMask = this.emittedLightMask;
+ final List<BlockPos> positions = this.getSources(lightAccess, chunk);
+ for (int i = 0, len = positions.size(); i < len; ++i) {
+ final BlockPos pos = positions.get(i);
+ final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ());
+ final int emittedLight = blockState.getLightEmission() & emittedMask;
+
+ if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) {
+ // some other source is brighter
+ continue;
+ }
+
+ this.appendToIncreaseQueue(
+ ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (emittedLight & 0xFL) << (6 + 6 + 16)
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0)
+ );
+
+
+ // propagation wont set this for us
+ this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight);
+ }
+
+ if (needsEdgeChecks) {
+ // not required to propagate here, but this will reduce the hit of the edge checks
+ this.performLightIncrease(lightAccess);
+
+ // verify neighbour edges
+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection);
+ } else {
+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection);
+
+ this.performLightIncrease(lightAccess);
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java
new file mode 100644
index 0000000000000000000000000000000000000000..4ca68a903e67606fc4ef0bfa9862a73797121c8b
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java
@@ -0,0 +1,440 @@
+package ca.spottedleaf.moonrise.patches.starlight.light;
+
+import net.minecraft.world.level.chunk.DataLayer;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+
+// SWMR -> Single Writer Multi Reader Nibble Array
+public final class SWMRNibbleArray {
+
+ /*
+ * Null nibble - nibble does not exist, and should not be written to. Just like vanilla - null
+ * nibbles are always 0 - and they are never written to directly. Only initialised/uninitialised
+ * nibbles can be written to.
+ *
+ * Uninitialised nibble - They are all 0, but the backing array isn't initialised.
+ *
+ * Initialised nibble - Has light data.
+ */
+
+ protected static final int INIT_STATE_NULL = 0; // null
+ protected static final int INIT_STATE_UNINIT = 1; // uninitialised
+ protected static final int INIT_STATE_INIT = 2; // initialised
+ protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL
+
+ public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block
+ // this allows us to maintain only 1 byte array when we're not updating
+ static final ThreadLocal<ArrayDeque<byte[]>> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new);
+
+ private static byte[] allocateBytes() {
+ final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst();
+ if (inPool != null) {
+ return inPool;
+ }
+
+ return new byte[ARRAY_SIZE];
+ }
+
+ private static void freeBytes(final byte[] bytes) {
+ WORKING_BYTES_POOL.get().addFirst(bytes);
+ }
+
+ public static SWMRNibbleArray fromVanilla(final DataLayer nibble) {
+ if (nibble == null) {
+ return new SWMRNibbleArray(null, true);
+ } else if (nibble.isEmpty()) {
+ return new SWMRNibbleArray();
+ } else {
+ return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later
+ }
+ }
+
+ protected int stateUpdating;
+ protected volatile int stateVisible;
+
+ protected byte[] storageUpdating;
+ protected boolean updatingDirty; // only returns whether storageUpdating is dirty
+ protected volatile byte[] storageVisible;
+
+ public SWMRNibbleArray() {
+ this(null, false); // lazy init
+ }
+
+ public SWMRNibbleArray(final byte[] bytes) {
+ this(bytes, false);
+ }
+
+ public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) {
+ if (bytes != null && bytes.length != ARRAY_SIZE) {
+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length);
+ }
+ this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT;
+ this.storageUpdating = this.storageVisible = bytes;
+ }
+
+ public SWMRNibbleArray(final byte[] bytes, final int state) {
+ if (bytes != null && bytes.length != ARRAY_SIZE) {
+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length);
+ }
+ if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) {
+ throw new IllegalArgumentException("Data cannot be null and have state be initialised");
+ }
+ this.stateUpdating = this.stateVisible = state;
+ this.storageUpdating = this.storageVisible = bytes;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("State: ");
+ switch (this.stateVisible) {
+ case INIT_STATE_NULL:
+ stringBuilder.append("null");
+ break;
+ case INIT_STATE_UNINIT:
+ stringBuilder.append("uninitialised");
+ break;
+ case INIT_STATE_INIT:
+ stringBuilder.append("initialised");
+ break;
+ case INIT_STATE_HIDDEN:
+ stringBuilder.append("hidden");
+ break;
+ default:
+ stringBuilder.append("unknown");
+ break;
+ }
+ stringBuilder.append("\nData:\n");
+
+ final byte[] data = this.storageVisible;
+ if (data != null) {
+ for (int i = 0; i < 4096; ++i) {
+ // Copied from NibbleArray#toString
+ final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF);
+
+ stringBuilder.append(Integer.toHexString(level));
+ if ((i & 15) == 15) {
+ stringBuilder.append("\n");
+ }
+
+ if ((i & 255) == 255) {
+ stringBuilder.append("\n");
+ }
+ }
+ } else {
+ stringBuilder.append("null");
+ }
+
+ return stringBuilder.toString();
+ }
+
+ public SaveState getSaveState() {
+ synchronized (this) {
+ final int state = this.stateVisible;
+ final byte[] data = this.storageVisible;
+ if (state == INIT_STATE_NULL) {
+ return null;
+ }
+ if (state == INIT_STATE_UNINIT) {
+ return new SaveState(null, state);
+ }
+ final boolean zero = isAllZero(data);
+ if (zero) {
+ return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null;
+ } else {
+ return new SaveState(data.clone(), state);
+ }
+ }
+ }
+
+ protected static boolean isAllZero(final byte[] data) {
+ for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) {
+ byte whole = data[i << 4];
+
+ for (int k = 1; k < (1 << 4); ++k) {
+ whole |= data[(i << 4) | k];
+ }
+
+ if (whole != 0) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // operation type: updating on src, updating on other
+ public void extrudeLower(final SWMRNibbleArray other) {
+ if (other.stateUpdating == INIT_STATE_NULL) {
+ throw new IllegalArgumentException();
+ }
+
+ if (other.storageUpdating == null) {
+ this.setUninitialised();
+ return;
+ }
+
+ final byte[] src = other.storageUpdating;
+ final byte[] into;
+
+ if (!this.updatingDirty) {
+ if (this.storageUpdating != null) {
+ into = this.storageUpdating = allocateBytes();
+ } else {
+ this.storageUpdating = into = allocateBytes();
+ this.stateUpdating = INIT_STATE_INIT;
+ }
+ this.updatingDirty = true;
+ } else {
+ into = this.storageUpdating;
+ }
+
+ final int start = 0;
+ final int end = (15 | (15 << 4)) >>> 1;
+
+ /* x | (z << 4) | (y << 8) */
+ for (int y = 0; y <= 15; ++y) {
+ System.arraycopy(src, start, into, y << (8 - 1), end - start + 1);
+ }
+ }
+
+ // operation type: updating
+ public void setFull() {
+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
+ this.stateUpdating = INIT_STATE_INIT;
+ }
+ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1);
+ this.updatingDirty = true;
+ }
+
+ // operation type: updating
+ public void setZero() {
+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
+ this.stateUpdating = INIT_STATE_INIT;
+ }
+ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0);
+ this.updatingDirty = true;
+ }
+
+ // operation type: updating
+ public void setNonNull() {
+ if (this.stateUpdating == INIT_STATE_HIDDEN) {
+ this.stateUpdating = INIT_STATE_INIT;
+ return;
+ }
+ if (this.stateUpdating != INIT_STATE_NULL) {
+ return;
+ }
+ this.stateUpdating = INIT_STATE_UNINIT;
+ }
+
+ // operation type: updating
+ public void setNull() {
+ this.stateUpdating = INIT_STATE_NULL;
+ if (this.updatingDirty && this.storageUpdating != null) {
+ freeBytes(this.storageUpdating);
+ }
+ this.storageUpdating = null;
+ this.updatingDirty = false;
+ }
+
+ // operation type: updating
+ public void setUninitialised() {
+ this.stateUpdating = INIT_STATE_UNINIT;
+ if (this.storageUpdating != null && this.updatingDirty) {
+ freeBytes(this.storageUpdating);
+ }
+ this.storageUpdating = null;
+ this.updatingDirty = false;
+ }
+
+ // operation type: updating
+ public void setHidden() {
+ if (this.stateUpdating == INIT_STATE_HIDDEN) {
+ return;
+ }
+ if (this.stateUpdating != INIT_STATE_INIT) {
+ this.setNull();
+ } else {
+ this.stateUpdating = INIT_STATE_HIDDEN;
+ }
+ }
+
+ // operation type: updating
+ public boolean isDirty() {
+ return this.stateUpdating != this.stateVisible || this.updatingDirty;
+ }
+
+ // operation type: updating
+ public boolean isNullNibbleUpdating() {
+ return this.stateUpdating == INIT_STATE_NULL;
+ }
+
+ // operation type: visible
+ public boolean isNullNibbleVisible() {
+ return this.stateVisible == INIT_STATE_NULL;
+ }
+
+ // opeartion type: updating
+ public boolean isUninitialisedUpdating() {
+ return this.stateUpdating == INIT_STATE_UNINIT;
+ }
+
+ // operation type: visible
+ public boolean isUninitialisedVisible() {
+ return this.stateVisible == INIT_STATE_UNINIT;
+ }
+
+ // operation type: updating
+ public boolean isInitialisedUpdating() {
+ return this.stateUpdating == INIT_STATE_INIT;
+ }
+
+ // operation type: visible
+ public boolean isInitialisedVisible() {
+ return this.stateVisible == INIT_STATE_INIT;
+ }
+
+ // operation type: updating
+ public boolean isHiddenUpdating() {
+ return this.stateUpdating == INIT_STATE_HIDDEN;
+ }
+
+ // operation type: updating
+ public boolean isHiddenVisible() {
+ return this.stateVisible == INIT_STATE_HIDDEN;
+ }
+
+ // operation type: updating
+ protected void swapUpdatingAndMarkDirty() {
+ if (this.updatingDirty) {
+ return;
+ }
+
+ if (this.storageUpdating == null) {
+ this.storageUpdating = allocateBytes();
+ Arrays.fill(this.storageUpdating, (byte)0);
+ } else {
+ System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE);
+ }
+
+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
+ this.stateUpdating = INIT_STATE_INIT;
+ }
+ this.updatingDirty = true;
+ }
+
+ // operation type: updating
+ public boolean updateVisible() {
+ if (!this.isDirty()) {
+ return false;
+ }
+
+ synchronized (this) {
+ if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) {
+ this.storageVisible = null;
+ } else {
+ if (this.storageVisible == null) {
+ this.storageVisible = this.storageUpdating.clone();
+ } else {
+ if (this.storageUpdating != this.storageVisible) {
+ System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE);
+ }
+ }
+
+ if (this.storageUpdating != this.storageVisible) {
+ freeBytes(this.storageUpdating);
+ }
+ this.storageUpdating = this.storageVisible;
+ }
+ this.updatingDirty = false;
+ this.stateVisible = this.stateUpdating;
+ }
+
+ return true;
+ }
+
+ // operation type: visible
+ public DataLayer toVanillaNibble() {
+ synchronized (this) {
+ switch (this.stateVisible) {
+ case INIT_STATE_HIDDEN:
+ case INIT_STATE_NULL:
+ return null;
+ case INIT_STATE_UNINIT:
+ return new DataLayer();
+ case INIT_STATE_INIT:
+ return new DataLayer(this.storageVisible.clone());
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ /* x | (z << 4) | (y << 8) */
+
+ // operation type: updating
+ public int getUpdating(final int x, final int y, final int z) {
+ return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8));
+ }
+
+ // operation type: updating
+ public int getUpdating(final int index) {
+ // indices range from 0 -> 4096
+ final byte[] bytes = this.storageUpdating;
+ if (bytes == null) {
+ return 0;
+ }
+ final byte value = bytes[index >>> 1];
+
+ // if we are an even index, we want lower 4 bits
+ // if we are an odd index, we want upper 4 bits
+ return ((value >>> ((index & 1) << 2)) & 0xF);
+ }
+
+ // operation type: visible
+ public int getVisible(final int x, final int y, final int z) {
+ return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8));
+ }
+
+ // operation type: visible
+ public int getVisible(final int index) {
+ // indices range from 0 -> 4096
+ final byte[] visibleBytes = this.storageVisible;
+ if (visibleBytes == null) {
+ return 0;
+ }
+ final byte value = visibleBytes[index >>> 1];
+
+ // if we are an even index, we want lower 4 bits
+ // if we are an odd index, we want upper 4 bits
+ return ((value >>> ((index & 1) << 2)) & 0xF);
+ }
+
+ // operation type: updating
+ public void set(final int x, final int y, final int z, final int value) {
+ this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value);
+ }
+
+ // operation type: updating
+ public void set(final int index, final int value) {
+ if (!this.updatingDirty) {
+ this.swapUpdatingAndMarkDirty();
+ }
+ final int shift = (index & 1) << 2;
+ final int i = index >>> 1;
+
+ this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift));
+ }
+
+ public static final class SaveState {
+
+ public final byte[] data;
+ public final int state;
+
+ public SaveState(final byte[] data, final int state) {
+ this.data = data;
+ this.state = state;
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..fdbc015f498164c9d2c578cd84a73def568142a4
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
@@ -0,0 +1,711 @@
+package ca.spottedleaf.moonrise.patches.starlight.light;
+
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState;
+import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
+import it.unimi.dsi.fastutil.shorts.ShortCollection;
+import it.unimi.dsi.fastutil.shorts.ShortIterator;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.BlockGetter;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunkSection;
+import net.minecraft.world.level.chunk.LightChunkGetter;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.phys.shapes.Shapes;
+import net.minecraft.world.phys.shapes.VoxelShape;
+import java.util.Arrays;
+import java.util.Set;
+
+public final class SkyStarLightEngine extends StarLightEngine {
+
+ /*
+ Specification for managing the initialisation and de-initialisation of skylight nibble arrays:
+
+ Skylight nibble initialisation requires that non-empty chunk sections have 1 radius nibbles non-null.
+
+ This presents some problems, as vanilla is only guaranteed to have 0 radius neighbours loaded when editing blocks.
+ However starlight fixes this so that it has 1 radius loaded. Still, we don't actually have guarantees
+ that we have the necessary chunks loaded to de-initialise neighbour sections (but we do have enough to de-initialise
+ our own) - we need a radius of 2 to de-initialise neighbour nibbles.
+ How do we solve this?
+
+ Each chunk will store the last known "emptiness" of sections for each of their 1 radius neighbour chunk sections.
+ If the chunk does not have full data, then its nibbles are NOT de-initialised. This is because obviously the
+ chunk did not go through the light stage yet - or its neighbours are not lit. In either case, once the last
+ known "emptiness" of neighbouring sections is filled with data, the chunk will run a full check of the data
+ to see if any of its nibbles need to be de-initialised.
+
+ The emptiness map allows us to de-initialise neighbour nibbles if the neighbour has it filled with data,
+ and if it doesn't have data then we know it will correctly de-initialise once it fills up.
+
+ Unlike vanilla, we store whether nibbles are uninitialised on disk - so we don't need any dumb hacking
+ around those.
+ */
+
+ protected final int[] heightMapBlockChange = new int[16 * 16];
+ {
+ Arrays.fill(this.heightMapBlockChange, Integer.MIN_VALUE); // clear heightmap
+ }
+
+ protected final boolean[] nullPropagationCheckCache;
+
+ public SkyStarLightEngine(final Level world) {
+ super(true, world);
+ this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)];
+ }
+
+ @Override
+ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) {
+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) {
+ return;
+ }
+ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+ if (nibble == null) {
+ if (!initRemovedNibbles) {
+ throw new IllegalStateException();
+ } else {
+ this.setNibbleInCache(chunkX, chunkY, chunkZ, nibble = new SWMRNibbleArray(null, true));
+ }
+ }
+ this.initNibble(nibble, chunkX, chunkY, chunkZ, extrude);
+ }
+
+ @Override
+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) {
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+ if (nibble != null) {
+ nibble.setNull();
+ }
+ }
+
+ protected final void initNibble(final SWMRNibbleArray currNibble, final int chunkX, final int chunkY, final int chunkZ, final boolean extrude) {
+ if (!currNibble.isNullNibbleUpdating()) {
+ // already initialised
+ return;
+ }
+
+ final boolean[] emptinessMap = this.getEmptinessMap(chunkX, chunkZ);
+
+ // are we above this chunk's lowest empty section?
+ int lowestY = this.minLightSection - 1;
+ for (int currY = this.maxSection; currY >= this.minSection; --currY) {
+ if (emptinessMap == null) {
+ // cannot delay nibble init for lit chunks, as we need to init to propagate into them.
+ final LevelChunkSection current = this.getChunkSection(chunkX, currY, chunkZ);
+ if (current == null || current.hasOnlyAir()) {
+ continue;
+ }
+ } else {
+ if (emptinessMap[currY - this.minSection]) {
+ continue;
+ }
+ }
+
+ // should always be full lit here
+ lowestY = currY;
+ break;
+ }
+
+ if (chunkY > lowestY) {
+ // we need to set this one to full
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+ nibble.setNonNull();
+ nibble.setFull();
+ return;
+ }
+
+ if (extrude) {
+ // this nibble is going to depend solely on the skylight data above it
+ // find first non-null data above (there does exist one, as we just found it above)
+ for (int currY = chunkY + 1; currY <= this.maxLightSection; ++currY) {
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, currY, chunkZ);
+ if (nibble != null && !nibble.isNullNibbleUpdating()) {
+ currNibble.setNonNull();
+ currNibble.extrudeLower(nibble);
+ break;
+ }
+ }
+ } else {
+ currNibble.setNonNull();
+ }
+ }
+
+ protected final void rewriteNibbleCacheForSkylight(final ChunkAccess chunk) {
+ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) {
+ final SWMRNibbleArray nibble = this.nibbleCache[index];
+ if (nibble != null && nibble.isNullNibbleUpdating()) {
+ // stop propagation in these areas
+ this.nibbleCache[index] = null;
+ nibble.updateVisible();
+ }
+ }
+ }
+
+ // rets whether neighbours were init'd
+
+ protected final boolean checkNullSection(final int chunkX, final int chunkY, final int chunkZ,
+ final boolean extrudeInitialised) {
+ // null chunk sections may have nibble neighbours in the horizontal 1 radius that are
+ // non-null. Propagation to these neighbours is necessary.
+ // What makes this easy is we know none of these neighbours are non-empty (otherwise
+ // this nibble would be initialised). So, we don't have to initialise
+ // the neighbours in the full 1 radius, because there's no worry that any "paths"
+ // to the neighbours on this horizontal plane are blocked.
+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.nullPropagationCheckCache[chunkY - this.minLightSection]) {
+ return false;
+ }
+ this.nullPropagationCheckCache[chunkY - this.minLightSection] = true;
+
+ // check horizontal neighbours
+ boolean needInitNeighbours = false;
+ neighbour_search:
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, chunkY, dz + chunkZ);
+ if (nibble != null && !nibble.isNullNibbleUpdating()) {
+ needInitNeighbours = true;
+ break neighbour_search;
+ }
+ }
+ }
+
+ if (needInitNeighbours) {
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ this.initNibble(dx + chunkX, chunkY, dz + chunkZ, (dx | dz) == 0 ? extrudeInitialised : true, true);
+ }
+ }
+ }
+
+ return needInitNeighbours;
+ }
+
+ protected final int getLightLevelExtruded(final int worldX, final int worldY, final int worldZ) {
+ final int chunkX = worldX >> 4;
+ int chunkY = worldY >> 4;
+ final int chunkZ = worldZ >> 4;
+
+ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+ if (nibble != null) {
+ return nibble.getUpdating(worldX, worldY, worldZ);
+ }
+
+ for (;;) {
+ if (++chunkY > this.maxLightSection) {
+ return 15;
+ }
+
+ nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+
+ if (nibble != null) {
+ return nibble.getUpdating(worldX, 0, worldZ);
+ }
+ }
+ }
+
+ @Override
+ protected boolean[] getEmptinessMap(final ChunkAccess chunk) {
+ return ((StarlightChunk)chunk).starlight$getSkyEmptinessMap();
+ }
+
+ @Override
+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) {
+ ((StarlightChunk)chunk).starlight$setSkyEmptinessMap(to);
+ }
+
+ @Override
+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) {
+ return ((StarlightChunk)chunk).starlight$getSkyNibbles();
+ }
+
+ @Override
+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) {
+ ((StarlightChunk)chunk).starlight$setSkyNibbles(to);
+ }
+
+ @Override
+ protected boolean canUseChunk(final ChunkAccess chunk) {
+ // can only use chunks for sky stuff if their sections have been init'd
+ return chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect());
+ }
+
+ @Override
+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection,
+ final int toSection) {
+ Arrays.fill(this.nullPropagationCheckCache, false);
+ this.rewriteNibbleCacheForSkylight(chunk);
+ final int chunkX = chunk.getPos().x;
+ final int chunkZ = chunk.getPos().z;
+ for (int y = toSection; y >= fromSection; --y) {
+ this.checkNullSection(chunkX, y, chunkZ, true);
+ }
+
+ super.checkChunkEdges(lightAccess, chunk, fromSection, toSection);
+ }
+
+ @Override
+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) {
+ Arrays.fill(this.nullPropagationCheckCache, false);
+ this.rewriteNibbleCacheForSkylight(chunk);
+ final int chunkX = chunk.getPos().x;
+ final int chunkZ = chunk.getPos().z;
+ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) {
+ final int y = (int)iterator.nextShort();
+ this.checkNullSection(chunkX, y, chunkZ, true);
+ }
+
+ super.checkChunkEdges(lightAccess, chunk, sections);
+ }
+
+ @Override
+ protected void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) {
+ // blocks can change opacity
+ // blocks can change direction of propagation
+
+ // same logic applies from BlockStarLightEngine#checkBlock
+
+ final int encodeOffset = this.coordinateOffset;
+
+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
+
+ if (currentLevel == 15) {
+ // must re-propagate clobbered source
+ this.appendToIncreaseQueue(
+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (currentLevel & 0xFL) << (6 + 6 + 16)
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent
+ );
+ } else {
+ this.setLightLevel(worldX, worldY, worldZ, 0);
+ }
+
+ this.appendToDecreaseQueue(
+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (currentLevel & 0xFL) << (6 + 6 + 16)
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ );
+ }
+
+ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
+ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos();
+
+ @Override
+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
+ final int expect) {
+ if (expect == 15) {
+ return expect;
+ }
+
+ final int sectionOffset = this.chunkSectionIndexOffset;
+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
+ int opacity = ((StarlightAbstractBlockState)centerState).starlight$getOpacityIfCached();
+
+ final BlockState conditionallyOpaqueState;
+ if (opacity < 0) {
+ this.recalcCenterPos.set(worldX, worldY, worldZ);
+ opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos));
+ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) {
+ conditionallyOpaqueState = centerState;
+ } else {
+ conditionallyOpaqueState = null;
+ }
+ } else {
+ conditionallyOpaqueState = null;
+ opacity = Math.max(1, opacity);
+ }
+
+ int level = 0;
+
+ for (final AxisDirection direction : AXIS_DIRECTIONS) {
+ final int offX = worldX + direction.x;
+ final int offY = worldY + direction.y;
+ final int offZ = worldZ + direction.z;
+
+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
+
+ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8));
+
+ if ((neighbourLevel - 1) <= level) {
+ // don't need to test transparency, we know it wont affect the result.
+ continue;
+ }
+
+ final BlockState neighbourState = this.getBlockState(offX, offY, offZ);
+
+ if (((StarlightAbstractBlockState)neighbourState).starlight$isConditionallyFullOpaque()) {
+ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
+ // we don't read the blockstate because most of the time this is false, so using the faster
+ // known transparency lookup results in a net win
+ this.recalcNeighbourPos.set(offX, offY, offZ);
+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms);
+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms);
+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) {
+ // not allowed to propagate
+ continue;
+ }
+ }
+
+ final int calculated = neighbourLevel - opacity;
+ level = Math.max(calculated, level);
+ if (level > expect) {
+ return level;
+ }
+ }
+
+ return level;
+ }
+
+ @Override
+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions) {
+ this.rewriteNibbleCacheForSkylight(atChunk);
+ Arrays.fill(this.nullPropagationCheckCache, false);
+
+ final BlockGetter world = lightAccess.getLevel();
+ final int chunkX = atChunk.getPos().x;
+ final int chunkZ = atChunk.getPos().z;
+ final int heightMapOffset = chunkX * -16 + (chunkZ * (-16 * 16));
+
+ // setup heightmap for changes
+ for (final BlockPos pos : positions) {
+ final int index = pos.getX() + (pos.getZ() << 4) + heightMapOffset;
+ final int curr = this.heightMapBlockChange[index];
+ if (pos.getY() > curr) {
+ this.heightMapBlockChange[index] = pos.getY();
+ }
+ }
+
+ // note: light sets are delayed while processing skylight source changes due to how
+ // nibbles are initialised, as we want to avoid clobbering nibble values so what when
+ // below nibbles are initialised they aren't reading from partially modified nibbles
+
+ // now we can recalculate the sources for the changed columns
+ for (int index = 0; index < (16 * 16); ++index) {
+ final int maxY = this.heightMapBlockChange[index];
+ if (maxY == Integer.MIN_VALUE) {
+ // not changed
+ continue;
+ }
+ this.heightMapBlockChange[index] = Integer.MIN_VALUE; // restore default for next caller
+
+ final int columnX = (index & 15) | (chunkX << 4);
+ final int columnZ = (index >>> 4) | (chunkZ << 4);
+
+ // try and propagate from the above y
+ // delay light set until after processing all sources to setup
+ final int maxPropagationY = this.tryPropagateSkylight(world, columnX, maxY, columnZ, true, true);
+
+ // maxPropagationY is now the highest block that could not be propagated to
+
+ // remove all sources below that are 15
+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection;
+ final int encodeOffset = this.coordinateOffset;
+
+ if (this.getLightLevelExtruded(columnX, maxPropagationY, columnZ) == 15) {
+ // ensure section is checked
+ this.checkNullSection(columnX >> 4, maxPropagationY >> 4, columnZ >> 4, true);
+
+ for (int currY = maxPropagationY; currY >= (this.minLightSection << 4); --currY) {
+ if ((currY & 15) == 15) {
+ // ensure section is checked
+ this.checkNullSection(columnX >> 4, (currY >> 4), columnZ >> 4, true);
+ }
+
+ // ensure section below is always checked
+ final SWMRNibbleArray nibble = this.getNibbleFromCache(columnX >> 4, currY >> 4, columnZ >> 4);
+ if (nibble == null) {
+ // advance currY to the the top of the section below
+ currY = (currY) & (~15);
+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually
+ // end up there
+ continue;
+ }
+
+ if (nibble.getUpdating(columnX, currY, columnZ) != 15) {
+ break;
+ }
+
+ // delay light set until after processing all sources to setup
+ this.appendToDecreaseQueue(
+ ((columnX + (columnZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (15L << (6 + 6 + 16))
+ | (propagateDirection << (6 + 6 + 16 + 4))
+ // do not set transparent blocks for the same reason we don't in the checkBlock method
+ );
+ }
+ }
+ }
+
+ // delayed light sets are processed here, and must be processed before checkBlock as checkBlock reads
+ // immediate light value
+ this.processDelayedIncreases();
+ this.processDelayedDecreases();
+
+ for (final BlockPos pos : positions) {
+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
+ }
+
+ this.performLightDecrease(lightAccess);
+ }
+
+ protected final int[] heightMapGen = new int[32 * 32];
+
+ @Override
+ protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
+ this.rewriteNibbleCacheForSkylight(chunk);
+ Arrays.fill(this.nullPropagationCheckCache, false);
+
+ final BlockGetter world = lightAccess.getLevel();
+ final ChunkPos chunkPos = chunk.getPos();
+ final int chunkX = chunkPos.x;
+ final int chunkZ = chunkPos.z;
+
+ final LevelChunkSection[] sections = chunk.getSections();
+
+ int highestNonEmptySection = this.maxSection;
+ while (highestNonEmptySection == (this.minSection - 1) ||
+ sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].hasOnlyAir()) {
+ this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false);
+ // try propagate FULL to neighbours
+
+ // check neighbours to see if we need to propagate into them
+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
+ final int neighbourX = chunkX + direction.x;
+ final int neighbourZ = chunkZ + direction.z;
+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ);
+ if (neighbourNibble == null) {
+ // unloaded neighbour
+ // most of the time we fall here
+ continue;
+ }
+
+ // it looks like we need to propagate into the neighbour
+
+ final int incX;
+ final int incZ;
+ final int startX;
+ final int startZ;
+
+ if (direction.x != 0) {
+ // x direction
+ incX = 0;
+ incZ = 1;
+
+ if (direction.x < 0) {
+ // negative
+ startX = chunkX << 4;
+ } else {
+ startX = chunkX << 4 | 15;
+ }
+ startZ = chunkZ << 4;
+ } else {
+ // z direction
+ incX = 1;
+ incZ = 0;
+
+ if (direction.z < 0) {
+ // negative
+ startZ = chunkZ << 4;
+ } else {
+ startZ = chunkZ << 4 | 15;
+ }
+ startX = chunkX << 4;
+ }
+
+ final int encodeOffset = this.coordinateOffset;
+ final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction
+
+ for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) {
+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
+ this.appendToIncreaseQueue(
+ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
+ | (propagateDirection << (6 + 6 + 16 + 4))
+ // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY)
+ );
+ }
+ }
+ }
+
+ if (highestNonEmptySection-- == (this.minSection - 1)) {
+ break;
+ }
+ }
+
+ if (highestNonEmptySection >= this.minSection) {
+ // fill out our other sources
+ final int minX = chunkPos.x << 4;
+ final int maxX = chunkPos.x << 4 | 15;
+ final int minZ = chunkPos.z << 4;
+ final int maxZ = chunkPos.z << 4 | 15;
+ final int startY = highestNonEmptySection << 4 | 15;
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
+ for (int currX = minX; currX <= maxX; ++currX) {
+ this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false);
+ }
+ }
+ } // else: apparently the chunk is empty
+
+ if (needsEdgeChecks) {
+ // not required to propagate here, but this will reduce the hit of the edge checks
+ this.performLightIncrease(lightAccess);
+
+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) {
+ this.checkNullSection(chunkX, y, chunkZ, false);
+ }
+ // no need to rewrite the nibble cache again
+ super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection);
+ } else {
+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) {
+ this.checkNullSection(chunkX, y, chunkZ, false);
+ }
+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection);
+
+ this.performLightIncrease(lightAccess);
+ }
+ }
+
+ protected final void processDelayedIncreases() {
+ // copied from performLightIncrease
+ final long[] queue = this.increaseQueue;
+ final int decodeOffsetX = -this.encodeOffsetX;
+ final int decodeOffsetY = -this.encodeOffsetY;
+ final int decodeOffsetZ = -this.encodeOffsetZ;
+
+ for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) {
+ final long queueValue = queue[i];
+
+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF);
+
+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel);
+ }
+ }
+
+ protected final void processDelayedDecreases() {
+ // copied from performLightDecrease
+ final long[] queue = this.decreaseQueue;
+ final int decodeOffsetX = -this.encodeOffsetX;
+ final int decodeOffsetY = -this.encodeOffsetY;
+ final int decodeOffsetZ = -this.encodeOffsetZ;
+
+ for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) {
+ final long queueValue = queue[i];
+
+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
+
+ this.setLightLevel(posX, posY, posZ, 0);
+ }
+ }
+
+ // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays
+ // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so
+ // clobbering the light values will result in broken propagation)
+ protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ,
+ final boolean extrudeInitialised, final boolean delayLightSet) {
+ final BlockPos.MutableBlockPos mutablePos = this.mutablePos3;
+ final int encodeOffset = this.coordinateOffset;
+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards.
+
+ if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) {
+ return startY;
+ }
+
+ // ensure this section is always checked
+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
+
+ BlockState above = this.getBlockState(worldX, startY + 1, worldZ);
+
+ for (;startY >= (this.minLightSection << 4); --startY) {
+ if ((startY & 15) == 15) {
+ // ensure this section is always checked
+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
+ }
+ final BlockState current = this.getBlockState(worldX, startY, worldZ);
+
+ final VoxelShape fromShape;
+ if (((StarlightAbstractBlockState)above).starlight$isConditionallyFullOpaque()) {
+ this.mutablePos2.set(worldX, startY + 1, worldZ);
+ fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms);
+ if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
+ // above wont let us propagate
+ break;
+ }
+ } else {
+ fromShape = Shapes.empty();
+ }
+
+ final int opacityIfCached = ((StarlightAbstractBlockState)current).starlight$getOpacityIfCached();
+ // does light propagate from the top down?
+ if (opacityIfCached != -1) {
+ if (opacityIfCached != 0) {
+ // we cannot propagate 15 through this
+ break;
+ }
+ // most of the time it falls here.
+ // add to propagate
+ // light set delayed until we determine if this nibble section is null
+ this.appendToIncreaseQueue(
+ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
+ | (propagateDirection << (6 + 6 + 16 + 4))
+ );
+ } else {
+ mutablePos.set(worldX, startY, worldZ);
+ long flags = 0L;
+ if (((StarlightAbstractBlockState)current).starlight$isConditionallyFullOpaque()) {
+ final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms);
+
+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
+ // can't propagate here, we're done on this column.
+ break;
+ }
+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+ }
+
+ final int opacity = current.getLightBlock(world, mutablePos);
+ if (opacity > 0) {
+ // let the queued value (if any) handle it from here.
+ break;
+ }
+
+ // light set delayed until we determine if this nibble section is null
+ this.appendToIncreaseQueue(
+ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
+ | (propagateDirection << (6 + 6 + 16 + 4))
+ | flags
+ );
+ }
+
+ above = current;
+
+ if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) {
+ // we skip empty sections here, as this is just an easy way of making sure the above block
+ // can propagate through air.
+
+ // nothing can propagate in null sections, remove the queue entry for it
+ --this.increaseQueueInitialLength;
+
+ // advance currY to the the top of the section below
+ startY = (startY) & (~15);
+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually
+ // end up there
+
+ // make sure this is marked as AIR
+ above = AIR_BLOCK_STATE;
+ } else if (!delayLightSet) {
+ this.setLightLevel(worldX, startY, worldZ, 15);
+ }
+ }
+
+ return startY;
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017c58191e2
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
@@ -0,0 +1,1573 @@
+package ca.spottedleaf.moonrise.patches.starlight.light;
+
+import ca.spottedleaf.concurrentutil.util.IntegerUtil;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.shorts.ShortCollection;
+import it.unimi.dsi.fastutil.shorts.ShortIterator;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.core.SectionPos;
+import net.minecraft.world.level.BlockGetter;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.LevelHeightAccessor;
+import net.minecraft.world.level.LightLayer;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunkSection;
+import net.minecraft.world.level.chunk.LightChunkGetter;
+import net.minecraft.world.phys.shapes.Shapes;
+import net.minecraft.world.phys.shapes.VoxelShape;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+
+public abstract class StarLightEngine {
+
+ protected static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState();
+
+ protected static final AxisDirection[] DIRECTIONS = AxisDirection.values();
+ protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS;
+ protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] {
+ AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X,
+ AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z
+ };
+
+ protected static enum AxisDirection {
+
+ // Declaration order is important and relied upon. Do not change without modifying propagation code.
+ POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0),
+ POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1),
+ POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0);
+
+ static {
+ POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X;
+ POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z;
+ POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y;
+ }
+
+ protected AxisDirection opposite;
+
+ public final int x;
+ public final int y;
+ public final int z;
+ public final Direction nms;
+ public final long everythingButThisDirection;
+ public final long everythingButTheOppositeDirection;
+
+ AxisDirection(final int x, final int y, final int z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.nms = Direction.fromDelta(x, y, z);
+ this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal()));
+ // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction.
+ this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1)));
+ }
+
+ public AxisDirection getOpposite() {
+ return this.opposite;
+ }
+ }
+
+ // I'd like to thank https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1
+ // for explaining how light propagates via breadth-first search
+
+ // While the above is a good start to understanding the general idea of what the general principles are, it's not
+ // exactly how the vanilla light engine should behave for minecraft.
+
+ // similar to the above, except the chunk section indices vary from [-1, 1], or [0, 2]
+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
+ // index = x + (z * 5) + (y * 25)
+ // null index indicates the chunk section doesn't exist (empty or out of bounds)
+ protected final LevelChunkSection[] sectionCache;
+
+ // the exact same as above, except for storing fast access to SWMRNibbleArray
+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
+ // index = x + (z * 5) + (y * 25)
+ protected final SWMRNibbleArray[] nibbleCache;
+
+ // the exact same as above, except for storing fast access to nibbles to call change callbacks for
+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
+ // index = x + (z * 5) + (y * 25)
+ protected final boolean[] notifyUpdateCache;
+
+ // always initialsed during start of lighting.
+ // index = x + (z * 5)
+ protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5];
+
+ // index = x + (z * 5)
+ protected final boolean[][] emptinessMapCache = new boolean[5 * 5][];
+
+ protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos();
+ protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos();
+ protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos();
+
+ protected int encodeOffsetX;
+ protected int encodeOffsetY;
+ protected int encodeOffsetZ;
+
+ protected int coordinateOffset;
+
+ protected int chunkOffsetX;
+ protected int chunkOffsetY;
+ protected int chunkOffsetZ;
+
+ protected int chunkIndexOffset;
+ protected int chunkSectionIndexOffset;
+
+ protected final boolean skylightPropagator;
+ protected final int emittedLightMask;
+ protected final boolean isClientSide;
+
+ protected final Level world;
+ protected final int minLightSection;
+ protected final int maxLightSection;
+ protected final int minSection;
+ protected final int maxSection;
+
+ protected StarLightEngine(final boolean skylightPropagator, final Level world) {
+ this.skylightPropagator = skylightPropagator;
+ this.emittedLightMask = skylightPropagator ? 0 : 0xF;
+ this.isClientSide = world.isClientSide;
+ this.world = world;
+ this.minLightSection = WorldUtil.getMinLightSection(world);
+ this.maxLightSection = WorldUtil.getMaxLightSection(world);
+ this.minSection = WorldUtil.getMinSection(world);
+ this.maxSection = WorldUtil.getMaxSection(world);
+
+ this.sectionCache = new LevelChunkSection[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
+ this.nibbleCache = new SWMRNibbleArray[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
+ this.notifyUpdateCache = new boolean[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
+ }
+
+ protected final void setupEncodeOffset(final int centerX, final int centerY, final int centerZ) {
+ // 31 = center + encodeOffset
+ this.encodeOffsetX = 31 - centerX;
+ this.encodeOffsetY = (-(this.minLightSection - 1) << 4); // we want 0 to be the smallest encoded value
+ this.encodeOffsetZ = 31 - centerZ;
+
+ // coordinateIndex = x | (z << 6) | (y << 12)
+ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << 6) + (this.encodeOffsetY << 12);
+
+ // 2 = (centerX >> 4) + chunkOffset
+ this.chunkOffsetX = 2 - (centerX >> 4);
+ this.chunkOffsetY = -(this.minLightSection - 1); // lowest should be 0
+ this.chunkOffsetZ = 2 - (centerZ >> 4);
+
+ // chunk index = x + (5 * z)
+ this.chunkIndexOffset = this.chunkOffsetX + (5 * this.chunkOffsetZ);
+
+ // chunk section index = x + (5 * z) + ((5*5) * y)
+ this.chunkSectionIndexOffset = this.chunkIndexOffset + ((5 * 5) * this.chunkOffsetY);
+ }
+
+ protected final void setupCaches(final LightChunkGetter chunkProvider, final int centerX, final int centerY, final int centerZ,
+ final boolean relaxed, final boolean tryToLoadChunksFor2Radius) {
+ final int centerChunkX = centerX >> 4;
+ final int centerChunkY = centerY >> 4;
+ final int centerChunkZ = centerZ >> 4;
+
+ this.setupEncodeOffset(centerChunkX * 16 + 7, centerChunkY * 16 + 7, centerChunkZ * 16 + 7);
+
+ final int radius = tryToLoadChunksFor2Radius ? 2 : 1;
+
+ for (int dz = -radius; dz <= radius; ++dz) {
+ for (int dx = -radius; dx <= radius; ++dx) {
+ final int cx = centerChunkX + dx;
+ final int cz = centerChunkZ + dz;
+ final boolean isTwoRadius = Math.max(IntegerUtil.branchlessAbs(dx), IntegerUtil.branchlessAbs(dz)) == 2;
+ final ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz);
+
+ if (chunk == null) {
+ if (relaxed | isTwoRadius) {
+ continue;
+ }
+ throw new IllegalArgumentException("Trying to propagate light update before 1 radius neighbours ready");
+ }
+
+ if (!this.canUseChunk(chunk)) {
+ continue;
+ }
+
+ this.setChunkInCache(cx, cz, chunk);
+ this.setEmptinessMapCache(cx, cz, this.getEmptinessMap(chunk));
+ if (!isTwoRadius) {
+ this.setBlocksForChunkInCache(cx, cz, chunk.getSections());
+ this.setNibblesForChunkInCache(cx, cz, this.getNibblesOnChunk(chunk));
+ }
+ }
+ }
+ }
+
+ protected final ChunkAccess getChunkInCache(final int chunkX, final int chunkZ) {
+ return this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
+ }
+
+ protected final void setChunkInCache(final int chunkX, final int chunkZ, final ChunkAccess chunk) {
+ this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk;
+ }
+
+ protected final LevelChunkSection getChunkSection(final int chunkX, final int chunkY, final int chunkZ) {
+ return this.sectionCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
+ }
+
+ protected final void setChunkSectionInCache(final int chunkX, final int chunkY, final int chunkZ, final LevelChunkSection section) {
+ this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section;
+ }
+
+ protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] sections) {
+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
+ this.setChunkSectionInCache(chunkX, cy, chunkZ,
+ sections == null ? null : (cy >= this.minSection && cy <= this.maxSection ? sections[cy - this.minSection] : null));
+ }
+ }
+
+ protected final SWMRNibbleArray getNibbleFromCache(final int chunkX, final int chunkY, final int chunkZ) {
+ return this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
+ }
+
+ protected final SWMRNibbleArray[] getNibblesForChunkFromCache(final int chunkX, final int chunkZ) {
+ final SWMRNibbleArray[] ret = new SWMRNibbleArray[this.maxLightSection - this.minLightSection + 1];
+
+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
+ ret[cy - this.minLightSection] = this.nibbleCache[chunkX + 5*chunkZ + (cy * (5 * 5)) + this.chunkSectionIndexOffset];
+ }
+
+ return ret;
+ }
+
+ protected final void setNibbleInCache(final int chunkX, final int chunkY, final int chunkZ, final SWMRNibbleArray nibble) {
+ this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset] = nibble;
+ }
+
+ protected final void setNibblesForChunkInCache(final int chunkX, final int chunkZ, final SWMRNibbleArray[] nibbles) {
+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
+ this.setNibbleInCache(chunkX, cy, chunkZ, nibbles == null ? null : nibbles[cy - this.minLightSection]);
+ }
+ }
+
+ protected final void updateVisible(final LightChunkGetter lightAccess) {
+ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) {
+ final SWMRNibbleArray nibble = this.nibbleCache[index];
+ if (!this.notifyUpdateCache[index] && (nibble == null || !nibble.isDirty())) {
+ continue;
+ }
+
+ final int chunkX = (index % 5) - this.chunkOffsetX;
+ final int chunkZ = ((index / 5) % 5) - this.chunkOffsetZ;
+ final int ySections = this.maxSection - this.minSection + 1;
+ final int chunkY = ((index / (5*5)) % (ySections + 2 + 2)) - this.chunkOffsetY;
+ if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) {
+ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, chunkY, chunkZ));
+ }
+ }
+ }
+
+ protected final void destroyCaches() {
+ Arrays.fill(this.sectionCache, null);
+ Arrays.fill(this.nibbleCache, null);
+ Arrays.fill(this.chunkCache, null);
+ Arrays.fill(this.emptinessMapCache, null);
+ if (this.isClientSide) {
+ Arrays.fill(this.notifyUpdateCache, false);
+ }
+ }
+
+ protected final BlockState getBlockState(final int worldX, final int worldY, final int worldZ) {
+ final LevelChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
+
+ if (section != null) {
+ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15);
+ }
+
+ return AIR_BLOCK_STATE;
+ }
+
+ protected final BlockState getBlockState(final int sectionIndex, final int localIndex) {
+ final LevelChunkSection section = this.sectionCache[sectionIndex];
+
+ if (section != null) {
+ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.states.get(localIndex);
+ }
+
+ return AIR_BLOCK_STATE;
+ }
+
+ protected final int getLightLevel(final int worldX, final int worldY, final int worldZ) {
+ final SWMRNibbleArray nibble = this.nibbleCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
+
+ return nibble == null ? 0 : nibble.getUpdating((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8));
+ }
+
+ protected final int getLightLevel(final int sectionIndex, final int localIndex) {
+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
+
+ return nibble == null ? 0 : nibble.getUpdating(localIndex);
+ }
+
+ protected final void setLightLevel(final int worldX, final int worldY, final int worldZ, final int level) {
+ final int sectionIndex = (worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset;
+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
+
+ if (nibble != null) {
+ nibble.set((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8), level);
+ if (this.isClientSide) {
+ int cx1 = (worldX - 1) >> 4;
+ int cx2 = (worldX + 1) >> 4;
+ int cy1 = (worldY - 1) >> 4;
+ int cy2 = (worldY + 1) >> 4;
+ int cz1 = (worldZ - 1) >> 4;
+ int cz2 = (worldZ + 1) >> 4;
+ for (int x = cx1; x <= cx2; ++x) {
+ for (int y = cy1; y <= cy2; ++y) {
+ for (int z = cz1; z <= cz2; ++z) {
+ this.notifyUpdateCache[x + 5 * z + (5 * 5) * y + this.chunkSectionIndexOffset] = true;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) {
+ if (this.isClientSide) {
+ int cx1 = (worldX - 1) >> 4;
+ int cx2 = (worldX + 1) >> 4;
+ int cy1 = (worldY - 1) >> 4;
+ int cy2 = (worldY + 1) >> 4;
+ int cz1 = (worldZ - 1) >> 4;
+ int cz2 = (worldZ + 1) >> 4;
+ for (int x = cx1; x <= cx2; ++x) {
+ for (int y = cy1; y <= cy2; ++y) {
+ for (int z = cz1; z <= cz2; ++z) {
+ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
+ }
+ }
+ }
+ }
+ }
+
+ protected final void setLightLevel(final int sectionIndex, final int localIndex, final int worldX, final int worldY, final int worldZ, final int level) {
+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
+
+ if (nibble != null) {
+ nibble.set(localIndex, level);
+ if (this.isClientSide) {
+ int cx1 = (worldX - 1) >> 4;
+ int cx2 = (worldX + 1) >> 4;
+ int cy1 = (worldY - 1) >> 4;
+ int cy2 = (worldY + 1) >> 4;
+ int cz1 = (worldZ - 1) >> 4;
+ int cz2 = (worldZ + 1) >> 4;
+ for (int x = cx1; x <= cx2; ++x) {
+ for (int y = cy1; y <= cy2; ++y) {
+ for (int z = cz1; z <= cz2; ++z) {
+ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ protected final boolean[] getEmptinessMap(final int chunkX, final int chunkZ) {
+ return this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
+ }
+
+ protected final void setEmptinessMapCache(final int chunkX, final int chunkZ, final boolean[] emptinessMap) {
+ this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = emptinessMap;
+ }
+
+ public static SWMRNibbleArray[] getFilledEmptyLight(final LevelHeightAccessor world) {
+ return getFilledEmptyLight(WorldUtil.getTotalLightSections(world));
+ }
+
+ private static SWMRNibbleArray[] getFilledEmptyLight(final int totalLightSections) {
+ final SWMRNibbleArray[] ret = new SWMRNibbleArray[totalLightSections];
+
+ for (int i = 0, len = ret.length; i < len; ++i) {
+ ret[i] = new SWMRNibbleArray(null, true);
+ }
+
+ return ret;
+ }
+
+ protected abstract boolean[] getEmptinessMap(final ChunkAccess chunk);
+
+ protected abstract void setEmptinessMap(final ChunkAccess chunk, final boolean[] to);
+
+ protected abstract SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk);
+
+ protected abstract void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to);
+
+ protected abstract boolean canUseChunk(final ChunkAccess chunk);
+
+ public final void blocksChangedInChunk(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ,
+ final Set<BlockPos> positions, final Boolean[] changedSections) {
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
+ try {
+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
+ if (chunk == null) {
+ return;
+ }
+ if (changedSections != null) {
+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, changedSections, false);
+ if (ret != null) {
+ this.setEmptinessMap(chunk, ret);
+ }
+ }
+ if (!positions.isEmpty()) {
+ this.propagateBlockChanges(lightAccess, chunk, positions);
+ }
+ this.updateVisible(lightAccess);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ // subclasses should not initialise caches, as this will always be done by the super call
+ // subclasses should not invoke updateVisible, as this will always be done by the super call
+ protected abstract void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions);
+
+ protected abstract void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ);
+
+ // if ret > expect, then the real value is at least ret (early returns if ret > expect, rather than calculating actual)
+ // if ret == expect, then expect is the correct light value for pos
+ // if ret < expect, then ret is the real light value
+ protected abstract int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
+ final int expect);
+
+ protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16];
+ protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16];
+
+ protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess chunk,
+ final int chunkX, final int chunkY, final int chunkZ) {
+ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
+ if (currNibble == null) {
+ return;
+ }
+
+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
+ final int neighbourOffX = direction.x;
+ final int neighbourOffZ = direction.z;
+
+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX,
+ chunkY, chunkZ + neighbourOffZ);
+
+ if (neighbourNibble == null) {
+ continue;
+ }
+
+ if (!currNibble.isInitialisedUpdating() && !neighbourNibble.isInitialisedUpdating()) {
+ // both are zero, nothing to check.
+ continue;
+ }
+
+ // this chunk
+ final int incX;
+ final int incZ;
+ final int startX;
+ final int startZ;
+
+ if (neighbourOffX != 0) {
+ // x direction
+ incX = 0;
+ incZ = 1;
+
+ if (direction.x < 0) {
+ // negative
+ startX = chunkX << 4;
+ } else {
+ startX = chunkX << 4 | 15;
+ }
+ startZ = chunkZ << 4;
+ } else {
+ // z direction
+ incX = 1;
+ incZ = 0;
+
+ if (neighbourOffZ < 0) {
+ // negative
+ startZ = chunkZ << 4;
+ } else {
+ startZ = chunkZ << 4 | 15;
+ }
+ startX = chunkX << 4;
+ }
+
+ int centerDelayedChecks = 0;
+ int neighbourDelayedChecks = 0;
+ for (int currY = chunkY << 4, maxY = currY | 15; currY <= maxY; ++currY) {
+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
+ final int neighbourX = currX + neighbourOffX;
+ final int neighbourZ = currZ + neighbourOffZ;
+
+ final int currentIndex = (currX & 15) |
+ ((currZ & 15)) << 4 |
+ ((currY & 15) << 8);
+ final int currentLevel = currNibble.getUpdating(currentIndex);
+
+ final int neighbourIndex =
+ (neighbourX & 15) |
+ ((neighbourZ & 15)) << 4 |
+ ((currY & 15) << 8);
+ final int neighbourLevel = neighbourNibble.getUpdating(neighbourIndex);
+
+ // the checks are delayed because the checkBlock method clobbers light values - which then
+ // affect later calculate light value operations. While they don't affect it in a behaviourly significant
+ // way, they do have a negative performance impact due to simply queueing more values
+
+ if (this.calculateLightValue(lightAccess, currX, currY, currZ, currentLevel) != currentLevel) {
+ this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex;
+ }
+
+ if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != neighbourLevel) {
+ this.chunkCheckDelayedUpdatesNeighbour[neighbourDelayedChecks++] = neighbourIndex;
+ }
+ }
+ }
+
+ final int currentChunkOffX = chunkX << 4;
+ final int currentChunkOffZ = chunkZ << 4;
+ final int neighbourChunkOffX = (chunkX + direction.x) << 4;
+ final int neighbourChunkOffZ = (chunkZ + direction.z) << 4;
+ final int chunkOffY = chunkY << 4;
+ for (int i = 0, len = Math.max(centerDelayedChecks, neighbourDelayedChecks); i < len; ++i) {
+ // try to queue neighbouring data together
+ // index = x | (z << 4) | (y << 8)
+ if (i < centerDelayedChecks) {
+ final int value = this.chunkCheckDelayedUpdatesCenter[i];
+ this.checkBlock(lightAccess, currentChunkOffX | (value & 15),
+ chunkOffY | (value >>> 8),
+ currentChunkOffZ | ((value >>> 4) & 0xF));
+ }
+ if (i < neighbourDelayedChecks) {
+ final int value = this.chunkCheckDelayedUpdatesNeighbour[i];
+ this.checkBlock(lightAccess, neighbourChunkOffX | (value & 15),
+ chunkOffY | (value >>> 8),
+ neighbourChunkOffZ | ((value >>> 4) & 0xF));
+ }
+ }
+ }
+ }
+
+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) {
+ final ChunkPos chunkPos = chunk.getPos();
+ final int chunkX = chunkPos.x;
+ final int chunkZ = chunkPos.z;
+
+ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) {
+ this.checkChunkEdge(lightAccess, chunk, chunkX, iterator.nextShort(), chunkZ);
+ }
+
+ this.performLightDecrease(lightAccess);
+ }
+
+ // subclasses should not initialise caches, as this will always be done by the super call
+ // subclasses should not invoke updateVisible, as this will always be done by the super call
+ // verifies that light levels on this chunks edges are consistent with this chunk's neighbours
+ // edges. if they are not, they are decreased (effectively performing the logic in checkBlock).
+ // This does not resolve skylight source problems.
+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) {
+ final ChunkPos chunkPos = chunk.getPos();
+ final int chunkX = chunkPos.x;
+ final int chunkZ = chunkPos.z;
+
+ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) {
+ this.checkChunkEdge(lightAccess, chunk, chunkX, currSectionY, chunkZ);
+ }
+
+ this.performLightDecrease(lightAccess);
+ }
+
+ // pulls light from neighbours, and adds them into the increase queue. does not actually propagate.
+ protected final void propagateNeighbourLevels(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) {
+ final ChunkPos chunkPos = chunk.getPos();
+ final int chunkX = chunkPos.x;
+ final int chunkZ = chunkPos.z;
+
+ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) {
+ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, currSectionY, chunkZ);
+ if (currNibble == null) {
+ continue;
+ }
+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
+ final int neighbourOffX = direction.x;
+ final int neighbourOffZ = direction.z;
+
+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX,
+ currSectionY, chunkZ + neighbourOffZ);
+
+ if (neighbourNibble == null || !neighbourNibble.isInitialisedUpdating()) {
+ // can't pull from 0
+ continue;
+ }
+
+ // neighbour chunk
+ final int incX;
+ final int incZ;
+ final int startX;
+ final int startZ;
+
+ if (neighbourOffX != 0) {
+ // x direction
+ incX = 0;
+ incZ = 1;
+
+ if (direction.x < 0) {
+ // negative
+ startX = (chunkX << 4) - 1;
+ } else {
+ startX = (chunkX << 4) + 16;
+ }
+ startZ = chunkZ << 4;
+ } else {
+ // z direction
+ incX = 1;
+ incZ = 0;
+
+ if (neighbourOffZ < 0) {
+ // negative
+ startZ = (chunkZ << 4) - 1;
+ } else {
+ startZ = (chunkZ << 4) + 16;
+ }
+ startX = chunkX << 4;
+ }
+
+ final long propagateDirection = 1L << direction.getOpposite().ordinal(); // we only want to check in this direction towards this chunk
+ final int encodeOffset = this.coordinateOffset;
+
+ for (int currY = currSectionY << 4, maxY = currY | 15; currY <= maxY; ++currY) {
+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
+ final int level = neighbourNibble.getUpdating(
+ (currX & 15)
+ | ((currZ & 15) << 4)
+ | ((currY & 15) << 8)
+ );
+
+ if (level <= 1) {
+ // nothing to propagate
+ continue;
+ }
+
+ this.appendToIncreaseQueue(
+ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((level & 0xFL) << (6 + 6 + 16))
+ | (propagateDirection << (6 + 6 + 16 + 4))
+ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the current block is transparent, must check.
+ );
+ }
+ }
+ }
+ }
+ }
+
+ public static Boolean[] getEmptySectionsForChunk(final ChunkAccess chunk) {
+ final LevelChunkSection[] sections = chunk.getSections();
+ final Boolean[] ret = new Boolean[sections.length];
+
+ for (int i = 0; i < sections.length; ++i) {
+ if (sections[i] == null || sections[i].hasOnlyAir()) {
+ ret[i] = Boolean.TRUE;
+ } else {
+ ret[i] = Boolean.FALSE;
+ }
+ }
+
+ return ret;
+ }
+
+ public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptinessChanges) {
+ final int chunkX = chunk.getPos().x;
+ final int chunkZ = chunk.getPos().z;
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
+ try {
+ // force current chunk into cache
+ this.setChunkInCache(chunkX, chunkZ, chunk);
+ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections());
+ this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk));
+ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk));
+
+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false);
+ if (ret != null) {
+ this.setEmptinessMap(chunk, ret);
+ }
+ this.updateVisible(lightAccess);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ public final void handleEmptySectionChanges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ,
+ final Boolean[] emptinessChanges) {
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
+ try {
+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
+ if (chunk == null) {
+ return;
+ }
+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false);
+ if (ret != null) {
+ this.setEmptinessMap(chunk, ret);
+ }
+ this.updateVisible(lightAccess);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles);
+
+ protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ);
+
+ // subclasses should not initialise caches, as this will always be done by the super call
+ // subclasses should not invoke updateVisible, as this will always be done by the super call
+ // subclasses are guaranteed that this is always called before a changed block set
+ // newChunk specifies whether the changes describe a "first load" of a chunk or changes to existing, already loaded chunks
+ // rets non-null when the emptiness map changed and needs to be updated
+ protected final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk,
+ final Boolean[] emptinessChanges, final boolean unlit) {
+ final Level world = (Level)lightAccess.getLevel();
+ final int chunkX = chunk.getPos().x;
+ final int chunkZ = chunk.getPos().z;
+
+ boolean[] chunkEmptinessMap = this.getEmptinessMap(chunkX, chunkZ);
+ boolean[] ret = null;
+ final boolean needsInit = unlit || chunkEmptinessMap == null;
+ if (needsInit) {
+ this.setEmptinessMapCache(chunkX, chunkZ, ret = chunkEmptinessMap = new boolean[WorldUtil.getTotalSections(world)]);
+ }
+
+ // update emptiness map
+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
+ Boolean valueBoxed = emptinessChanges[sectionIndex];
+ if (valueBoxed == null) {
+ if (!needsInit) {
+ continue;
+ }
+ final LevelChunkSection section = this.getChunkSection(chunkX, sectionIndex + this.minSection, chunkZ);
+ emptinessChanges[sectionIndex] = valueBoxed = section == null || section.hasOnlyAir() ? Boolean.TRUE : Boolean.FALSE;
+ }
+ chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue();
+ }
+
+ // now init neighbour nibbles
+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
+ final Boolean valueBoxed = emptinessChanges[sectionIndex];
+ final int sectionY = sectionIndex + this.minSection;
+ if (valueBoxed == null) {
+ continue;
+ }
+
+ final boolean empty = valueBoxed.booleanValue();
+
+ if (empty) {
+ continue;
+ }
+
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ // if we're not empty, we also need to initialise nibbles
+ // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up
+ final boolean extrude = (dx | dz) != 0 || !unlit;
+ for (int dy = 1; dy >= -1; --dy) {
+ this.initNibble(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false);
+ }
+ }
+ }
+ }
+
+ // check for de-init and lazy-init
+ // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running
+ // init checks.
+ for (int dz = -1; dz <= 1; ++dz) {
+ for (int dx = -1; dx <= 1; ++dx) {
+ // does this neighbour have 1 radius loaded?
+ boolean neighboursLoaded = true;
+ neighbour_loaded_search:
+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
+ if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) {
+ neighboursLoaded = false;
+ break neighbour_loaded_search;
+ }
+ }
+ }
+
+ for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) {
+ // check neighbours to see if we need to de-init this one
+ boolean allEmpty = true;
+ neighbour_search:
+ for (int dy2 = -1; dy2 <= 1; ++dy2) {
+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
+ final int y = sectionY + dy2;
+ if (y < this.minSection || y > this.maxSection) {
+ // empty
+ continue;
+ }
+ final boolean[] emptinessMap = this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ);
+ if (emptinessMap != null) {
+ if (!emptinessMap[y - this.minSection]) {
+ allEmpty = false;
+ break neighbour_search;
+ }
+ } else {
+ final LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ);
+ if (section != null && !section.hasOnlyAir()) {
+ allEmpty = false;
+ break neighbour_search;
+ }
+ }
+ }
+ }
+ }
+
+ if (allEmpty & neighboursLoaded) {
+ // can only de-init when neighbours are loaded
+ // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting
+ // to be correct
+
+ // all were empty, so de-init
+ this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ);
+ } else if (!allEmpty) {
+ // must init
+ final boolean extrude = (dx | dz) != 0 || !unlit;
+ this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false);
+ }
+ }
+ }
+ }
+
+ return ret;
+ }
+
+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) {
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
+ try {
+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
+ if (chunk == null) {
+ return;
+ }
+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection);
+ this.updateVisible(lightAccess);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) {
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
+ try {
+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
+ if (chunk == null) {
+ return;
+ }
+ this.checkChunkEdges(lightAccess, chunk, sections);
+ this.updateVisible(lightAccess);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ // subclasses should not initialise caches, as this will always be done by the super call
+ // subclasses should not invoke updateVisible, as this will always be done by the super call
+ // needsEdgeChecks applies when possibly loading vanilla data, which means we need to validate the current
+ // chunks light values with respect to neighbours
+ // subclasses should note that the emptiness changes are propagated BEFORE this is called, so this function
+ // does not need to detect empty chunks itself (and it should do no handling for them either!)
+ protected abstract void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks);
+
+ public final void light(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptySections) {
+ final int chunkX = chunk.getPos().x;
+ final int chunkZ = chunk.getPos().z;
+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
+
+ try {
+ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.maxLightSection - this.minLightSection + 1);
+ // force current chunk into cache
+ this.setChunkInCache(chunkX, chunkZ, chunk);
+ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections());
+ this.setNibblesForChunkInCache(chunkX, chunkZ, nibbles);
+ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk));
+
+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptySections, true);
+ if (ret != null) {
+ this.setEmptinessMap(chunk, ret);
+ }
+ this.lightChunk(lightAccess, chunk, true);
+ this.setNibbles(chunk, nibbles);
+ this.updateVisible(lightAccess);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ public final void relightChunks(final LightChunkGetter lightAccess, final Set<ChunkPos> chunks,
+ final Consumer<ChunkPos> chunkLightCallback, final IntConsumer onComplete) {
+ // it's recommended for maximum performance that the set is ordered according to a BFS from the center of
+ // the region of chunks to relight
+ // it's required that tickets are added for each chunk to keep them loaded
+ final Long2ObjectOpenHashMap<SWMRNibbleArray[]> nibblesByChunk = new Long2ObjectOpenHashMap<>();
+ final Long2ObjectOpenHashMap<boolean[]> emptinessMapByChunk = new Long2ObjectOpenHashMap<>();
+
+ final int[] neighbourLightOrder = new int[] {
+ // d = 0
+ 0, 0,
+ // d = 1
+ -1, 0,
+ 0, -1,
+ 1, 0,
+ 0, 1,
+ // d = 2
+ -1, 1,
+ 1, 1,
+ -1, -1,
+ 1, -1,
+ };
+
+ int lightCalls = 0;
+
+ for (final ChunkPos chunkPos : chunks) {
+ final int chunkX = chunkPos.x;
+ final int chunkZ = chunkPos.z;
+ final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(chunkX, chunkZ);
+ if (chunk == null || !this.canUseChunk(chunk)) {
+ throw new IllegalStateException();
+ }
+
+ for (int i = 0, len = neighbourLightOrder.length; i < len; i += 2) {
+ final int dx = neighbourLightOrder[i];
+ final int dz = neighbourLightOrder[i + 1];
+ final int neighbourX = dx + chunkX;
+ final int neighbourZ = dz + chunkZ;
+
+ final ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX, neighbourZ);
+ if (neighbour == null || !this.canUseChunk(neighbour)) {
+ continue;
+ }
+
+ if (nibblesByChunk.get(CoordinateUtils.getChunkKey(neighbourX, neighbourZ)) != null) {
+ // lit already called for neighbour, no need to light it now
+ continue;
+ }
+
+ // light neighbour chunk
+ this.setupEncodeOffset(neighbourX * 16 + 7, 128, neighbourZ * 16 + 7);
+ try {
+ // insert all neighbouring chunks for this neighbour that we have data for
+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
+ final int neighbourX2 = neighbourX + dx2;
+ final int neighbourZ2 = neighbourZ + dz2;
+ final long key = CoordinateUtils.getChunkKey(neighbourX2, neighbourZ2);
+ final ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX2, neighbourZ2);
+ if (neighbour2 == null || !this.canUseChunk(neighbour2)) {
+ continue;
+ }
+
+ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(key);
+ if (nibbles == null) {
+ // we haven't lit this chunk
+ continue;
+ }
+
+ this.setChunkInCache(neighbourX2, neighbourZ2, neighbour2);
+ this.setBlocksForChunkInCache(neighbourX2, neighbourZ2, neighbour2.getSections());
+ this.setNibblesForChunkInCache(neighbourX2, neighbourZ2, nibbles);
+ this.setEmptinessMapCache(neighbourX2, neighbourZ2, emptinessMapByChunk.get(key));
+ }
+ }
+
+ final long key = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
+
+ // now insert the neighbour chunk and light it
+ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.world);
+ nibblesByChunk.put(key, nibbles);
+
+ this.setChunkInCache(neighbourX, neighbourZ, neighbour);
+ this.setBlocksForChunkInCache(neighbourX, neighbourZ, neighbour.getSections());
+ this.setNibblesForChunkInCache(neighbourX, neighbourZ, nibbles);
+
+ final boolean[] neighbourEmptiness = this.handleEmptySectionChanges(lightAccess, neighbour, getEmptySectionsForChunk(neighbour), true);
+ emptinessMapByChunk.put(key, neighbourEmptiness);
+ if (chunks.contains(new ChunkPos(neighbourX, neighbourZ))) {
+ this.setEmptinessMap(neighbour, neighbourEmptiness);
+ }
+
+ this.lightChunk(lightAccess, neighbour, false);
+ } finally {
+ this.destroyCaches();
+ }
+ }
+
+ // done lighting all neighbours, so the chunk is now fully lit
+
+ // make sure nibbles are fully updated before calling back
+ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ for (final SWMRNibbleArray nibble : nibbles) {
+ nibble.updateVisible();
+ }
+
+ this.setNibbles(chunk, nibbles);
+
+ for (int y = this.minLightSection; y <= this.maxLightSection; ++y) {
+ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkZ));
+ }
+
+ // now do callback
+ if (chunkLightCallback != null) {
+ chunkLightCallback.accept(chunkPos);
+ }
+ ++lightCalls;
+ }
+
+ if (onComplete != null) {
+ onComplete.accept(lightCalls);
+ }
+ }
+
+ // contains:
+ // lower (6 + 6 + 16) = 28 bits: encoded coordinate position (x | (z << 6) | (y << (6 + 6))))
+ // next 4 bits: propagated light level (0, 15]
+ // next 6 bits: propagation direction bitset
+ // next 24 bits: unused
+ // last 3 bits: state flags
+ // state flags:
+ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading light
+ // updates for block sources
+ protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 2;
+ // whether the propagation needs to check if its current level is equal to the expected level
+ // used only in increase propagation
+ protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 1;
+ // whether the propagation needs to consider if its block is conditionally transparent
+ protected static final long FLAG_HAS_SIDED_TRANSPARENT_BLOCKS = Long.MIN_VALUE;
+
+ protected long[] increaseQueue = new long[16 * 16 * 16];
+ protected int increaseQueueInitialLength;
+ protected long[] decreaseQueue = new long[16 * 16 * 16];
+ protected int decreaseQueueInitialLength;
+
+ protected final long[] resizeIncreaseQueue() {
+ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2);
+ }
+
+ protected final long[] resizeDecreaseQueue() {
+ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2);
+ }
+
+ protected final void appendToIncreaseQueue(final long value) {
+ final int idx = this.increaseQueueInitialLength++;
+ long[] queue = this.increaseQueue;
+ if (idx >= queue.length) {
+ queue = this.resizeIncreaseQueue();
+ queue[idx] = value;
+ } else {
+ queue[idx] = value;
+ }
+ }
+
+ protected final void appendToDecreaseQueue(final long value) {
+ final int idx = this.decreaseQueueInitialLength++;
+ long[] queue = this.decreaseQueue;
+ if (idx >= queue.length) {
+ queue = this.resizeDecreaseQueue();
+ queue[idx] = value;
+ } else {
+ queue[idx] = value;
+ }
+ }
+
+ protected static final AxisDirection[][] OLD_CHECK_DIRECTIONS = new AxisDirection[1 << 6][];
+ protected static final int ALL_DIRECTIONS_BITSET = (1 << 6) - 1;
+ static {
+ for (int i = 0; i < OLD_CHECK_DIRECTIONS.length; ++i) {
+ final List<AxisDirection> directions = new ArrayList<>();
+ for (int bitset = i, len = Integer.bitCount(i), index = 0; index < len; ++index, bitset ^= IntegerUtil.getTrailingBit(bitset)) {
+ directions.add(AXIS_DIRECTIONS[IntegerUtil.trailingZeros(bitset)]);
+ }
+ OLD_CHECK_DIRECTIONS[i] = directions.toArray(new AxisDirection[0]);
+ }
+ }
+
+ protected final void performLightIncrease(final LightChunkGetter lightAccess) {
+ final BlockGetter world = lightAccess.getLevel();
+ long[] queue = this.increaseQueue;
+ int queueReadIndex = 0;
+ int queueLength = this.increaseQueueInitialLength;
+ this.increaseQueueInitialLength = 0;
+ final int decodeOffsetX = -this.encodeOffsetX;
+ final int decodeOffsetY = -this.encodeOffsetY;
+ final int decodeOffsetZ = -this.encodeOffsetZ;
+ final int encodeOffset = this.coordinateOffset;
+ final int sectionOffset = this.chunkSectionIndexOffset;
+
+ while (queueReadIndex < queueLength) {
+ final long queueValue = queue[queueReadIndex++];
+
+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xFL);
+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63L)];
+
+ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) {
+ if (this.getLightLevel(posX, posY, posZ) != propagatedLightLevel) {
+ // not at the level we expect, so something changed.
+ continue;
+ }
+ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) {
+ // these are used to restore block sources after a propagation decrease
+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel);
+ }
+
+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) {
+ // we don't need to worry about our state here.
+ for (final AxisDirection propagate : checkDirections) {
+ final int offX = posX + propagate.x;
+ final int offY = posY + propagate.y;
+ final int offZ = posZ + propagate.z;
+
+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
+
+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
+ final int currentLevel;
+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) {
+ continue; // already at the level we want or unloaded
+ }
+
+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
+ if (blockState == null) {
+ continue;
+ }
+ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
+ if (opacityCached != -1) {
+ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached);
+ if (targetLevel > currentLevel) {
+ currentNibble.set(localIndex, targetLevel);
+ this.postLightUpdate(offX, offY, offZ);
+
+ if (targetLevel > 1) {
+ if (queueLength >= queue.length) {
+ queue = this.resizeIncreaseQueue();
+ }
+ queue[queueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4));
+ continue;
+ }
+ }
+ continue;
+ } else {
+ this.mutablePos1.set(offX, offY, offZ);
+ long flags = 0;
+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
+
+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
+ continue;
+ }
+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+ }
+
+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
+ if (targetLevel <= currentLevel) {
+ continue;
+ }
+
+ currentNibble.set(localIndex, targetLevel);
+ this.postLightUpdate(offX, offY, offZ);
+
+ if (targetLevel > 1) {
+ if (queueLength >= queue.length) {
+ queue = this.resizeIncreaseQueue();
+ }
+ queue[queueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4))
+ | (flags);
+ }
+ continue;
+ }
+ }
+ } else {
+ // we actually need to worry about our state here
+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
+ this.mutablePos2.set(posX, posY, posZ);
+ for (final AxisDirection propagate : checkDirections) {
+ final int offX = posX + propagate.x;
+ final int offY = posY + propagate.y;
+ final int offZ = posZ + propagate.z;
+
+ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty();
+
+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
+ continue;
+ }
+
+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
+
+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
+ final int currentLevel;
+
+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) {
+ continue; // already at the level we want
+ }
+
+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
+ if (blockState == null) {
+ continue;
+ }
+ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
+ if (opacityCached != -1) {
+ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached);
+ if (targetLevel > currentLevel) {
+ currentNibble.set(localIndex, targetLevel);
+ this.postLightUpdate(offX, offY, offZ);
+
+ if (targetLevel > 1) {
+ if (queueLength >= queue.length) {
+ queue = this.resizeIncreaseQueue();
+ }
+ queue[queueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4));
+ continue;
+ }
+ }
+ continue;
+ } else {
+ this.mutablePos1.set(offX, offY, offZ);
+ long flags = 0;
+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
+
+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
+ continue;
+ }
+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+ }
+
+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
+ if (targetLevel <= currentLevel) {
+ continue;
+ }
+
+ currentNibble.set(localIndex, targetLevel);
+ this.postLightUpdate(offX, offY, offZ);
+
+ if (targetLevel > 1) {
+ if (queueLength >= queue.length) {
+ queue = this.resizeIncreaseQueue();
+ }
+ queue[queueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4))
+ | (flags);
+ }
+ continue;
+ }
+ }
+ }
+ }
+ }
+
+ protected final void performLightDecrease(final LightChunkGetter lightAccess) {
+ final BlockGetter world = lightAccess.getLevel();
+ long[] queue = this.decreaseQueue;
+ long[] increaseQueue = this.increaseQueue;
+ int queueReadIndex = 0;
+ int queueLength = this.decreaseQueueInitialLength;
+ this.decreaseQueueInitialLength = 0;
+ int increaseQueueLength = this.increaseQueueInitialLength;
+ final int decodeOffsetX = -this.encodeOffsetX;
+ final int decodeOffsetY = -this.encodeOffsetY;
+ final int decodeOffsetZ = -this.encodeOffsetZ;
+ final int encodeOffset = this.coordinateOffset;
+ final int sectionOffset = this.chunkSectionIndexOffset;
+ final int emittedMask = this.emittedLightMask;
+
+ while (queueReadIndex < queueLength) {
+ final long queueValue = queue[queueReadIndex++];
+
+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF);
+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)];
+
+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) {
+ // we don't need to worry about our state here.
+ for (final AxisDirection propagate : checkDirections) {
+ final int offX = posX + propagate.x;
+ final int offY = posY + propagate.y;
+ final int offZ = posZ + propagate.z;
+
+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
+
+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
+ final int lightLevel;
+
+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) {
+ // already at lowest (or unloaded), nothing we can do
+ continue;
+ }
+
+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
+ if (blockState == null) {
+ continue;
+ }
+ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
+ if (opacityCached != -1) {
+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached));
+ if (lightLevel > targetLevel) {
+ // it looks like another source propagated here, so re-propagate it
+ if (increaseQueueLength >= increaseQueue.length) {
+ increaseQueue = this.resizeIncreaseQueue();
+ }
+ increaseQueue[increaseQueueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ | FLAG_RECHECK_LEVEL;
+ continue;
+ }
+ final int emittedLight = blockState.getLightEmission() & emittedMask;
+ if (emittedLight != 0) {
+ // re-propagate source
+ // note: do not set recheck level, or else the propagation will fail
+ if (increaseQueueLength >= increaseQueue.length) {
+ increaseQueue = this.resizeIncreaseQueue();
+ }
+ increaseQueue[increaseQueueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL);
+ }
+
+ currentNibble.set(localIndex, 0);
+ this.postLightUpdate(offX, offY, offZ);
+
+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
+ if (queueLength >= queue.length) {
+ queue = this.resizeDecreaseQueue();
+ }
+ queue[queueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4));
+ continue;
+ }
+ continue;
+ } else {
+ this.mutablePos1.set(offX, offY, offZ);
+ long flags = 0;
+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
+
+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
+ continue;
+ }
+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+ }
+
+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
+ if (lightLevel > targetLevel) {
+ // it looks like another source propagated here, so re-propagate it
+ if (increaseQueueLength >= increaseQueue.length) {
+ increaseQueue = this.resizeIncreaseQueue();
+ }
+ increaseQueue[increaseQueueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ | (FLAG_RECHECK_LEVEL | flags);
+ continue;
+ }
+ final int emittedLight = blockState.getLightEmission() & emittedMask;
+ if (emittedLight != 0) {
+ // re-propagate source
+ // note: do not set recheck level, or else the propagation will fail
+ if (increaseQueueLength >= increaseQueue.length) {
+ increaseQueue = this.resizeIncreaseQueue();
+ }
+ increaseQueue[increaseQueueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ | (flags | FLAG_WRITE_LEVEL);
+ }
+
+ currentNibble.set(localIndex, 0);
+ this.postLightUpdate(offX, offY, offZ);
+
+ if (targetLevel > 0) {
+ if (queueLength >= queue.length) {
+ queue = this.resizeDecreaseQueue();
+ }
+ queue[queueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4))
+ | flags;
+ }
+ continue;
+ }
+ }
+ } else {
+ // we actually need to worry about our state here
+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
+ this.mutablePos2.set(posX, posY, posZ);
+ for (final AxisDirection propagate : checkDirections) {
+ final int offX = posX + propagate.x;
+ final int offY = posY + propagate.y;
+ final int offZ = posZ + propagate.z;
+
+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
+
+ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty();
+
+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
+ continue;
+ }
+
+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
+ final int lightLevel;
+
+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) {
+ // already at lowest (or unloaded), nothing we can do
+ continue;
+ }
+
+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
+ if (blockState == null) {
+ continue;
+ }
+ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
+ if (opacityCached != -1) {
+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached));
+ if (lightLevel > targetLevel) {
+ // it looks like another source propagated here, so re-propagate it
+ if (increaseQueueLength >= increaseQueue.length) {
+ increaseQueue = this.resizeIncreaseQueue();
+ }
+ increaseQueue[increaseQueueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ | FLAG_RECHECK_LEVEL;
+ continue;
+ }
+ final int emittedLight = blockState.getLightEmission() & emittedMask;
+ if (emittedLight != 0) {
+ // re-propagate source
+ // note: do not set recheck level, or else the propagation will fail
+ if (increaseQueueLength >= increaseQueue.length) {
+ increaseQueue = this.resizeIncreaseQueue();
+ }
+ increaseQueue[increaseQueueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL);
+ }
+
+ currentNibble.set(localIndex, 0);
+ this.postLightUpdate(offX, offY, offZ);
+
+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
+ if (queueLength >= queue.length) {
+ queue = this.resizeDecreaseQueue();
+ }
+ queue[queueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4));
+ continue;
+ }
+ continue;
+ } else {
+ this.mutablePos1.set(offX, offY, offZ);
+ long flags = 0;
+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
+
+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
+ continue;
+ }
+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+ }
+
+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
+ if (lightLevel > targetLevel) {
+ // it looks like another source propagated here, so re-propagate it
+ if (increaseQueueLength >= increaseQueue.length) {
+ increaseQueue = this.resizeIncreaseQueue();
+ }
+ increaseQueue[increaseQueueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ | (FLAG_RECHECK_LEVEL | flags);
+ continue;
+ }
+ final int emittedLight = blockState.getLightEmission() & emittedMask;
+ if (emittedLight != 0) {
+ // re-propagate source
+ // note: do not set recheck level, or else the propagation will fail
+ if (increaseQueueLength >= increaseQueue.length) {
+ increaseQueue = this.resizeIncreaseQueue();
+ }
+ increaseQueue[increaseQueueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+ | (flags | FLAG_WRITE_LEVEL);
+ }
+
+ currentNibble.set(localIndex, 0);
+ this.postLightUpdate(offX, offY, offZ);
+
+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
+ if (queueLength >= queue.length) {
+ queue = this.resizeDecreaseQueue();
+ }
+ queue[queueLength++] =
+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4))
+ | flags;
+ }
+ continue;
+ }
+ }
+ }
+ }
+
+ // propagate sources we clobbered
+ this.increaseQueueInitialLength = increaseQueueLength;
+ this.performLightIncrease(lightAccess);
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
new file mode 100644
index 0000000000000000000000000000000000000000..c64ab41198a5e0c7cbcbe6452af11f82f5938862
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
@@ -0,0 +1,930 @@
+package ca.spottedleaf.moonrise.patches.starlight.light;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
+import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.shorts.ShortCollection;
+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.SectionPos;
+import net.minecraft.server.level.ChunkLevel;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.TicketType;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.DataLayer;
+import net.minecraft.world.level.chunk.LightChunkGetter;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import net.minecraft.world.level.lighting.LayerLightEventListener;
+import net.minecraft.world.level.lighting.LevelLightEngine;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+
+public final class StarLightInterface {
+
+ public static final TicketType<Long> CHUNK_WORK_TICKET = TicketType.create("starlight:chunk_work_ticket", Long::compareTo);
+ public static final int LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(ChunkStatus.LIGHT);
+ // ticket level = ChunkLevel.byStatus(FullChunkStatus.FULL) - input
+ public static final int REGION_LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.FULL) - LIGHT_TICKET_LEVEL;
+
+ /**
+ * Can be {@code null}, indicating the light is all empty.
+ */
+ public final Level world;
+ public final LightChunkGetter lightAccess;
+
+ private final ArrayDeque<SkyStarLightEngine> cachedSkyPropagators;
+ private final ArrayDeque<BlockStarLightEngine> cachedBlockPropagators;
+
+ private final LightQueue lightQueue;
+
+ private final LayerLightEventListener skyReader;
+ private final LayerLightEventListener blockReader;
+ private final boolean isClientSide;
+
+ public final int minSection;
+ public final int maxSection;
+ public final int minLightSection;
+ public final int maxLightSection;
+
+ public final LevelLightEngine lightEngine;
+
+ private final boolean hasBlockLight;
+ private final boolean hasSkyLight;
+
+ public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) {
+ this.lightAccess = lightAccess;
+ this.world = lightAccess == null ? null : (Level)lightAccess.getLevel();
+ this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null;
+ this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null;
+ this.isClientSide = !(this.world instanceof ServerLevel);
+ if (this.world == null) {
+ this.minSection = -4;
+ this.maxSection = 19;
+ this.minLightSection = -5;
+ this.maxLightSection = 20;
+ } else {
+ this.minSection = WorldUtil.getMinSection(this.world);
+ this.maxSection = WorldUtil.getMaxSection(this.world);
+ this.minLightSection = WorldUtil.getMinLightSection(this.world);
+ this.maxLightSection = WorldUtil.getMaxLightSection(this.world);
+ }
+
+ if (this.world instanceof ServerLevel) {
+ this.lightQueue = new ServerLightQueue(this);
+ } else {
+ this.lightQueue = new ClientLightQueue(this);
+ }
+
+ this.lightEngine = lightEngine;
+ this.hasBlockLight = hasBlockLight;
+ this.hasSkyLight = hasSkyLight;
+ this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
+ @Override
+ public void checkBlock(final BlockPos blockPos) {
+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
+ }
+
+ @Override
+ public void propagateLightSources(final ChunkPos chunkPos) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasLightWork() {
+ // not really correct...
+ return StarLightInterface.this.hasUpdates();
+ }
+
+ @Override
+ public int runLightUpdates() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DataLayer getDataLayerData(final SectionPos pos) {
+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
+ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) {
+ return null;
+ }
+
+ final int sectionY = pos.getY();
+
+ if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) {
+ return null;
+ }
+
+ if (((StarlightChunk)chunk).starlight$getSkyEmptinessMap() == null) {
+ return null;
+ }
+
+ return ((StarlightChunk)chunk).starlight$getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble();
+ }
+
+ @Override
+ public int getLightValue(final BlockPos blockPos) {
+ return StarLightInterface.this.getSkyLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4));
+ }
+
+ @Override
+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
+ StarLightInterface.this.sectionChange(pos, notReady);
+ }
+ };
+ this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
+ @Override
+ public void checkBlock(final BlockPos blockPos) {
+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
+ }
+
+ @Override
+ public void propagateLightSources(final ChunkPos chunkPos) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasLightWork() {
+ // not really correct...
+ return StarLightInterface.this.hasUpdates();
+ }
+
+ @Override
+ public int runLightUpdates() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DataLayer getDataLayerData(final SectionPos pos) {
+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
+
+ if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) {
+ return null;
+ }
+
+ return ((StarlightChunk)chunk).starlight$getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble();
+ }
+
+ @Override
+ public int getLightValue(final BlockPos blockPos) {
+ return StarLightInterface.this.getBlockLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4));
+ }
+
+ @Override
+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
+ StarLightInterface.this.sectionChange(pos, notReady);
+ }
+ };
+ }
+
+ public ClientLightQueue getClientLightQueue() {
+ if (this.lightQueue instanceof ClientLightQueue clientLightQueue) {
+ return clientLightQueue;
+ }
+ return null;
+ }
+
+ public ServerLightQueue getServerLightQueue() {
+ if (this.lightQueue instanceof ServerLightQueue serverLightQueue) {
+ return serverLightQueue;
+ }
+ return null;
+ }
+
+ public boolean hasSkyLight() {
+ return this.hasSkyLight;
+ }
+
+ public boolean hasBlockLight() {
+ return this.hasBlockLight;
+ }
+
+ public int getSkyLightValue(final BlockPos blockPos, final ChunkAccess chunk) {
+ if (!this.hasSkyLight) {
+ return 0;
+ }
+ final int x = blockPos.getX();
+ int y = blockPos.getY();
+ final int z = blockPos.getZ();
+
+ final int minSection = this.minSection;
+ final int maxSection = this.maxSection;
+ final int minLightSection = this.minLightSection;
+ final int maxLightSection = this.maxLightSection;
+
+ if (chunk == null || (!this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) {
+ return 15;
+ }
+
+ int sectionY = y >> 4;
+
+ if (sectionY > maxLightSection) {
+ return 15;
+ }
+
+ if (sectionY < minLightSection) {
+ sectionY = minLightSection;
+ y = sectionY << 4;
+ }
+
+ final SWMRNibbleArray[] nibbles = ((StarlightChunk)chunk).starlight$getSkyNibbles();
+ final SWMRNibbleArray immediate = nibbles[sectionY - minLightSection];
+
+ if (!immediate.isNullNibbleVisible()) {
+ return immediate.getVisible(x, y, z);
+ }
+
+ final boolean[] emptinessMap = ((StarlightChunk)chunk).starlight$getSkyEmptinessMap();
+
+ if (emptinessMap == null) {
+ return 15;
+ }
+
+ // are we above this chunk's lowest empty section?
+ int lowestY = minLightSection - 1;
+ for (int currY = maxSection; currY >= minSection; --currY) {
+ if (emptinessMap[currY - minSection]) {
+ continue;
+ }
+
+ // should always be full lit here
+ lowestY = currY;
+ break;
+ }
+
+ if (sectionY > lowestY) {
+ return 15;
+ }
+
+ // this nibble is going to depend solely on the skylight data above it
+ // find first non-null data above (there does exist one, as we just found it above)
+ for (int currY = sectionY + 1; currY <= maxLightSection; ++currY) {
+ final SWMRNibbleArray nibble = nibbles[currY - minLightSection];
+ if (!nibble.isNullNibbleVisible()) {
+ return nibble.getVisible(x, 0, z);
+ }
+ }
+
+ // should never reach here
+ return 15;
+ }
+
+ public int getBlockLightValue(final BlockPos blockPos, final ChunkAccess chunk) {
+ if (!this.hasBlockLight) {
+ return 0;
+ }
+ final int y = blockPos.getY();
+ final int cy = y >> 4;
+
+ final int minLightSection = this.minLightSection;
+ final int maxLightSection = this.maxLightSection;
+
+ if (cy < minLightSection || cy > maxLightSection) {
+ return 0;
+ }
+
+ if (chunk == null) {
+ return 0;
+ }
+
+ final SWMRNibbleArray nibble = ((StarlightChunk)chunk).starlight$getBlockNibbles()[cy - minLightSection];
+ return nibble.getVisible(blockPos.getX(), y, blockPos.getZ());
+ }
+
+ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) {
+ final ChunkAccess chunk = this.getAnyChunkNow(pos.getX() >> 4, pos.getZ() >> 4);
+
+ final int sky = this.getSkyLightValue(pos, chunk) - ambientDarkness;
+ // Don't fetch the block light level if the skylight level is 15, since the value will never be higher.
+ if (sky == 15) {
+ return 15;
+ }
+ final int block = this.getBlockLightValue(pos, chunk);
+ return Math.max(sky, block);
+ }
+
+ public LayerLightEventListener getSkyReader() {
+ return this.skyReader;
+ }
+
+ public LayerLightEventListener getBlockReader() {
+ return this.blockReader;
+ }
+
+ public boolean isClientSide() {
+ return this.isClientSide;
+ }
+
+ public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) {
+ if (this.world == null) {
+ // empty world
+ return null;
+ }
+ return ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ);
+ }
+
+ public boolean hasUpdates() {
+ return !this.lightQueue.isEmpty();
+ }
+
+ public Level getWorld() {
+ return this.world;
+ }
+
+ public LightChunkGetter getLightAccess() {
+ return this.lightAccess;
+ }
+
+ public SkyStarLightEngine getSkyLightEngine() {
+ if (this.cachedSkyPropagators == null) {
+ return null;
+ }
+ final SkyStarLightEngine ret;
+ synchronized (this.cachedSkyPropagators) {
+ ret = this.cachedSkyPropagators.pollFirst();
+ }
+
+ if (ret == null) {
+ return new SkyStarLightEngine(this.world);
+ }
+ return ret;
+ }
+
+ public void releaseSkyLightEngine(final SkyStarLightEngine engine) {
+ if (this.cachedSkyPropagators == null) {
+ return;
+ }
+ synchronized (this.cachedSkyPropagators) {
+ this.cachedSkyPropagators.addFirst(engine);
+ }
+ }
+
+ public BlockStarLightEngine getBlockLightEngine() {
+ if (this.cachedBlockPropagators == null) {
+ return null;
+ }
+ final BlockStarLightEngine ret;
+ synchronized (this.cachedBlockPropagators) {
+ ret = this.cachedBlockPropagators.pollFirst();
+ }
+
+ if (ret == null) {
+ return new BlockStarLightEngine(this.world);
+ }
+ return ret;
+ }
+
+ public void releaseBlockLightEngine(final BlockStarLightEngine engine) {
+ if (this.cachedBlockPropagators == null) {
+ return;
+ }
+ synchronized (this.cachedBlockPropagators) {
+ this.cachedBlockPropagators.addFirst(engine);
+ }
+ }
+
+ public LightQueue.ChunkTasks blockChange(final BlockPos pos) {
+ if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world
+ return null;
+ }
+
+ return this.lightQueue.queueBlockChange(pos);
+ }
+
+ public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) {
+ if (this.world == null) { // empty world
+ return null;
+ }
+
+ return this.lightQueue.queueSectionChange(pos, newEmptyValue);
+ }
+
+ public void forceLoadInChunk(final ChunkAccess chunk, final Boolean[] emptySections) {
+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
+
+ try {
+ if (skyEngine != null) {
+ skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections);
+ }
+ if (blockEngine != null) {
+ blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections);
+ }
+ } finally {
+ this.releaseSkyLightEngine(skyEngine);
+ this.releaseBlockLightEngine(blockEngine);
+ }
+ }
+
+ public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) {
+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
+
+ try {
+ if (skyEngine != null) {
+ skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections);
+ }
+ if (blockEngine != null) {
+ blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections);
+ }
+ } finally {
+ this.releaseSkyLightEngine(skyEngine);
+ this.releaseBlockLightEngine(blockEngine);
+ }
+ }
+
+ public void lightChunk(final ChunkAccess chunk, final Boolean[] emptySections) {
+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
+
+ try {
+ if (skyEngine != null) {
+ skyEngine.light(this.lightAccess, chunk, emptySections);
+ }
+ if (blockEngine != null) {
+ blockEngine.light(this.lightAccess, chunk, emptySections);
+ }
+ } finally {
+ this.releaseSkyLightEngine(skyEngine);
+ this.releaseBlockLightEngine(blockEngine);
+ }
+ }
+
+ public void relightChunks(final Set<ChunkPos> chunks, final Consumer<ChunkPos> chunkLightCallback,
+ final IntConsumer onComplete) {
+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
+
+ try {
+ if (skyEngine != null) {
+ skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null,
+ blockEngine == null ? onComplete : null);
+ }
+ if (blockEngine != null) {
+ blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete);
+ }
+ } finally {
+ this.releaseSkyLightEngine(skyEngine);
+ this.releaseBlockLightEngine(blockEngine);
+ }
+ }
+
+ public void checkChunkEdges(final int chunkX, final int chunkZ) {
+ this.checkSkyEdges(chunkX, chunkZ);
+ this.checkBlockEdges(chunkX, chunkZ);
+ }
+
+ public void checkSkyEdges(final int chunkX, final int chunkZ) {
+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
+
+ try {
+ if (skyEngine != null) {
+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ);
+ }
+ } finally {
+ this.releaseSkyLightEngine(skyEngine);
+ }
+ }
+
+ public void checkBlockEdges(final int chunkX, final int chunkZ) {
+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
+ try {
+ if (blockEngine != null) {
+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ);
+ }
+ } finally {
+ this.releaseBlockLightEngine(blockEngine);
+ }
+ }
+
+ public void propagateChanges() {
+ final LightQueue lightQueue = this.lightQueue;
+ if (lightQueue instanceof ClientLightQueue clientLightQueue) {
+ clientLightQueue.drainTasks();
+ } // else: invalid usage, although we won't throw because mods...
+ }
+
+ public static abstract class LightQueue {
+
+ protected final StarLightInterface lightInterface;
+
+ public LightQueue(final StarLightInterface lightInterface) {
+ this.lightInterface = lightInterface;
+ }
+
+ public abstract boolean isEmpty();
+
+ public abstract ChunkTasks queueBlockChange(final BlockPos pos);
+
+ public abstract ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue);
+
+ public abstract ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections);
+
+ public abstract ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections);
+
+ public static abstract class ChunkTasks implements Runnable {
+
+ public final long chunkCoordinate;
+
+ protected final StarLightInterface lightEngine;
+ protected final LightQueue queue;
+ protected final MultiThreadedQueue<Runnable> onComplete = new MultiThreadedQueue<>();
+ protected final Set<BlockPos> changedPositions = new HashSet<>();
+ protected Boolean[] changedSectionSet;
+ protected ShortOpenHashSet queuedEdgeChecksSky;
+ protected ShortOpenHashSet queuedEdgeChecksBlock;
+ protected List<BooleanSupplier> lightTasks;
+
+ public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue) {
+ this.chunkCoordinate = chunkCoordinate;
+ this.lightEngine = lightEngine;
+ this.queue = queue;
+ }
+
+ @Override
+ public abstract void run();
+
+ public void queueOrRunTask(final Runnable run) {
+ if (!this.onComplete.add(run)) {
+ run.run();
+ }
+ }
+
+ protected void addChangedPosition(final BlockPos pos) {
+ this.changedPositions.add(pos.immutable());
+ }
+
+ protected void setChangedSection(final int y, final Boolean newEmptyValue) {
+ if (this.changedSectionSet == null) {
+ this.changedSectionSet = new Boolean[this.lightEngine.maxSection - this.lightEngine.minSection + 1];
+ }
+ this.changedSectionSet[y - this.lightEngine.minSection] = newEmptyValue;
+ }
+
+ protected void addLightTask(final BooleanSupplier lightTask) {
+ if (this.lightTasks == null) {
+ this.lightTasks = new ArrayList<>();
+ }
+ this.lightTasks.add(lightTask);
+ }
+
+ protected void addEdgeChecksSky(final ShortCollection values) {
+ if (this.queuedEdgeChecksSky == null) {
+ this.queuedEdgeChecksSky = new ShortOpenHashSet(Math.max(8, values.size()));
+ }
+ this.queuedEdgeChecksSky.addAll(values);
+ }
+
+ protected void addEdgeChecksBlock(final ShortCollection values) {
+ if (this.queuedEdgeChecksBlock == null) {
+ this.queuedEdgeChecksBlock = new ShortOpenHashSet(Math.max(8, values.size()));
+ }
+ this.queuedEdgeChecksBlock.addAll(values);
+ }
+
+ protected final void runTasks() {
+ boolean litChunk = false;
+ if (this.lightTasks != null) {
+ for (final BooleanSupplier run : this.lightTasks) {
+ if (run.getAsBoolean()) {
+ litChunk = true;
+ break;
+ }
+ }
+ }
+
+ if (!litChunk) {
+ final SkyStarLightEngine skyEngine = this.lightEngine.getSkyLightEngine();
+ final BlockStarLightEngine blockEngine = this.lightEngine.getBlockLightEngine();
+ try {
+ final long coordinate = this.chunkCoordinate;
+ final int chunkX = CoordinateUtils.getChunkX(coordinate);
+ final int chunkZ = CoordinateUtils.getChunkZ(coordinate);
+
+ final Set<BlockPos> positions = this.changedPositions;
+ final Boolean[] sectionChanges = this.changedSectionSet;
+
+ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
+ skyEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges);
+ }
+ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
+ blockEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges);
+ }
+
+ if (skyEngine != null && this.queuedEdgeChecksSky != null) {
+ skyEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksSky);
+ }
+ if (blockEngine != null && this.queuedEdgeChecksBlock != null) {
+ blockEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksBlock);
+ }
+ } finally {
+ this.lightEngine.releaseSkyLightEngine(skyEngine);
+ this.lightEngine.releaseBlockLightEngine(blockEngine);
+ }
+ }
+
+ Runnable run;
+ while ((run = this.onComplete.pollOrBlockAdds()) != null) {
+ run.run();
+ }
+ }
+ }
+ }
+
+ public static final class ClientLightQueue extends LightQueue {
+
+ private final Long2ObjectLinkedOpenHashMap<ClientChunkTasks> chunkTasks = new Long2ObjectLinkedOpenHashMap<>();
+
+ public ClientLightQueue(final StarLightInterface lightInterface) {
+ super(lightInterface);
+ }
+
+ @Override
+ public synchronized boolean isEmpty() {
+ return this.chunkTasks.isEmpty();
+ }
+
+ // must hold synchronized lock on this object
+ private ClientChunkTasks getOrCreate(final long key) {
+ return this.chunkTasks.computeIfAbsent(key, (final long keyInMap) -> {
+ return new ClientChunkTasks(keyInMap, ClientLightQueue.this.lightInterface, ClientLightQueue.this);
+ });
+ }
+
+ @Override
+ public synchronized ClientChunkTasks queueBlockChange(final BlockPos pos) {
+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
+ tasks.addChangedPosition(pos);
+ return tasks;
+ }
+
+ @Override
+ public synchronized ClientChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) {
+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
+
+ tasks.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue));
+
+ return tasks;
+ }
+
+ @Override
+ public synchronized ClientChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
+
+ tasks.addEdgeChecksSky(sections);
+
+ return tasks;
+ }
+
+ @Override
+ public synchronized ClientChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
+ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos));
+
+ tasks.addEdgeChecksBlock(sections);
+
+ return tasks;
+ }
+
+ public synchronized ClientChunkTasks removeFirstTask() {
+ if (this.chunkTasks.isEmpty()) {
+ return null;
+ }
+ return this.chunkTasks.removeFirst();
+ }
+
+ public void drainTasks() {
+ ClientChunkTasks task;
+ while ((task = this.removeFirstTask()) != null) {
+ task.runTasks();
+ }
+ }
+
+ public static final class ClientChunkTasks extends ChunkTasks {
+
+ public ClientChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final ClientLightQueue queue) {
+ super(chunkCoordinate, lightEngine, queue);
+ }
+
+ @Override
+ public void run() {
+ this.runTasks();
+ }
+ }
+ }
+
+ public static final class ServerLightQueue extends LightQueue {
+
+ private final ConcurrentLong2ReferenceChainedHashTable<ServerChunkTasks> chunkTasks = new ConcurrentLong2ReferenceChainedHashTable<>();
+
+ public ServerLightQueue(final StarLightInterface lightInterface) {
+ super(lightInterface);
+ }
+
+ public void lowerPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) {
+ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ if (task != null) {
+ task.lowerPriority(priority);
+ }
+ }
+
+ public void setPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) {
+ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ if (task != null) {
+ task.setPriority(priority);
+ }
+ }
+
+ public void raisePriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) {
+ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ if (task != null) {
+ task.raisePriority(priority);
+ }
+ }
+
+ public PrioritisedExecutor.Priority getPriority(final int chunkX, final int chunkZ) {
+ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ if (task != null) {
+ return task.getPriority();
+ }
+
+ return PrioritisedExecutor.Priority.COMPLETING;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.chunkTasks.isEmpty();
+ }
+
+ @Override
+ public ServerChunkTasks queueBlockChange(final BlockPos pos) {
+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
+ if (valueInMap == null) {
+ valueInMap = new ServerChunkTasks(
+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
+ );
+ }
+ valueInMap.addChangedPosition(pos);
+ return valueInMap;
+ });
+
+ ret.schedule();
+
+ return ret;
+ }
+
+ @Override
+ public ServerChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) {
+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
+ if (valueInMap == null) {
+ valueInMap = new ServerChunkTasks(
+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
+ );
+ }
+
+ valueInMap.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue));
+
+ return valueInMap;
+ });
+
+ ret.schedule();
+
+ return ret;
+ }
+
+ public ServerChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final PrioritisedExecutor.Priority priority) {
+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
+ if (valueInMap == null) {
+ valueInMap = new ServerChunkTasks(
+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this, priority
+ );
+ }
+
+ valueInMap.addLightTask(lightTask);
+
+ return valueInMap;
+ });
+
+ ret.schedule();
+
+ return ret;
+ }
+
+ @Override
+ public ServerChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
+ if (valueInMap == null) {
+ valueInMap = new ServerChunkTasks(
+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
+ );
+ }
+
+ valueInMap.addEdgeChecksSky(sections);
+
+ return valueInMap;
+ });
+
+ ret.schedule();
+
+ return ret;
+ }
+
+ @Override
+ public ServerChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
+ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> {
+ if (valueInMap == null) {
+ valueInMap = new ServerChunkTasks(
+ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this
+ );
+ }
+
+ valueInMap.addEdgeChecksBlock(sections);
+
+ return valueInMap;
+ });
+
+ ret.schedule();
+
+ return ret;
+ }
+
+ public static final class ServerChunkTasks extends ChunkTasks {
+
+ private final AtomicBoolean ticketAdded = new AtomicBoolean();
+ private final PrioritisedExecutor.PrioritisedTask task;
+
+ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine,
+ final ServerLightQueue queue) {
+ this(chunkCoordinate, lightEngine, queue, PrioritisedExecutor.Priority.NORMAL);
+ }
+
+ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine,
+ final ServerLightQueue queue, final PrioritisedExecutor.Priority priority) {
+ super(chunkCoordinate, lightEngine, queue);
+ this.task = ((ChunkSystemServerLevel)(ServerLevel)lightEngine.getWorld()).moonrise$getChunkTaskScheduler().radiusAwareScheduler.createTask(
+ CoordinateUtils.getChunkX(chunkCoordinate), CoordinateUtils.getChunkZ(chunkCoordinate),
+ ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$getWriteRadius(), this, priority
+ );
+ }
+
+ public boolean markTicketAdded() {
+ return !this.ticketAdded.get() && !this.ticketAdded.getAndSet(true);
+ }
+
+ public void schedule() {
+ this.task.queue();
+ }
+
+ public boolean cancel() {
+ return this.task.cancel();
+ }
+
+ public PrioritisedExecutor.Priority getPriority() {
+ return this.task.getPriority();
+ }
+
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+ this.task.lowerPriority(priority);
+ }
+
+ public void setPriority(final PrioritisedExecutor.Priority priority) {
+ this.task.setPriority(priority);
+ }
+
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
+ this.task.raisePriority(priority);
+ }
+
+ @Override
+ public void run() {
+ ((ServerLightQueue)this.queue).chunkTasks.remove(this.chunkCoordinate, this);
+
+ this.runTasks();
+ }
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..7fe59ab70557aa6a484a02db2b2007fdd9e4bbb8
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java
@@ -0,0 +1,29 @@
+package ca.spottedleaf.moonrise.patches.starlight.light;
+
+import net.minecraft.core.SectionPos;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.LightLayer;
+import net.minecraft.world.level.chunk.DataLayer;
+import net.minecraft.world.level.chunk.LevelChunk;
+import java.util.Collection;
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+
+public interface StarLightLightingProvider {
+
+ public StarLightInterface starlight$getLightEngine();
+
+ public void starlight$clientUpdateLight(final LightLayer lightType, final SectionPos pos,
+ final DataLayer nibble, final boolean trustEdges);
+
+ public void starlight$clientRemoveLightData(final ChunkPos chunkPos);
+
+ public void starlight$clientChunkLoad(final ChunkPos pos, final LevelChunk chunk);
+
+ public default int starlight$serverRelightChunks(final Collection<ChunkPos> chunks,
+ final Consumer<ChunkPos> chunkLightCallback,
+ final IntConsumer onComplete) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..57692a503e147a00ac4e1586cd78e12b71a80d3f
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
@@ -0,0 +1,188 @@
+package ca.spottedleaf.moonrise.patches.starlight.util;
+
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
+import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine;
+import com.mojang.logging.LogUtils;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.ListTag;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import org.slf4j.Logger;
+
+public final class SaveUtil {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ private static final int STARLIGHT_LIGHT_VERSION = 9;
+
+ public static int getLightVersion() {
+ return STARLIGHT_LIGHT_VERSION;
+ }
+
+ private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state";
+ private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state";
+ private static final String STARLIGHT_VERSION_TAG = "starlight.light_version";
+
+ public static void saveLightHook(final Level world, final ChunkAccess chunk, final CompoundTag nbt) {
+ try {
+ saveLightHookReal(world, chunk, nbt);
+ } catch (final Throwable ex) {
+ // failing to inject is not fatal so we catch anything here. if it fails, it will have correctly set lit to false
+ // for Vanilla to relight on load and it will not set our lit tag so we will relight on load
+ LOGGER.warn("Failed to inject light data into save data for chunk " + chunk.getPos() + ", chunk light will be recalculated on its next load", ex);
+ }
+ }
+
+ private static void saveLightHookReal(final Level world, final ChunkAccess chunk, final CompoundTag tag) {
+ if (tag == null) {
+ return;
+ }
+
+ final int minSection = WorldUtil.getMinLightSection(world);
+ final int maxSection = WorldUtil.getMaxLightSection(world);
+
+ SWMRNibbleArray[] blockNibbles = ((StarlightChunk)chunk).starlight$getBlockNibbles();
+ SWMRNibbleArray[] skyNibbles = ((StarlightChunk)chunk).starlight$getSkyNibbles();
+
+ boolean lit = chunk.isLightCorrect() || !(world instanceof ServerLevel);
+ // diff start - store our tag for whether light data is init'd
+ if (lit) {
+ tag.putBoolean("isLightOn", false);
+ }
+ // diff end - store our tag for whether light data is init'd
+ ChunkStatus status = ChunkStatus.byName(tag.getString("Status"));
+
+ CompoundTag[] sections = new CompoundTag[maxSection - minSection + 1];
+
+ ListTag sectionsStored = tag.getList("sections", 10);
+
+ for (int i = 0; i < sectionsStored.size(); ++i) {
+ CompoundTag sectionStored = sectionsStored.getCompound(i);
+ int k = sectionStored.getByte("Y");
+
+ // strip light data
+ sectionStored.remove("BlockLight");
+ sectionStored.remove("SkyLight");
+
+ if (!sectionStored.isEmpty()) {
+ sections[k - minSection] = sectionStored;
+ }
+ }
+
+ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) {
+ for (int i = minSection; i <= maxSection; ++i) {
+ SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState();
+ SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState();
+ if (blockNibble != null || skyNibble != null) {
+ CompoundTag section = sections[i - minSection];
+ if (section == null) {
+ section = new CompoundTag();
+ section.putByte("Y", (byte)i);
+ sections[i - minSection] = section;
+ }
+
+ // we store under the same key so mod programs editing nbt
+ // can still read the data, hopefully.
+ // however, for compatibility we store chunks as unlit so vanilla
+ // is forced to re-light them if it encounters our data. It's too much of a burden
+ // to try and maintain compatibility with a broken and inferior skylight management system.
+
+ if (blockNibble != null) {
+ if (blockNibble.data != null) {
+ section.putByteArray("BlockLight", blockNibble.data);
+ }
+ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state);
+ }
+
+ if (skyNibble != null) {
+ if (skyNibble.data != null) {
+ section.putByteArray("SkyLight", skyNibble.data);
+ }
+ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state);
+ }
+ }
+ }
+ }
+
+ // rewrite section list
+ sectionsStored.clear();
+ for (CompoundTag section : sections) {
+ if (section != null) {
+ sectionsStored.add(section);
+ }
+ }
+ tag.put("sections", sectionsStored);
+ if (lit) {
+ tag.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // only mark as fully lit after we have successfully injected our data
+ }
+ }
+
+ public static void loadLightHook(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) {
+ try {
+ loadLightHookReal(world, pos, tag, into);
+ } catch (final Throwable ex) {
+ // failing to inject is not fatal so we catch anything here. if it fails, then we simply relight. Not a problem, we get correct
+ // lighting in both cases.
+ LOGGER.warn("Failed to load light for chunk " + pos + ", light will be recalculated", ex);
+ }
+ }
+
+ private static void loadLightHookReal(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) {
+ if (into == null) {
+ return;
+ }
+ final int minSection = WorldUtil.getMinLightSection(world);
+ final int maxSection = WorldUtil.getMaxLightSection(world);
+
+ into.setLightCorrect(false); // mark as unlit in case we fail parsing
+
+ SWMRNibbleArray[] blockNibbles = StarLightEngine.getFilledEmptyLight(world);
+ SWMRNibbleArray[] skyNibbles = StarLightEngine.getFilledEmptyLight(world);
+
+
+ // start copy from the original method
+ boolean lit = tag.get("isLightOn") != null && tag.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION;
+ boolean canReadSky = world.dimensionType().hasSkyLight();
+ ChunkStatus status = ChunkStatus.byName(tag.getString("Status"));
+ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { // diff - we add the status check here
+ ListTag sections = tag.getList("sections", 10);
+
+ for (int i = 0; i < sections.size(); ++i) {
+ CompoundTag sectionData = sections.getCompound(i);
+ int y = sectionData.getByte("Y");
+
+ if (sectionData.contains("BlockLight", 7)) {
+ // this is where our diff is
+ blockNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety
+ } else {
+ blockNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG));
+ }
+
+ if (canReadSky) {
+ if (sectionData.contains("SkyLight", 7)) {
+ // we store under the same key so mod programs editing nbt
+ // can still read the data, hopefully.
+ // however, for compatibility we store chunks as unlit so vanilla
+ // is forced to re-light them if it encounters our data. It's too much of a burden
+ // to try and maintain compatibility with a broken and inferior skylight management system.
+ skyNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety
+ } else {
+ skyNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG));
+ }
+ }
+ }
+ }
+ // end copy from vanilla
+
+ ((StarlightChunk)into).starlight$setBlockNibbles(blockNibbles);
+ ((StarlightChunk)into).starlight$setSkyNibbles(skyNibbles);
+ into.setLightCorrect(lit); // now we set lit here, only after we've correctly parsed data
+ }
+
+ private SaveUtil() {}
+}
diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
index a79abe9b26f68d573812e91554124783075ae17a..183d99ec9b94ca20a823c46a2d6bf0a215046d48 100644
--- a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
+++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
@@ -25,6 +25,10 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
+/**
+ * @deprecated Use {@link ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem}
+ */
+@Deprecated(forRemoval = true)
public final class ChunkSystem {
private static final Logger LOGGER = LogUtils.getLogger();
@@ -35,35 +39,17 @@ public final class ChunkSystem {
}
public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
- scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
+ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkTask(level, chunkX, chunkZ, run); // Paper - reroute
}
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);
+ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkTask(level, chunkX, chunkZ, run, priority); // Paper - reroute
}
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);
- }
- }
- }
- });
+ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkLoad(level, chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete); // Paper - reroute
}
static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo);
@@ -71,160 +57,29 @@ public final class ChunkSystem {
private static long chunkLoadCounter = 0L;
public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
- if (!Bukkit.isPrimaryThread()) {
- scheduleChunkTask(level, chunkX, chunkZ, () -> {
- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
- }, priority);
- return;
- }
-
- final int minLevel = 33 + ChunkSystem.getDistance(toStatus);
- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
- final ChunkPos chunkPos = new 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);
- SneakyThrow.sneaky(thr);
- } finally {
- if (addTicket) {
- level.chunkSource.addTicketAtLevel(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 CompletableFuture<ChunkResult<ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
-
- if (loadFuture.isDone()) {
- loadCallback.accept(loadFuture.join().orElse(null));
- return;
- }
-
- loadFuture.whenCompleteAsync((final ChunkResult<ChunkAccess> result, final Throwable thr) -> {
- if (thr != null) {
- loadCallback.accept(null);
- return;
- }
- loadCallback.accept(result.orElse(null));
- }, (final Runnable r) -> {
- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
- });
+ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - reroute
}
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 (!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 ChunkPos chunkPos = new 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);
- SneakyThrow.sneaky(thr);
- } finally {
- if (addTicket) {
- level.chunkSource.addTicketAtLevel(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 CompletableFuture<ChunkResult<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 ChunkResult<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);
- });
+ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - reroute
}
public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
- return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
+ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getVisibleChunkHolders(level); // Paper - reroute
}
public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
- return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
+ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getUpdatingChunkHolders(level); // Paper - reroute
}
public static int getVisibleChunkHolderCount(final ServerLevel level) {
- return level.chunkSource.chunkMap.visibleChunkMap.size();
+ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getVisibleChunkHolderCount(level); // Paper - reroute
}
public static int getUpdatingChunkHolderCount(final ServerLevel level) {
- return level.chunkSource.chunkMap.updatingChunkMap.size();
+ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getUpdatingChunkHolderCount(level); // Paper - reroute
}
public static boolean hasAnyChunkHolders(final ServerLevel level) {
@@ -268,27 +123,19 @@ public final class ChunkSystem {
}
public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
- return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ);
+ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getUnloadingChunkHolder(level, chunkX, chunkZ); // Paper - reroute
}
public static int getSendViewDistance(final ServerPlayer player) {
- return getLoadViewDistance(player);
+ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getSendViewDistance(player); // Paper - reroute
}
public static int getLoadViewDistance(final ServerPlayer player) {
- final ServerLevel level = player.serverLevel();
- if (level == null) {
- return Bukkit.getViewDistance();
- }
- return level.chunkSource.chunkMap.getPlayerViewDistance(player);
+ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getLoadViewDistance(player); // Paper - reroute
}
public static int getTickViewDistance(final ServerPlayer player) {
- final ServerLevel level = player.serverLevel();
- if (level == null) {
- return Bukkit.getSimulationDistance();
- }
- return level.chunkSource.chunkMap.distanceManager.simulationDistance;
+ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getTickViewDistance(player); // Paper - reroute
}
private ChunkSystem() {
diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
index 46bf42d5ea9e7b046f962531c5962d287cf44a41..362765d977aaa1996f9cef3404c0676d7bbddf38 100644
--- a/src/main/java/io/papermc/paper/command/PaperCommand.java
+++ b/src/main/java/io/papermc/paper/command/PaperCommand.java
@@ -42,6 +42,8 @@ public final class PaperCommand extends Command {
commands.put(Set.of("dumpitem"), new DumpItemCommand());
commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand());
commands.put(Set.of("dumplisteners"), new DumpListenersCommand());
+ commands.put(Set.of("fixlight"), new FixLightCommand()); // Paper - rewrite chunk system
+ commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand()); // Paper - rewrite chunk system
return commands.entrySet().stream()
.flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
diff --git a/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
new file mode 100644
index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd01fba0fc
--- /dev/null
+++ b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
@@ -0,0 +1,277 @@
+package io.papermc.paper.command.subcommands;
+
+import ca.spottedleaf.moonrise.common.util.JsonUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
+import io.papermc.paper.command.CommandUtil;
+import io.papermc.paper.command.PaperSubcommand;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.ImposterProtoChunk;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.ProtoChunk;
+import org.bukkit.Bukkit;
+import org.bukkit.command.CommandSender;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+import static net.kyori.adventure.text.Component.text;
+import static net.kyori.adventure.text.format.NamedTextColor.BLUE;
+import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA;
+import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
+import static net.kyori.adventure.text.format.NamedTextColor.RED;
+
+@DefaultQualifier(NonNull.class)
+public final class ChunkDebugCommand implements PaperSubcommand {
+ @Override
+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
+ switch (subCommand) {
+ case "debug" -> this.doDebug(sender, args);
+ case "chunkinfo" -> this.doChunkInfo(sender, args);
+ case "holderinfo" -> this.doHolderInfo(sender, args);
+ }
+ return true;
+ }
+
+ @Override
+ public List<String> tabComplete(final CommandSender sender, final String subCommand, final String[] args) {
+ switch (subCommand) {
+ case "debug" -> {
+ if (args.length == 1) {
+ return CommandUtil.getListMatchingLast(sender, args, "help", "chunks");
+ }
+ }
+ case "holderinfo" -> {
+ List<String> worldNames = new ArrayList<>();
+ worldNames.add("*");
+ for (org.bukkit.World world : Bukkit.getWorlds()) {
+ worldNames.add(world.getName());
+ }
+ if (args.length == 1) {
+ return CommandUtil.getListMatchingLast(sender, args, worldNames);
+ }
+ }
+ case "chunkinfo" -> {
+ List<String> worldNames = new ArrayList<>();
+ worldNames.add("*");
+ for (org.bukkit.World world : Bukkit.getWorlds()) {
+ worldNames.add(world.getName());
+ }
+ if (args.length == 1) {
+ return CommandUtil.getListMatchingLast(sender, args, worldNames);
+ }
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ private void doChunkInfo(final CommandSender sender, final String[] args) {
+ List<org.bukkit.World> worlds;
+ if (args.length < 1 || args[0].equals("*")) {
+ worlds = Bukkit.getWorlds();
+ } else {
+ worlds = new ArrayList<>(args.length);
+ for (final String arg : args) {
+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg);
+ if (world == null) {
+ sender.sendMessage(text("World '" + arg + "' is invalid", RED));
+ return;
+ }
+ worlds.add(world);
+ }
+ }
+
+ int accumulatedTotal = 0;
+ int accumulatedInactive = 0;
+ int accumulatedBorder = 0;
+ int accumulatedTicking = 0;
+ int accumulatedEntityTicking = 0;
+
+ for (final org.bukkit.World bukkitWorld : worlds) {
+ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle();
+
+ int total = 0;
+ int inactive = 0;
+ int full = 0;
+ int blockTicking = 0;
+ int entityTicking = 0;
+
+ for (final NewChunkHolder holder : ((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders()) {
+ final NewChunkHolder.ChunkCompletion completion = holder.getLastChunkCompletion();
+ final ChunkAccess chunk = completion == null ? null : completion.chunk();
+
+ if (!(chunk instanceof LevelChunk fullChunk)) {
+ continue;
+ }
+
+ ++total;
+
+ switch (holder.getChunkStatus()) {
+ case INACCESSIBLE: {
+ ++inactive;
+ break;
+ }
+ case FULL: {
+ ++full;
+ break;
+ }
+ case BLOCK_TICKING: {
+ ++blockTicking;
+ break;
+ }
+ case ENTITY_TICKING: {
+ ++entityTicking;
+ break;
+ }
+ }
+ }
+
+ accumulatedTotal += total;
+ accumulatedInactive += inactive;
+ accumulatedBorder += full;
+ accumulatedTicking += blockTicking;
+ accumulatedEntityTicking += entityTicking;
+
+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":")));
+ sender.sendMessage(text().color(DARK_AQUA).append(
+ text("Total: ", BLUE), text(total),
+ text(" Inactive: ", BLUE), text(inactive),
+ text(" Full: ", BLUE), text(full),
+ text(" Block Ticking: ", BLUE), text(blockTicking),
+ text(" Entity Ticking: ", BLUE), text(entityTicking)
+ ));
+ }
+ if (worlds.size() > 1) {
+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA)));
+ sender.sendMessage(text().color(DARK_AQUA).append(
+ text("Total: ", BLUE), text(accumulatedTotal),
+ text(" Inactive: ", BLUE), text(accumulatedInactive),
+ text(" Full: ", BLUE), text(accumulatedBorder),
+ text(" Block Ticking: ", BLUE), text(accumulatedTicking),
+ text(" Entity Ticking: ", BLUE), text(accumulatedEntityTicking)
+ ));
+ }
+ }
+
+ private void doHolderInfo(final CommandSender sender, final String[] args) {
+ List<org.bukkit.World> worlds;
+ if (args.length < 1 || args[0].equals("*")) {
+ worlds = Bukkit.getWorlds();
+ } else {
+ worlds = new ArrayList<>(args.length);
+ for (final String arg : args) {
+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg);
+ if (world == null) {
+ sender.sendMessage(text("World '" + arg + "' is invalid", RED));
+ return;
+ }
+ worlds.add(world);
+ }
+ }
+
+ int accumulatedTotal = 0;
+ int accumulatedCanUnload = 0;
+ int accumulatedNull = 0;
+ int accumulatedReadOnly = 0;
+ int accumulatedProtoChunk = 0;
+ int accumulatedFullChunk = 0;
+
+ for (final org.bukkit.World bukkitWorld : worlds) {
+ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle();
+
+ int total = 0;
+ int canUnload = 0;
+ int nullChunks = 0;
+ int readOnly = 0;
+ int protoChunk = 0;
+ int fullChunk = 0;
+
+ for (final NewChunkHolder holder : ((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders()) {
+ final NewChunkHolder.ChunkCompletion completion = holder.getLastChunkCompletion();
+ final ChunkAccess chunk = completion == null ? null : completion.chunk();
+
+ ++total;
+
+ if (chunk == null) {
+ ++nullChunks;
+ } else if (chunk instanceof ImposterProtoChunk) {
+ ++readOnly;
+ } else if (chunk instanceof ProtoChunk) {
+ ++protoChunk;
+ } else if (chunk instanceof LevelChunk) {
+ ++fullChunk;
+ }
+
+ if (holder.isSafeToUnload() == null) {
+ ++canUnload;
+ }
+ }
+
+ accumulatedTotal += total;
+ accumulatedCanUnload += canUnload;
+ accumulatedNull += nullChunks;
+ accumulatedReadOnly += readOnly;
+ accumulatedProtoChunk += protoChunk;
+ accumulatedFullChunk += fullChunk;
+
+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":")));
+ sender.sendMessage(text().color(DARK_AQUA).append(
+ text("Total: ", BLUE), text(total),
+ text(" Unloadable: ", BLUE), text(canUnload),
+ text(" Null: ", BLUE), text(nullChunks),
+ text(" ReadOnly: ", BLUE), text(readOnly),
+ text(" Proto: ", BLUE), text(protoChunk),
+ text(" Full: ", BLUE), text(fullChunk)
+ ));
+ }
+ if (worlds.size() > 1) {
+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA)));
+ sender.sendMessage(text().color(DARK_AQUA).append(
+ text("Total: ", BLUE), text(accumulatedTotal),
+ text(" Unloadable: ", BLUE), text(accumulatedCanUnload),
+ text(" Null: ", BLUE), text(accumulatedNull),
+ text(" ReadOnly: ", BLUE), text(accumulatedReadOnly),
+ text(" Proto: ", BLUE), text(accumulatedProtoChunk),
+ text(" Full: ", BLUE), text(accumulatedFullChunk)
+ ));
+ }
+ }
+
+ private void doDebug(final CommandSender sender, final String[] args) {
+ if (args.length < 1) {
+ sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED));
+ return;
+ }
+
+ final String debugType = args[0].toLowerCase(Locale.ROOT);
+ switch (debugType) {
+ case "chunks" -> {
+ if (args.length >= 2 && args[1].toLowerCase(Locale.ROOT).equals("help")) {
+ sender.sendMessage(text("Use /paper debug chunks to dump loaded chunk information to a file", RED));
+ break;
+ }
+ final File file = ChunkTaskScheduler.getChunkDebugFile();
+ sender.sendMessage(text("Writing chunk information dump to " + file, GREEN));
+ try {
+ JsonUtil.writeJson(ChunkTaskScheduler.debugAllWorlds(MinecraftServer.getServer()), file);
+ sender.sendMessage(text("Successfully written chunk information!", GREEN));
+ } catch (Throwable thr) {
+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
+ sender.sendMessage(text("Failed to dump chunk information, see console", RED));
+ }
+ }
+ // "help" & default
+ default -> sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED));
+ }
+ }
+
+}
diff --git a/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java
new file mode 100644
index 0000000000000000000000000000000000000000..85950a1aa732ab8c01ad28bec9e0de140e1a172e
--- /dev/null
+++ b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java
@@ -0,0 +1,116 @@
+package io.papermc.paper.command.subcommands;
+
+import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider;
+import io.papermc.paper.command.PaperSubcommand;
+import io.papermc.paper.util.MCUtil;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.ThreadedLevelLightEngine;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import org.bukkit.command.CommandSender;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+import java.text.DecimalFormat;
+
+import static net.kyori.adventure.text.Component.text;
+import static net.kyori.adventure.text.format.NamedTextColor.BLUE;
+import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA;
+import static net.kyori.adventure.text.format.NamedTextColor.RED;
+
+@DefaultQualifier(NonNull.class)
+public final class FixLightCommand implements PaperSubcommand {
+
+ private static final ThreadLocal<DecimalFormat> ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> {
+ return new DecimalFormat("#,##0.0");
+ });
+
+ @Override
+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
+ this.doFixLight(sender, args);
+ return true;
+ }
+
+ private void doFixLight(final CommandSender sender, final String[] args) {
+ if (!(sender instanceof Player)) {
+ sender.sendMessage(text("Only players can use this command", RED));
+ return;
+ }
+ @Nullable Runnable post = null;
+ int radius = 2;
+ if (args.length > 0) {
+ try {
+ final int parsed = Integer.parseInt(args[0]);
+ if (parsed < 0) {
+ sender.sendMessage(text("Radius cannot be negative!", RED));
+ return;
+ }
+ final int maxRadius = 32;
+ radius = Math.min(maxRadius, parsed);
+ if (radius != parsed) {
+ post = () -> sender.sendMessage(text("Radius '" + parsed + "' was not in the required range [0, " + maxRadius + "], it was lowered to the maximum (" + maxRadius + " chunks).", RED));
+ }
+ } catch (final Exception e) {
+ sender.sendMessage(text("'" + args[0] + "' is not a valid number.", RED));
+ return;
+ }
+ }
+
+ CraftPlayer player = (CraftPlayer) sender;
+ ServerPlayer handle = player.getHandle();
+ ServerLevel world = (ServerLevel) handle.level();
+ ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine();
+ this.starlightFixLight(handle, world, lightengine, radius, post);
+ }
+
+ private void starlightFixLight(
+ final ServerPlayer sender,
+ final ServerLevel world,
+ final ThreadedLevelLightEngine lightengine,
+ final int radius,
+ final @Nullable Runnable done
+ ) {
+ final long start = System.nanoTime();
+ final java.util.LinkedHashSet<ChunkPos> chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos
+
+ final int[] pending = new int[1];
+ for (java.util.Iterator<ChunkPos> iterator = chunks.iterator(); iterator.hasNext(); ) {
+ final ChunkPos chunkPos = iterator.next();
+
+ final @Nullable ChunkAccess chunk = (ChunkAccess) world.getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z);
+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) {
+ // cannot relight this chunk
+ iterator.remove();
+ continue;
+ }
+
+ ++pending[0];
+ }
+
+ final int[] relitChunks = new int[1];
+ ((StarLightLightingProvider)lightengine).starlight$serverRelightChunks(chunks,
+ (final ChunkPos chunkPos) -> {
+ ++relitChunks[0];
+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append(
+ text("Relit chunk ", BLUE), text(chunkPos.toString()),
+ text(", progress: ", BLUE), text(ONE_DECIMAL_PLACES.get().format(100.0 * (double) (relitChunks[0]) / (double) pending[0]) + "%")
+ ));
+ },
+ (final int totalRelit) -> {
+ final long end = System.nanoTime();
+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append(
+ text("Relit ", BLUE), text(totalRelit),
+ text(" chunks. Took ", BLUE), text(ONE_DECIMAL_PLACES.get().format(1.0e-6 * (end - start)) + "ms")
+ ));
+ if (done != null) {
+ done.run();
+ }
+ }
+ );
+ sender.getBukkitEntity().sendMessage(text().color(BLUE).append(text("Relighting "), text(pending[0], DARK_AQUA), text(" chunks")));
+ }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
index 2a5453707bc172d8d0efe3f11959cb0b5f830984..b8499c1cea97a1a88a53053bc7da132f2fd3928d 100644
--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
@@ -29,6 +29,45 @@ public class GlobalConfiguration extends ConfigurationPart {
public static GlobalConfiguration get() {
return instance;
}
+
+ public ChunkLoadingBasic chunkLoadingBasic;
+
+ public class ChunkLoadingBasic extends ConfigurationPart {
+ @Comment("The maximum rate in chunks per second that the server will send to any individual player. Set to -1 to disable this limit.")
+ public double playerMaxChunkSendRate = 75.0;
+
+ @Comment(
+ "The maximum rate at which chunks will load for any individual player. " +
+ "Note that this setting also affects chunk generations, since a chunk load is always first issued to test if a" +
+ "chunk is already generated. Set to -1 to disable this limit."
+ )
+ public double playerMaxChunkLoadRate = 100.0;
+
+ @Comment("The maximum rate at which chunks will generate for any individual player. Set to -1 to disable this limit.")
+ public double playerMaxChunkGenerateRate = -1.0;
+ }
+
+ public ChunkLoadingAdvanced chunkLoadingAdvanced;
+
+ public class ChunkLoadingAdvanced extends ConfigurationPart {
+ @Comment(
+ "Set to true if the server will match the chunk send radius that clients have configured" +
+ "in their view distance settings if the client is less-than the server's send distance."
+ )
+ public boolean autoConfigSendDistance = true;
+
+ @Comment(
+ "Specifies the maximum amount of concurrent chunk loads that an individual player can have." +
+ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit."
+ )
+ public int playerMaxConcurrentChunkLoads = 0;
+
+ @Comment(
+ "Specifies the maximum amount of concurrent chunk generations that an individual player can have." +
+ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit."
+ )
+ public int playerMaxConcurrentChunkGenerates = 0;
+ }
static void set(GlobalConfiguration instance) {
GlobalConfiguration.instance = instance;
}
@@ -130,21 +169,6 @@ public class GlobalConfiguration extends ConfigurationPart {
public int incomingPacketThreshold = 300;
}
- public ChunkLoading chunkLoading;
-
- public class ChunkLoading extends ConfigurationPart {
- public int minLoadRadius = 2;
- public int maxConcurrentSends = 2;
- public boolean autoconfigSendDistance = true;
- public double targetPlayerChunkSendRate = 100.0;
- public double globalMaxChunkSendRate = -1.0;
- public boolean enableFrustumPriority = false;
- public double globalMaxChunkLoadRate = -1.0;
- public double playerMaxConcurrentLoads = 20.0;
- public double globalMaxConcurrentLoads = 500.0;
- public double playerMaxChunkLoadRate = -1.0;
- }
-
public UnsupportedSettings unsupportedSettings;
public class UnsupportedSettings extends ConfigurationPart {
@@ -203,7 +227,7 @@ public class GlobalConfiguration extends ConfigurationPart {
@PostProcess
private void postProcess() {
- //io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this);
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.init(this);
}
}
diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
new file mode 100644
index 0000000000000000000000000000000000000000..8424cf9d4617b4732d44cc460d25b04481068989
--- /dev/null
+++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
@@ -0,0 +1,10 @@
+package io.papermc.paper.threadedregions;
+
+// placeholder class for Folia
+public class TickRegions {
+
+ public static int getRegionChunkShift() {
+ return ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ThreadedTicketLevelPropagator.SECTION_SHIFT;
+ }
+
+}
diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java
index 73e83d56a340f0c7dcb8ff737d621003e72c6de4..d05297d77147ab68f8c5bb08f13a1f882a686c4f 100644
--- a/src/main/java/io/papermc/paper/util/TickThread.java
+++ b/src/main/java/io/papermc/paper/util/TickThread.java
@@ -6,7 +6,7 @@ import net.minecraft.world.entity.Entity;
import org.bukkit.Bukkit;
import java.util.concurrent.atomic.AtomicInteger;
-public final class TickThread extends Thread {
+public class TickThread extends Thread {
public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.strict-thread-checks");
@@ -16,6 +16,10 @@ public final class TickThread extends Thread {
}
}
+ /**
+ * @deprecated
+ */
+ @Deprecated
public static void softEnsureTickThread(final String reason) {
if (!STRICT_THREAD_CHECKS) {
return;
@@ -23,6 +27,10 @@ public final class TickThread extends Thread {
ensureTickThread(reason);
}
+ /**
+ * @deprecated
+ */
+ @Deprecated
public static void ensureTickThread(final String reason) {
if (!isTickThread()) {
MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
@@ -30,6 +38,21 @@ public final class TickThread extends Thread {
}
}
+ public static void ensureTickThread(final ServerLevel world, final net.minecraft.core.BlockPos pos, final String reason) {
+ if (!isTickThreadFor(world, pos)) {
+ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
+ throw new IllegalStateException(reason);
+ }
+ }
+
+ public static void ensureTickThread(final ServerLevel world, final net.minecraft.world.level.ChunkPos pos, final String reason) {
+ if (!isTickThreadFor(world, pos)) {
+ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
+ throw new IllegalStateException(reason);
+ }
+ }
+
+
public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) {
if (!isTickThreadFor(world, chunkX, chunkZ)) {
MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
@@ -44,6 +67,21 @@ public final class TickThread extends Thread {
}
}
+ public static void ensureTickThread(final ServerLevel world, final net.minecraft.world.phys.AABB aabb, final String reason) {
+ if (!isTickThreadFor(world, aabb)) {
+ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
+ throw new IllegalStateException(reason);
+ }
+ }
+
+ public static void ensureTickThread(final ServerLevel world, final double blockX, final double blockZ, final String reason) {
+ if (!isTickThreadFor(world, blockX, blockZ)) {
+ MinecraftServer.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();
@@ -66,13 +104,45 @@ public final class TickThread extends Thread {
}
public static boolean isTickThread() {
- return Bukkit.isPrimaryThread();
+ return Thread.currentThread() instanceof TickThread;
+ }
+
+ public static boolean isShutdownThread() {
+ return false;
+ }
+
+ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.core.BlockPos pos) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.level.ChunkPos pos) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.phys.Vec3 pos) {
+ return isTickThread();
}
public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) {
return isTickThread();
}
+ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.phys.AABB aabb) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final ServerLevel world, final double blockX, final double blockZ) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.phys.Vec3 position, final net.minecraft.world.phys.Vec3 deltaMovement, final int buffer) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final ServerLevel world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) {
+ return isTickThread();
+ }
+
public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) {
return isTickThread();
}
diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java
index c33f85b570f159ab465b5a10a8044a81f2797f43..244a19ecd0234fa1d7a6ecfea20751595688605d 100644
--- a/src/main/java/net/minecraft/server/Main.java
+++ b/src/main/java/net/minecraft/server/Main.java
@@ -320,6 +320,7 @@ public class Main {
convertable_conversionsession.saveDataTag(iregistrycustom_dimension, savedata);
*/
+ Class.forName(net.minecraft.world.entity.npc.VillagerTrades.class.getName()); // Paper - load this sync so it won't fail later async
final DedicatedServer dedicatedserver = (DedicatedServer) MinecraftServer.spin((thread) -> {
DedicatedServer dedicatedserver1 = new DedicatedServer(optionset, worldLoader.get(), thread, convertable_conversionsession, resourcepackrepository, worldstem, dedicatedserversettings, DataFixers.getDataFixer(), services, LoggerChunkProgressListener::createFromGameruleRadius);
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index e14c0e1ccf526f81e28db5545d9e2351641e1bc8..91cc3eb6db2875710064f6b31413ccc84af56bb2 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -198,7 +198,7 @@ import org.bukkit.event.server.ServerLoadEvent;
import co.aikar.timings.MinecraftTimings; // Paper
-public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource, AutoCloseable {
+public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource, AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer { // Paper - rewrite chunk system
private static MinecraftServer SERVER; // Paper
public static final Logger LOGGER = LogUtils.getLogger();
@@ -322,7 +322,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
AtomicReference<S> atomicreference = new AtomicReference();
- Thread thread = new Thread(() -> {
+ Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system
((MinecraftServer) atomicreference.get()).runServer();
}, "Server thread");
@@ -341,6 +341,77 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
return s0;
}
+ // Paper start - rewrite chunk system
+ private volatile Throwable chunkSystemCrash;
+
+ @Override
+ public final void moonrise$setChunkSystemCrash(final Throwable throwable) {
+ this.chunkSystemCrash = throwable;
+ }
+
+ private static final long CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME = 25L * 1000L; // 25us
+ private static final long MAX_CHUNK_EXEC_TIME = 1000L; // 1us
+ private static final long TASK_EXECUTION_FAILURE_BACKOFF = 5L * 1000L; // 5us
+
+ private long lastMidTickExecute;
+ private long lastMidTickExecuteFailure;
+
+ private boolean tickMidTickTasks() {
+ // give all worlds a fair chance at by targeting them all.
+ // if we execute too many tasks, that's fine - we have logic to correctly handle overuse of allocated time.
+ boolean executed = false;
+ for (final ServerLevel world : this.getAllLevels()) {
+ long currTime = System.nanoTime();
+ if (currTime - ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getLastMidTickFailure() <= TASK_EXECUTION_FAILURE_BACKOFF) {
+ continue;
+ }
+ if (!world.getChunkSource().pollTask()) {
+ // we need to back off if this fails
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$setLastMidTickFailure(currTime);
+ } else {
+ executed = true;
+ }
+ }
+
+ return executed;
+ }
+
+ @Override
+ public final void moonrise$executeMidTickTasks() {
+ final long startTime = System.nanoTime();
+ if ((startTime - this.lastMidTickExecute) <= CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME || (startTime - this.lastMidTickExecuteFailure) <= TASK_EXECUTION_FAILURE_BACKOFF) {
+ // it's shown to be bad to constantly hit the queue (chunk loads slow to a crawl), even if no tasks are executed.
+ // so, backoff to prevent this
+ return;
+ }
+
+ for (;;) {
+ final boolean moreTasks = this.tickMidTickTasks();
+ final long currTime = System.nanoTime();
+ final long diff = currTime - startTime;
+
+ if (!moreTasks || diff >= MAX_CHUNK_EXEC_TIME) {
+ if (!moreTasks) {
+ this.lastMidTickExecuteFailure = currTime;
+ }
+
+ // note: negative values reduce the time
+ long overuse = diff - MAX_CHUNK_EXEC_TIME;
+ if (overuse >= (10L * 1000L * 1000L)) { // 10ms
+ // make sure something like a GC or dumb plugin doesn't screw us over...
+ overuse = 10L * 1000L * 1000L; // 10ms
+ }
+
+ final double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME;
+ final long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME);
+
+ this.lastMidTickExecute = currTime + extraSleep;
+ return;
+ }
+ }
+ }
+ // Paper end - rewrite chunk system
+
public MinecraftServer(OptionSet options, WorldLoader.DataLoadContext worldLoader, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) {
super("Server");
SERVER = this; // Paper - better singleton
@@ -657,7 +728,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.forceDifficulty();
for (ServerLevel worldserver : this.getAllLevels()) {
this.prepareLevels(worldserver.getChunkSource().chunkMap.progressListener, worldserver);
- worldserver.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API
+ // Paper - rewrite chunk system
this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(worldserver.getWorld()));
}
@@ -870,6 +941,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
public abstract boolean shouldRconBroadcast();
public boolean saveAllChunks(boolean suppressLogs, boolean flush, boolean force) {
+ // Paper start - add close param
+ return this.saveAllChunks(suppressLogs, flush, force, false);
+ }
+ public boolean saveAllChunks(boolean suppressLogs, boolean flush, boolean force, boolean close) {
+ // Paper end - add close param
boolean flag3 = false;
for (Iterator iterator = this.getAllLevels().iterator(); iterator.hasNext(); flag3 = true) {
@@ -879,7 +955,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
MinecraftServer.LOGGER.info("Saving chunks for level '{}'/{}", worldserver, worldserver.dimension().location());
}
- worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force);
+ worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force, close); // Paper - add close param
}
// CraftBukkit start - moved to WorldServer.save
@@ -980,7 +1056,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
}
- while (this.levels.values().stream().anyMatch((worldserver1) -> {
+ while (false && this.levels.values().stream().anyMatch((worldserver1) -> { // Paper - rewrite chunk system
return worldserver1.getChunkSource().chunkMap.hasWork();
})) {
this.nextTickTimeNanos = Util.getNanos() + TimeUtil.NANOSECONDS_PER_MILLISECOND;
@@ -997,19 +1073,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.waitUntilNextTick();
}
- this.saveAllChunks(false, true, false);
- iterator = this.getAllLevels().iterator();
-
- while (iterator.hasNext()) {
- worldserver = (ServerLevel) iterator.next();
- if (worldserver != null) {
- try {
- worldserver.close();
- } catch (IOException ioexception) {
- MinecraftServer.LOGGER.error("Exception closing the level", ioexception);
- }
- }
- }
+ this.saveAllChunks(false, true, true, true); // Paper - rewrite chunk system
this.isSaving = false;
this.resources.close();
@@ -1029,6 +1093,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
// Spigot end
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.deinit(); // Paper - rewrite chunk system
}
public String getLocalIp() {
@@ -1203,6 +1268,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.tickServer(flag ? () -> {
return false;
} : this::haveTime);
+ // Paper start - rewrite chunk system
+ final Throwable crash = this.chunkSystemCrash;
+ if (crash != null) {
+ this.chunkSystemCrash = null;
+ throw new RuntimeException("Chunk system crash propagated to tick()", crash);
+ }
+ // Paper end - rewrite chunk system
this.profiler.popPush("nextTickWait");
this.mayHaveDelayedTasks = true;
this.delayedTasksMaxNextTickTimeNanos = Math.max(Util.getNanos() + i, this.nextTickTimeNanos);
@@ -1392,6 +1464,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
private boolean pollTaskInternal() {
if (super.pollTask()) {
+ this.moonrise$executeMidTickTasks(); // Paper - rewrite chunk system
return true;
} else {
boolean ret = false; // Paper - force execution of all worlds, do not just bias the first
@@ -1594,7 +1667,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// Paper start - Folia scheduler API
((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) Bukkit.getGlobalRegionScheduler()).tick();
getAllLevels().forEach(level -> {
- for (final Entity entity : level.getEntities().getAll()) {
+ for (final Entity entity : level.moonrise$getEntityLookup().getAllCopy()) { // Paper - rewrite chunk system
if (entity.isRemoved()) {
continue;
}
@@ -2656,6 +2729,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
+ // Paper start - rewrite chunk system
+ @Override
+ public boolean isSameThread() {
+ return io.papermc.paper.util.TickThread.isTickThread();
+ }
+ // Paper end - rewrite chunk system
+
// CraftBukkit start
public boolean isDebugging() {
return false;
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
index 0761d5bc5f2813bb4a9f664ac7a05b9744d0a778..7d2896918ff5fed37e5de5a22c37b0c7f32634a8 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
@@ -476,7 +476,33 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
return world.dimension() == net.minecraft.world.level.Level.NETHER ? this.getProperties().allowNether : true;
}
+ private static final java.util.concurrent.atomic.AtomicInteger ASYNC_DEBUG_CHUNKS_COUNT = new java.util.concurrent.atomic.AtomicInteger(); // Paper - rewrite chunk system
+
public void handleConsoleInput(String command, CommandSourceStack commandSource) {
+ // Paper start - rewrite chunk system
+ if (command.equalsIgnoreCase("paper debug chunks --async")) {
+ LOGGER.info("Scheduling async debug chunks");
+ Runnable run = () -> {
+ LOGGER.info("Async debug chunks executing");
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(this, false);
+ CommandSender sender = MinecraftServer.getServer().console;
+ java.io.File file = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getChunkDebugFile();
+ sender.sendMessage(net.kyori.adventure.text.Component.text("Writing chunk information dump to " + file, net.kyori.adventure.text.format.NamedTextColor.GREEN));
+ try {
+ ca.spottedleaf.moonrise.common.util.JsonUtil.writeJson(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.debugAllWorlds(this), file);
+ sender.sendMessage(net.kyori.adventure.text.Component.text("Successfully written chunk information!", net.kyori.adventure.text.format.NamedTextColor.GREEN));
+ } catch (Throwable thr) {
+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
+ sender.sendMessage(net.kyori.adventure.text.Component.text("Failed to dump chunk information, see console", net.kyori.adventure.text.format.NamedTextColor.RED));
+ }
+ };
+ Thread t = new Thread(run);
+ t.setName("Async debug thread #" + ASYNC_DEBUG_CHUNKS_COUNT.getAndIncrement());
+ t.setDaemon(true);
+ t.start();
+ return;
+ }
+ // Paper end - rewrite chunk system
this.serverCommandQueue.add(new ConsoleInput(command, commandSource)); // Paper - Perf: use proper queue
}
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
index c643bb0daa5cd264fd6ebab7acf0a2bdd7fe7029..9bc59697fc71d4e3c226aa7fe958f57193fc4bd4 100644
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -32,28 +32,20 @@ import net.minecraft.world.level.lighting.LevelLightEngine;
import net.minecraft.server.MinecraftServer;
// CraftBukkit end
-public class ChunkHolder extends GenerationChunkHolder {
+public class ChunkHolder extends GenerationChunkHolder implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder { // Paper - rewrite chunk system
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 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;
+ // Paper - rewrite chunk system
private boolean hasChangedSections;
private final ShortSet[] changedBlocksPerSection;
private final BitSet blockChangedLightSectionFilter;
private final BitSet skyChangedLightSectionFilter;
private final LevelLightEngine lightEngine;
- private final ChunkHolder.LevelChangeListener onLevelChange;
+ // Paper - rewrite chunk system
public final ChunkHolder.PlayerProvider playerProvider;
- private boolean wasAccessibleSinceLastSave;
- private CompletableFuture<?> pendingFullStateConfirmation;
- private CompletableFuture<?> sendSync;
- private CompletableFuture<?> saveSync;
+ // Paper - rewrite chunk system
private final ChunkMap chunkMap; // Paper
@@ -67,23 +59,110 @@ public class ChunkHolder extends GenerationChunkHolder {
}
// Paper end
+ // Paper start - rewrite chunk system
+ private ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder;
+
+ private static final ServerPlayer[] EMPTY_PLAYER_ARRAY = new ServerPlayer[0];
+ private final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> playersSentChunkTo = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_ARRAY, 0);
+
+ private ChunkMap getChunkMap() {
+ return (ChunkMap)this.playerProvider;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder moonrise$getRealChunkHolder() {
+ return this.newChunkHolder;
+ }
+
+ @Override
+ public final void moonrise$setRealChunkHolder(final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder) {
+ this.newChunkHolder = newChunkHolder;
+ }
+
+ @Override
+ public final void moonrise$addReceivedChunk(final ServerPlayer player) {
+ if (!this.playersSentChunkTo.add(player)) {
+ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player);
+ }
+ }
+
+ @Override
+ public final void moonrise$removeReceivedChunk(final ServerPlayer player) {
+ if (!this.playersSentChunkTo.remove(player)) {
+ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player);
+ }
+ }
+
+ @Override
+ public final boolean moonrise$hasChunkBeenSent() {
+ return this.playersSentChunkTo.size() != 0;
+ }
+
+ @Override
+ public final boolean moonrise$hasChunkBeenSent(final ServerPlayer to) {
+ return this.playersSentChunkTo.contains(to);
+ }
+
+ @Override
+ public final List<ServerPlayer> moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge) {
+ final List<ServerPlayer> ret = new java.util.ArrayList<>();
+ final ServerPlayer[] raw = this.playersSentChunkTo.getRawDataUnchecked();
+ for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) {
+ final ServerPlayer player = raw[i];
+ if (onlyOnWatchDistanceEdge && !((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getPlayerChunkLoader().isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) {
+ continue;
+ }
+ ret.add(player);
+ }
+
+ return ret;
+ }
+
+ @Override
+ public final LevelChunk moonrise$getFullChunk() {
+ if (this.newChunkHolder.isFullChunkReady()) {
+ if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) {
+ return levelChunk;
+ } // else: race condition: chunk unload
+ }
+ return null;
+ }
+
+ private boolean isRadiusLoaded(final int radius) {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getChunkTaskScheduler()
+ .chunkHolderManager;
+ final ChunkPos pos = this.pos;
+ final int chunkX = pos.x;
+ final int chunkZ = pos.z;
+ for (int dz = -radius; dz <= radius; ++dz) {
+ for (int dx = -radius; dx <= radius; ++dx) {
+ if ((dx | dz) == 0) {
+ continue;
+ }
+
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = manager.getChunkHolder(dx + chunkX, dz + chunkZ);
+
+ if (holder == null || !holder.isFullChunkReady()) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+ // Paper end - rewrite chunk system
+
public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) {
super(pos);
- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
+ // Paper - rewrite chunk system
this.blockChangedLightSectionFilter = new BitSet();
this.skyChangedLightSectionFilter = new BitSet();
- this.pendingFullStateConfirmation = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
- this.sendSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
- this.saveSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
+ // Paper - rewrite chunk system
this.levelHeightAccessor = world;
this.lightEngine = lightingProvider;
- this.onLevelChange = levelUpdateListener;
+ // Paper - rewrite chunk system
this.playerProvider = playersWatchingChunkProvider;
- this.oldTicketLevel = ChunkLevel.MAX_LEVEL + 1;
- this.ticketLevel = this.oldTicketLevel;
- this.queueLevel = this.oldTicketLevel;
+ // Paper - rewrite chunk system
this.setTicketLevel(level);
this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()];
this.chunkMap = (ChunkMap)playersWatchingChunkProvider; // Paper
@@ -91,21 +170,13 @@ public class ChunkHolder extends GenerationChunkHolder {
// Paper start
public @Nullable ChunkAccess getAvailableChunkNow() {
- // TODO can we just getStatusFuture(EMPTY)?
- for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) {
- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(curr);
- if (chunkAccess == null) {
- continue;
- }
- return chunkAccess;
- }
- return null;
+ return this.getChunkIfPresent(ChunkStatus.EMPTY); // Paper - rewrite chunk system
}
// Paper end
// CraftBukkit start
public LevelChunk getFullChunkNow() {
// Note: We use the oldTicketLevel for isLoaded checks.
- if (!ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) return null;
+ if (!this.newChunkHolder.isFullChunkReady()) return null; // Paper - rewrite chunk system
return this.getFullChunkNowUnchecked();
}
@@ -115,39 +186,46 @@ public class ChunkHolder extends GenerationChunkHolder {
// CraftBukkit end
public final CompletableFuture<ChunkResult<LevelChunk>> getTickingChunkFuture() { // Paper - final for inline
- return this.tickingChunkFuture;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public final CompletableFuture<ChunkResult<LevelChunk>> getEntityTickingChunkFuture() { // Paper - final for inline
- return this.entityTickingChunkFuture;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public final CompletableFuture<ChunkResult<LevelChunk>> getFullChunkFuture() { // Paper - final for inline
- return this.fullChunkFuture;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Nullable
public final LevelChunk getTickingChunk() { // Paper - final for inline
- return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error
+ // Paper start - rewrite chunk system
+ if (this.newChunkHolder.isTickingReady()) {
+ if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) {
+ return levelChunk;
+ } // else: race condition: chunk unload
+ }
+ return null;
+ // Paper end - rewrite chunk system
}
@Nullable
public LevelChunk getChunkToSend() {
- return !this.sendSync.isDone() ? null : this.getTickingChunk();
+ // Paper start - rewrite chunk system
+ final LevelChunk ret = this.moonrise$getFullChunk();
+ if (ret != null && this.isRadiusLoaded(1)) {
+ return ret;
+ }
+ return null;
+ // Paper end - rewrite chunk system
}
public CompletableFuture<?> getSendSyncFuture() {
- return this.sendSync;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void addSendDependency(CompletableFuture<?> postProcessingFuture) {
- if (this.sendSync.isDone()) {
- this.sendSync = postProcessingFuture;
- } else {
- this.sendSync = this.sendSync.thenCombine(postProcessingFuture, (object, object1) -> {
- return null;
- });
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@@ -166,26 +244,20 @@ public class ChunkHolder extends GenerationChunkHolder {
// Paper end
public CompletableFuture<?> getSaveSyncFuture() {
- return this.saveSync;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public boolean isReadyForSaving() {
- return this.getGenerationRefCount() == 0 && this.saveSync.isDone();
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void addSaveDependency(CompletableFuture<?> savingFuture) {
- if (this.saveSync.isDone()) {
- this.saveSync = savingFuture;
- } else {
- this.saveSync = this.saveSync.thenCombine(savingFuture, (object, object1) -> {
- return null;
- });
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void blockChanged(BlockPos pos) {
- LevelChunk chunk = this.getTickingChunk();
+ LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system
if (chunk != null) {
int i = this.levelHeightAccessor.getSectionIndex(pos.getY());
@@ -205,7 +277,7 @@ public class ChunkHolder extends GenerationChunkHolder {
if (ichunkaccess != null) {
ichunkaccess.setUnsaved(true);
- LevelChunk chunk = this.getTickingChunk();
+ LevelChunk chunk = this.getChunkToSend(); // Paper - rewrite chunk system
if (chunk != null) {
int j = this.lightEngine.getMinLightSection();
@@ -231,7 +303,7 @@ public class ChunkHolder extends GenerationChunkHolder {
List list;
if (!this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) {
- list = this.playerProvider.getPlayers(this.pos, true);
+ list = this.moonrise$getPlayers(true); // Paper - rewrite chunk system
if (!list.isEmpty()) {
ClientboundLightUpdatePacket packetplayoutlightupdate = new ClientboundLightUpdatePacket(chunk.getPos(), this.lightEngine, this.skyChangedLightSectionFilter, this.blockChangedLightSectionFilter);
@@ -243,7 +315,7 @@ public class ChunkHolder extends GenerationChunkHolder {
}
if (this.hasChangedSections) {
- list = this.playerProvider.getPlayers(this.pos, false);
+ list = this.moonrise$getPlayers(false); // Paper - rewrite chunk system
for (int i = 0; i < this.changedBlocksPerSection.length; ++i) {
ShortSet shortset = this.changedBlocksPerSection[i];
@@ -309,193 +381,40 @@ public class ChunkHolder extends GenerationChunkHolder {
@Override
public final int getTicketLevel() { // Paper - final for inline
- return this.ticketLevel;
+ return this.newChunkHolder.getTicketLevel(); // Paper - rewrite chunk system
}
@Override
public int getQueueLevel() {
- return this.queueLevel;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void setQueueLevel(int level) {
- this.queueLevel = level;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void setTicketLevel(int level) {
- this.ticketLevel = level;
+ // Paper - rewrite chunk system
}
private void scheduleFullChunkPromotion(ChunkMap chunkLoadingManager, CompletableFuture<ChunkResult<LevelChunk>> chunkFuture, Executor executor, FullChunkStatus target) {
- this.pendingFullStateConfirmation.cancel(false);
- CompletableFuture<Void> completablefuture1 = new CompletableFuture();
-
- completablefuture1.thenRunAsync(() -> {
- chunkLoadingManager.onFullChunkStatusChange(this.pos, target);
- }, executor);
- this.pendingFullStateConfirmation = completablefuture1;
- chunkFuture.thenAccept((chunkresult) -> {
- chunkresult.ifSuccess((chunk) -> {
- completablefuture1.complete(null); // CraftBukkit - decompile error
- });
- });
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void demoteFullChunk(ChunkMap chunkLoadingManager, FullChunkStatus target) {
- this.pendingFullStateConfirmation.cancel(false);
- chunkLoadingManager.onFullChunkStatusChange(this.pos, target);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
protected void updateFutures(ChunkMap chunkLoadingManager, Executor executor) {
- FullChunkStatus fullchunkstatus = ChunkLevel.fullStatus(this.oldTicketLevel);
- FullChunkStatus fullchunkstatus1 = ChunkLevel.fullStatus(this.ticketLevel);
- boolean flag = fullchunkstatus.isOrAfter(FullChunkStatus.FULL);
- boolean flag1 = fullchunkstatus1.isOrAfter(FullChunkStatus.FULL);
- // CraftBukkit start
- // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins.
- if (flag && !flag1) {
- this.getFullChunkFuture().thenAccept((either) -> {
- LevelChunk chunk = (LevelChunk) either.orElse(null);
- if (chunk != null) {
- chunkLoadingManager.callbackExecutor.execute(() -> {
- // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick
- // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag.
- // These actions may however happen deferred, so we manually set the needsSaving flag already here.
- chunk.setUnsaved(true);
- chunk.unloadCallback();
- });
- }
- }).exceptionally((throwable) -> {
- // ensure exceptions are printed, by default this is not the case
- MinecraftServer.LOGGER.error("Failed to schedule unload callback for chunk " + ChunkHolder.this.pos, throwable);
- return null;
- });
-
- // Run callback right away if the future was already done
- chunkLoadingManager.callbackExecutor.run();
- }
- // CraftBukkit end
-
- 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;
- io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(chunk, this);
- }
- });
- });
- // Paper end - cache ticking ready status
- this.addSaveDependency(this.fullChunkFuture);
- }
-
- if (flag && !flag1) {
- // Paper start
- if (this.isFullChunkReady) {
- io.papermc.paper.chunk.system.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;
- }
-
- boolean flag2 = fullchunkstatus.isOrAfter(FullChunkStatus.BLOCK_TICKING);
- boolean flag3 = fullchunkstatus1.isOrAfter(FullChunkStatus.BLOCK_TICKING);
-
- 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;
- io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(chunk, this);
- });
- });
- // Paper end
- this.addSaveDependency(this.tickingChunkFuture);
- }
-
- if (flag2 && !flag3) {
- // Paper start
- if (this.isTickingReady) {
- io.papermc.paper.chunk.system.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;
- }
-
- boolean flag4 = fullchunkstatus.isOrAfter(FullChunkStatus.ENTITY_TICKING);
- boolean flag5 = fullchunkstatus1.isOrAfter(FullChunkStatus.ENTITY_TICKING);
-
- if (!flag4 && flag5) {
- if (this.entityTickingChunkFuture != ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE) {
- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException());
- }
-
- 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;
- io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(chunk, this);
- });
- });
- // Paper end
- this.addSaveDependency(this.entityTickingChunkFuture);
- }
-
- if (flag4 && !flag5) {
- // Paper start
- if (this.isEntityTickingReady) {
- io.papermc.paper.chunk.system.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;
- }
-
- if (!fullchunkstatus1.isOrAfter(fullchunkstatus)) {
- this.demoteFullChunk(chunkLoadingManager, fullchunkstatus1);
- }
-
- this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel);
- this.oldTicketLevel = this.ticketLevel;
- // CraftBukkit start
- // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins.
- if (!fullchunkstatus.isOrAfter(FullChunkStatus.FULL) && fullchunkstatus1.isOrAfter(FullChunkStatus.FULL)) {
- this.getFullChunkFuture().thenAccept((either) -> {
- LevelChunk chunk = (LevelChunk) either.orElse(null);
- if (chunk != null) {
- chunkLoadingManager.callbackExecutor.execute(() -> {
- chunk.loadCallback();
- });
- }
- }).exceptionally((throwable) -> {
- // ensure exceptions are printed, by default this is not the case
- MinecraftServer.LOGGER.error("Failed to schedule load callback for chunk " + ChunkHolder.this.pos, throwable);
- return null;
- });
-
- // Run callback right away if the future was already done
- chunkLoadingManager.callbackExecutor.run();
- }
- // CraftBukkit end
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public boolean wasAccessibleSinceLastSave() {
- return this.wasAccessibleSinceLastSave;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void refreshAccessibility() {
- this.wasAccessibleSinceLastSave = ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@FunctionalInterface
@@ -511,15 +430,15 @@ public class ChunkHolder extends GenerationChunkHolder {
// Paper start
public final boolean isEntityTickingReady() {
- return this.isEntityTickingReady;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public final boolean isTickingReady() {
- return this.isTickingReady;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public final boolean isFullChunkReady() {
- return this.isFullChunkReady;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
// Paper end
}
diff --git a/src/main/java/net/minecraft/server/level/ChunkLevel.java b/src/main/java/net/minecraft/server/level/ChunkLevel.java
index d9ad32acdf46a43a649334a3b736aeb7b3af21d1..fae17a075d7efaf24d916877dd5968eb9652bb66 100644
--- a/src/main/java/net/minecraft/server/level/ChunkLevel.java
+++ b/src/main/java/net/minecraft/server/level/ChunkLevel.java
@@ -7,9 +7,9 @@ import net.minecraft.world.level.chunk.status.ChunkStep;
import org.jetbrains.annotations.Contract;
public class ChunkLevel {
- private static final int FULL_CHUNK_LEVEL = 33;
- private static final int BLOCK_TICKING_LEVEL = 32;
- private static final int ENTITY_TICKING_LEVEL = 31;
+ public static final int FULL_CHUNK_LEVEL = 33;
+ public static final int BLOCK_TICKING_LEVEL = 32;
+ public static final int ENTITY_TICKING_LEVEL = 31;
private static final ChunkStep FULL_CHUNK_STEP = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
public static final int RADIUS_AROUND_FULL_CHUNK = FULL_CHUNK_STEP.accumulatedDependencies().getRadius();
public static final int MAX_LEVEL = 33 + RADIUS_AROUND_FULL_CHUNK;
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index a03385b1b0a2f9b98319137b87d917856d3c632c..1363dda031d1b541d76241812a957a12521cbc05 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -122,10 +122,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
public static final int MIN_VIEW_DISTANCE = 2;
public static final int MAX_VIEW_DISTANCE = 32;
public static final int FORCED_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING);
- public final Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap = new Long2ObjectLinkedOpenHashMap();
- public volatile Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunkMap;
- private final Long2ObjectLinkedOpenHashMap<ChunkHolder> pendingUnloads;
- private final List<ChunkGenerationTask> pendingGenerationTasks;
+ // Paper - rewrite chunk system
public final ServerLevel level;
private final ThreadedLevelLightEngine lightEngine;
private final BlockableEventLoop<Runnable> mainThreadExecutor;
@@ -135,21 +132,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
private final PoiManager poiManager;
public final LongSet toDrop;
private boolean modified;
- private final ChunkTaskPriorityQueueSorter queueSorter;
- private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> worldgenMailbox;
- private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> mainThreadMailbox;
+ // Paper - rewrite chunk system
public final ChunkProgressListener progressListener;
private final ChunkStatusUpdateListener chunkStatusListener;
public final ChunkMap.ChunkDistanceManager distanceManager;
- private final AtomicInteger tickingGenerated;
+ public final AtomicInteger tickingGenerated; // Paper - public
private final String storageName;
private final PlayerMap playerMap;
public final Int2ObjectMap<ChunkMap.TrackedEntity> entityMap;
private final Long2ByteMap chunkTypeCache;
private final Long2LongMap chunkSaveCooldowns;
- private final Queue<Runnable> unloadQueue;
+ // Paper - rewrite chunk system
public int serverViewDistance;
- private final WorldGenContext worldGenContext;
+ public final WorldGenContext worldGenContext; // Paper - public
// CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback()
public final CallbackExecutor callbackExecutor = new CallbackExecutor();
@@ -198,23 +193,21 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
// Paper end
// Paper start
public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
- return this.pendingUnloads.get(io.papermc.paper.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ return null; // Paper - rewrite chunk system
}
public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers;
// Paper end
public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
- this.visibleChunkMap = this.updatingChunkMap.clone();
- this.pendingUnloads = new Long2ObjectLinkedOpenHashMap();
- this.pendingGenerationTasks = new ArrayList();
+ // Paper - rewrite chunk system
this.toDrop = new LongOpenHashSet();
this.tickingGenerated = new AtomicInteger();
this.playerMap = new PlayerMap();
this.entityMap = new Int2ObjectOpenHashMap();
this.chunkTypeCache = new Long2ByteOpenHashMap();
this.chunkSaveCooldowns = new Long2LongOpenHashMap();
- this.unloadQueue = Queues.newConcurrentLinkedQueue();
+ // Paper - rewrite chunk system
Path path = session.getDimensionPath(world.dimension());
this.storageName = path.getFileName().toString();
@@ -245,15 +238,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.chunkStatusListener = chunkStatusChangeListener;
ProcessorMailbox<Runnable> threadedmailbox1 = ProcessorMailbox.create(executor, "light");
- this.queueSorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(threadedmailbox, mailbox, threadedmailbox1), executor, Integer.MAX_VALUE);
- this.worldgenMailbox = this.queueSorter.getProcessor(threadedmailbox, false);
- this.mainThreadMailbox = this.queueSorter.getProcessor(mailbox, false);
- this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), threadedmailbox1, this.queueSorter.getProcessor(threadedmailbox1, false));
+ // Paper - rewrite chunk system
+ this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), threadedmailbox1, null); // Paper - rewrite chunk system
this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor);
this.overworldDataStorage = persistentStateManagerFactory;
this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world.getServer(), world);
this.setServerViewDistance(viewDistance);
- this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, this.mainThreadMailbox);
+ this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, null); // Paper - rewrite chunk system
// Paper start
this.nearbyPlayers = new io.papermc.paper.util.player.NearbyPlayers(this.level);
// Paper end
@@ -292,23 +283,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
boolean isChunkTracked(ServerPlayer player, int chunkX, int chunkZ) {
- return player.getChunkTrackingView().contains(chunkX, chunkZ) && !player.connection.chunkSender.isPending(ChunkPos.asLong(chunkX, chunkZ));
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ); // Paper - rewrite chunk system
}
private boolean isChunkOnTrackedBorder(ServerPlayer player, int chunkX, int chunkZ) {
- if (!this.isChunkTracked(player, chunkX, chunkZ)) {
- return false;
- } else {
- for (int k = -1; k <= 1; ++k) {
- for (int l = -1; l <= 1; ++l) {
- if ((k != 0 || l != 0) && !this.isChunkTracked(player, chunkX + k, chunkZ + l)) {
- return true;
- }
- }
- }
-
- return false;
- }
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ, true); // Paper - rewrite chunk system
}
protected ThreadedLevelLightEngine getLightEngine() {
@@ -317,20 +296,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
@Nullable
protected ChunkHolder getUpdatingChunkIfPresent(long pos) {
- return (ChunkHolder) this.updatingChunkMap.get(pos);
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
+ return holder == null ? null : holder.vanillaChunkHolder;
+ // Paper end - rewrite chunk system
}
@Nullable
public ChunkHolder getVisibleChunkIfPresent(long pos) {
- return (ChunkHolder) this.visibleChunkMap.get(pos);
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
+ return holder == null ? null : holder.vanillaChunkHolder;
+ // Paper end - rewrite chunk system
}
protected IntSupplier getChunkQueueLevel(long pos) {
- return () -> {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
-
- return playerchunk == null ? ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1 : Math.min(playerchunk.getQueueLevel(), ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1);
- };
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public String getChunkDebugData(ChunkPos chunkPos) {
@@ -359,55 +340,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
private CompletableFuture<ChunkResult<List<ChunkAccess>>> getChunkRangeFuture(ChunkHolder centerChunk, int margin, IntFunction<ChunkStatus> distanceToStatus) {
- if (margin == 0) {
- ChunkStatus chunkstatus = (ChunkStatus) distanceToStatus.apply(0);
-
- return centerChunk.scheduleChunkGenerationTask(chunkstatus, this).thenApply((chunkresult) -> {
- return chunkresult.map(List::of);
- });
- } else {
- List<CompletableFuture<ChunkResult<ChunkAccess>>> list = new ArrayList();
- ChunkPos chunkcoordintpair = centerChunk.getPos();
-
- for (int j = -margin; j <= margin; ++j) {
- for (int k = -margin; k <= margin; ++k) {
- int l = Math.max(Math.abs(k), Math.abs(j));
- long i1 = ChunkPos.asLong(chunkcoordintpair.x + k, chunkcoordintpair.z + j);
- ChunkHolder playerchunk1 = this.getUpdatingChunkIfPresent(i1);
-
- if (playerchunk1 == null) {
- return ChunkMap.UNLOADED_CHUNK_LIST_FUTURE;
- }
-
- ChunkStatus chunkstatus1 = (ChunkStatus) distanceToStatus.apply(l);
-
- list.add(playerchunk1.scheduleChunkGenerationTask(chunkstatus1, this));
- }
- }
-
- return Util.sequence(list).thenApply((list1) -> {
- List<ChunkAccess> list2 = Lists.newArrayList();
- Iterator iterator = list1.iterator();
-
- while (iterator.hasNext()) {
- ChunkResult<ChunkAccess> chunkresult = (ChunkResult) iterator.next();
-
- if (chunkresult == null) {
- throw this.debugFuturesAndCreateReportedException(new IllegalStateException("At least one of the chunk futures were null"), "n/a");
- }
-
- ChunkAccess ichunkaccess = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error
-
- if (ichunkaccess == null) {
- return ChunkMap.UNLOADED_CHUNK_LIST_RESULT;
- }
-
- list2.add(ichunkaccess);
- }
-
- return ChunkResult.of(list2);
- });
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public ReportedException debugFuturesAndCreateReportedException(IllegalStateException exception, String details) {
@@ -437,93 +370,23 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public CompletableFuture<ChunkResult<LevelChunk>> prepareEntityTickingChunk(ChunkHolder holder) {
- return this.getChunkRangeFuture(holder, 2, (i) -> {
- return ChunkStatus.FULL;
- }).thenApplyAsync((chunkresult) -> {
- return chunkresult.map((list) -> {
- return (LevelChunk) list.get(list.size() / 2);
- });
- }, this.mainThreadExecutor);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Nullable
ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) {
- if (!ChunkLevel.isLoaded(k) && !ChunkLevel.isLoaded(level)) {
- return holder;
- } else {
- if (holder != null) {
- holder.setTicketLevel(level);
- }
-
- if (holder != null) {
- if (!ChunkLevel.isLoaded(level)) {
- this.toDrop.add(pos);
- } else {
- this.toDrop.remove(pos);
- }
- }
-
- if (ChunkLevel.isLoaded(level) && holder == null) {
- holder = (ChunkHolder) this.pendingUnloads.remove(pos);
- if (holder != null) {
- holder.setTicketLevel(level);
- } else {
- holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this);
- // Paper start
- io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderCreate(this.level, holder);
- // Paper end
- }
-
- // Paper start
- holder.onChunkAdd();
- // Paper end
- this.updatingChunkMap.put(pos, holder);
- this.modified = true;
- }
-
- return holder;
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
public void close() throws IOException {
- try {
- this.queueSorter.close();
- this.poiManager.close();
- } finally {
- super.close();
- }
-
+ throw new UnsupportedOperationException("Use ServerChunkCache#close"); // Paper - rewrite chunk system
}
protected void saveAllChunks(boolean flush) {
- if (flush) {
- List<ChunkHolder> list = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper
- MutableBoolean mutableboolean = new MutableBoolean();
-
- do {
- mutableboolean.setFalse();
- list.stream().map((playerchunk) -> {
- BlockableEventLoop iasynctaskhandler = this.mainThreadExecutor;
-
- Objects.requireNonNull(playerchunk);
- iasynctaskhandler.managedBlock(playerchunk::isReadyForSaving);
- return playerchunk.getLatestChunk();
- }).filter((ichunkaccess) -> {
- return ichunkaccess instanceof ImposterProtoChunk || ichunkaccess instanceof LevelChunk;
- }).filter(this::save).forEach((ichunkaccess) -> {
- mutableboolean.setTrue();
- });
- } while (mutableboolean.isTrue());
-
- this.processUnloads(() -> {
- return true;
- });
- this.flushWorker();
- } else {
- io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded);
- }
-
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.saveAllChunks(
+ flush, false, false
+ );
}
protected void tick(BooleanSupplier shouldKeepTicking) {
@@ -540,134 +403,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public boolean hasWork() {
- return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || io.papermc.paper.chunk.system.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void processUnloads(BooleanSupplier shouldKeepTicking) {
- LongIterator longiterator = this.toDrop.iterator();
- int i = 0;
-
- while (longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000)) {
- long j = longiterator.nextLong();
- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(j);
-
- if (playerchunk != null) {
- if (playerchunk.getGenerationRefCount() != 0) {
- continue;
- }
-
- this.updatingChunkMap.remove(j);
- playerchunk.onChunkRemove(); // Paper
- this.pendingUnloads.put(j, playerchunk);
- this.modified = true;
- ++i;
- this.scheduleUnload(j, playerchunk);
- }
-
- longiterator.remove();
- }
-
- int k = Math.max(0, this.unloadQueue.size() - 2000);
-
- Runnable runnable;
-
- while ((shouldKeepTicking.getAsBoolean() || k > 0) && (runnable = (Runnable) this.unloadQueue.poll()) != null) {
- --k;
- runnable.run();
- }
-
- int l = 0;
- Iterator<ChunkHolder> objectiterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
-
- while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) {
- if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) {
- ++l;
- }
- }
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processUnloads(); // Paper - rewrite chunk system
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.autoSave(); // Paper - rewrite chunk system
}
private void scheduleUnload(long pos, ChunkHolder holder) {
- CompletableFuture completablefuture = holder.getSaveSyncFuture();
- Runnable runnable = () -> {
- if (!holder.isReadyForSaving()) {
- this.scheduleUnload(pos, holder);
- } else {
- ChunkAccess ichunkaccess = holder.getLatestChunk();
-
- // Paper start
- boolean removed;
- if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) {
- io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder);
- // Paper end
- LevelChunk chunk;
-
- if (ichunkaccess instanceof LevelChunk) {
- chunk = (LevelChunk) ichunkaccess;
- chunk.setLoaded(false);
- }
-
- this.save(ichunkaccess);
- if (ichunkaccess instanceof LevelChunk) {
- chunk = (LevelChunk) ichunkaccess;
- this.level.unload(chunk);
- }
-
- this.lightEngine.updateChunkStatus(ichunkaccess.getPos());
- this.lightEngine.tryScheduleUpdate();
- this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null);
- this.chunkSaveCooldowns.remove(ichunkaccess.getPos().toLong());
- } else if (removed) { // Paper start
- io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder);
- } // Paper end
-
- }
- };
- Queue queue = this.unloadQueue;
-
- Objects.requireNonNull(this.unloadQueue);
- completablefuture.thenRunAsync(runnable, queue::add).whenComplete((ovoid, throwable) -> {
- if (throwable != null) {
- ChunkMap.LOGGER.error("Failed to save chunk {}", holder.getPos(), throwable);
- }
-
- });
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
protected boolean promoteChunkMap() {
- if (!this.modified) {
- return false;
- } else {
- this.visibleChunkMap = this.updatingChunkMap.clone();
- this.modified = false;
- return true;
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private CompletableFuture<ChunkAccess> scheduleChunkLoad(ChunkPos pos) {
- return this.readChunk(pos).thenApply((optional) -> {
- return optional.filter((nbttagcompound) -> {
- boolean flag = ChunkMap.isChunkDataValid(nbttagcompound);
-
- if (!flag) {
- ChunkMap.LOGGER.error("Chunk file at {} is missing level data, skipping", pos);
- }
-
- return flag;
- });
- }).thenApplyAsync((optional) -> {
- this.level.getProfiler().incrementCounter("chunkLoad");
- if (optional.isPresent()) {
- ProtoChunk protochunk = ChunkSerializer.read(this.level, this.poiManager, this.storageInfo(), pos, (CompoundTag) optional.get());
-
- this.markPosition(pos, protochunk.getPersistedStatus().getChunkType());
- return protochunk;
- } else {
- return this.createEmptyChunk(pos);
- }
- }, this.mainThreadExecutor).exceptionallyAsync((throwable) -> {
- return this.handleChunkLoadFailure(throwable, pos);
- }, this.mainThreadExecutor);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private static boolean isChunkDataValid(CompoundTag nbt) {
@@ -727,137 +481,44 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
@Override
public GenerationChunkHolder acquireGeneration(long pos) {
- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(pos);
-
- playerchunk.increaseGenerationRefCount();
- return playerchunk;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
public void releaseGeneration(GenerationChunkHolder chunkHolder) {
- chunkHolder.decreaseGenerationRefCount();
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
public CompletableFuture<ChunkAccess> applyStep(GenerationChunkHolder chunkHolder, ChunkStep step, StaticCache2D<GenerationChunkHolder> chunks) {
- ChunkPos chunkcoordintpair = chunkHolder.getPos();
-
- if (step.targetStatus() == ChunkStatus.EMPTY) {
- return this.scheduleChunkLoad(chunkcoordintpair);
- } else {
- try {
- GenerationChunkHolder generationchunkholder1 = (GenerationChunkHolder) chunks.get(chunkcoordintpair.x, chunkcoordintpair.z);
- ChunkAccess ichunkaccess = generationchunkholder1.getChunkIfPresentUnchecked(step.targetStatus().getParent());
-
- if (ichunkaccess == null) {
- throw new IllegalStateException("Parent chunk missing");
- } else {
- CompletableFuture<ChunkAccess> completablefuture = step.apply(this.worldGenContext, chunks, ichunkaccess);
-
- this.progressListener.onStatusChange(chunkcoordintpair, step.targetStatus());
- return completablefuture;
- }
- } catch (Exception exception) {
- exception.getStackTrace();
- CrashReport crashreport = CrashReport.forThrowable(exception, "Exception generating new chunk");
- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk to be generated");
-
- crashreportsystemdetails.setDetail("Status being generated", () -> {
- return step.targetStatus().getName();
- });
- crashreportsystemdetails.setDetail("Location", (Object) String.format(Locale.ROOT, "%d,%d", chunkcoordintpair.x, chunkcoordintpair.z));
- crashreportsystemdetails.setDetail("Position hash", (Object) ChunkPos.asLong(chunkcoordintpair.x, chunkcoordintpair.z));
- crashreportsystemdetails.setDetail("Generator", (Object) this.generator());
- this.mainThreadExecutor.execute(() -> {
- throw new ReportedException(crashreport);
- });
- throw new ReportedException(crashreport);
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
public ChunkGenerationTask scheduleGenerationTask(ChunkStatus requestedStatus, ChunkPos pos) {
- ChunkGenerationTask chunkgenerationtask = ChunkGenerationTask.create(this, requestedStatus, pos);
-
- this.pendingGenerationTasks.add(chunkgenerationtask);
- return chunkgenerationtask;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void runGenerationTask(ChunkGenerationTask chunkLoader) {
- this.worldgenMailbox.tell(ChunkTaskPriorityQueueSorter.message(chunkLoader.getCenter(), () -> {
- CompletableFuture<?> completablefuture = chunkLoader.runUntilWait();
-
- if (completablefuture != null) {
- completablefuture.thenRun(() -> {
- this.runGenerationTask(chunkLoader);
- });
- }
- }));
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
public void runGenerationTasks() {
- this.pendingGenerationTasks.forEach(this::runGenerationTask);
- this.pendingGenerationTasks.clear();
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public CompletableFuture<ChunkResult<LevelChunk>> prepareTickingChunk(ChunkHolder holder) {
- CompletableFuture<ChunkResult<List<ChunkAccess>>> completablefuture = this.getChunkRangeFuture(holder, 1, (i) -> {
- return ChunkStatus.FULL;
- });
- CompletableFuture<ChunkResult<LevelChunk>> completablefuture1 = completablefuture.thenApplyAsync((chunkresult) -> {
- return chunkresult.map((list) -> {
- return (LevelChunk) list.get(list.size() / 2);
- });
- }, (runnable) -> {
- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable));
- }).thenApplyAsync((chunkresult) -> {
- return chunkresult.ifSuccess((chunk) -> {
- chunk.postProcessGeneration();
- this.level.startTickingChunk(chunk);
- CompletableFuture<?> completablefuture2 = holder.getSendSyncFuture();
-
- if (completablefuture2.isDone()) {
- this.onChunkReadyToSend(chunk);
- } else {
- completablefuture2.thenAcceptAsync((object) -> {
- this.onChunkReadyToSend(chunk);
- }, this.mainThreadExecutor);
- }
-
- });
- }, this.mainThreadExecutor);
-
- completablefuture1.handle((chunkresult, throwable) -> {
- this.tickingGenerated.getAndIncrement();
- return null;
- });
- return completablefuture1;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void onChunkReadyToSend(LevelChunk chunk) {
- ChunkPos chunkcoordintpair = chunk.getPos();
- Iterator iterator = this.playerMap.getAllPlayers().iterator();
-
- while (iterator.hasNext()) {
- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-
- if (entityplayer.getChunkTrackingView().contains(chunkcoordintpair)) {
- ChunkMap.markChunkPendingToSend(entityplayer, chunk);
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public CompletableFuture<ChunkResult<LevelChunk>> prepareAccessibleChunk(ChunkHolder holder) {
- return this.getChunkRangeFuture(holder, 1, ChunkLevel::getStatusAroundFullChunk).thenApplyAsync((chunkresult) -> {
- return chunkresult.map((list) -> {
- return (LevelChunk) list.get(list.size() / 2);
- });
- }, (runnable) -> {
- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable));
- });
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public int getTickingGenerated() {
@@ -865,135 +526,84 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
private boolean saveChunkIfNeeded(ChunkHolder chunkHolder) {
- if (chunkHolder.wasAccessibleSinceLastSave() && chunkHolder.isReadyForSaving()) {
- ChunkAccess ichunkaccess = chunkHolder.getLatestChunk();
-
- if (!(ichunkaccess instanceof ImposterProtoChunk) && !(ichunkaccess instanceof LevelChunk)) {
- return false;
- } else {
- long i = ichunkaccess.getPos().toLong();
- long j = this.chunkSaveCooldowns.getOrDefault(i, -1L);
- long k = System.currentTimeMillis();
-
- if (k < j) {
- return false;
- } else {
- boolean flag = this.save(ichunkaccess);
-
- chunkHolder.refreshAccessibility();
- if (flag) {
- this.chunkSaveCooldowns.put(i, k + 10000L);
- }
-
- return flag;
- }
- }
- } else {
- return false;
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public boolean save(ChunkAccess chunk) {
- this.poiManager.flush(chunk.getPos());
- if (!chunk.isUnsaved()) {
- return false;
- } else {
- chunk.setUnsaved(false);
- ChunkPos chunkcoordintpair = chunk.getPos();
-
- try {
- ChunkStatus chunkstatus = chunk.getPersistedStatus();
-
- if (chunkstatus.getChunkType() != ChunkType.LEVELCHUNK) {
- if (this.isExistingChunkFull(chunkcoordintpair)) {
- return false;
- }
-
- if (chunkstatus == ChunkStatus.EMPTY && chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) {
- return false;
- }
- }
-
- this.level.getProfiler().incrementCounter("chunkSave");
- CompoundTag nbttagcompound = ChunkSerializer.write(this.level, chunk);
-
- this.write(chunkcoordintpair, nbttagcompound).exceptionally((throwable) -> {
- this.level.getServer().reportChunkSaveFailure(throwable, this.storageInfo(), chunkcoordintpair);
- return null;
- });
- this.markPosition(chunkcoordintpair, chunkstatus.getChunkType());
- return true;
- } catch (Exception exception) {
- this.level.getServer().reportChunkSaveFailure(exception, this.storageInfo(), chunkcoordintpair);
- return false;
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private boolean isExistingChunkFull(ChunkPos pos) {
- byte b0 = this.chunkTypeCache.get(pos.toLong());
-
- if (b0 != 0) {
- return b0 == 1;
- } else {
- CompoundTag nbttagcompound;
-
- try {
- nbttagcompound = (CompoundTag) ((Optional) this.readChunk(pos).join()).orElse((Object) null);
- if (nbttagcompound == null) {
- this.markPositionReplaceable(pos);
- return false;
- }
- } catch (Exception exception) {
- ChunkMap.LOGGER.error("Failed to read chunk {}", pos, exception);
- this.markPositionReplaceable(pos);
- return false;
- }
-
- ChunkType chunktype = ChunkSerializer.getChunkTypeFromTag(nbttagcompound);
-
- return this.markPosition(pos, chunktype) == 1;
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void setServerViewDistance(int watchDistance) { // Paper - public
- int j = Mth.clamp(watchDistance, 2, 32);
-
- if (j != this.serverViewDistance) {
- this.serverViewDistance = j;
- this.distanceManager.updatePlayerTickets(this.serverViewDistance);
- Iterator iterator = this.playerMap.getAllPlayers().iterator();
-
- while (iterator.hasNext()) {
- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-
- this.updateChunkTracking(entityplayer);
- }
+ // Paper start - rewrite chunk system
+ final int clamped = Mth.clamp(watchDistance, 2, ca.spottedleaf.moonrise.common.util.MoonriseConstants.MAX_VIEW_DISTANCE);
+ if (clamped == this.serverViewDistance) {
+ return;
}
+ this.serverViewDistance = clamped;
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().setLoadDistance(this.serverViewDistance + 1);
+ // Paper end - rewrite chunk system
}
public int getPlayerViewDistance(ServerPlayer player) { // Paper - public
- return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance);
+ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getSendViewDistance(player); // Paper - rewrite chunk system
}
private void markChunkPendingToSend(ServerPlayer player, ChunkPos pos) {
- LevelChunk chunk = this.getChunkToSend(pos.toLong());
-
- if (chunk != null) {
- ChunkMap.markChunkPendingToSend(player, chunk);
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private static void markChunkPendingToSend(ServerPlayer player, LevelChunk chunk) {
- player.connection.chunkSender.markChunkPendingToSend(chunk);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private static void dropChunk(ServerPlayer player, ChunkPos pos) {
- player.connection.chunkSender.dropChunk(player, pos);
+ // Paper - rewrite chunk system
+ }
+
+ // Paper start - rewrite chunk system
+ @Override
+ public CompletableFuture<Optional<CompoundTag>> read(final ChunkPos pos) {
+ if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) {
+ try {
+ return CompletableFuture.completedFuture(
+ Optional.ofNullable(
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.loadData(
+ this.level, pos.x, pos.z, ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA,
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread()
+ )
+ )
+ );
+ } catch (final Throwable thr) {
+ return CompletableFuture.failedFuture(thr);
+ }
+ }
+ return super.read(pos);
}
+ @Override
+ public CompletableFuture<Void> write(final ChunkPos pos, final CompoundTag tag) {
+ if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) {
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.scheduleSave(
+ this.level, pos.x, pos.z, tag,
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA);
+ return null;
+ }
+ super.write(pos, tag);
+ return null;
+ }
+
+ @Override
+ public void flushWorker() {
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.flush();
+ }
+ // Paper end - rewrite chunk system
+
@Nullable
public LevelChunk getChunkToSend(long pos) {
ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
@@ -1059,7 +669,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
// CraftBukkit start
- private CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) {
+ public CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) { // Paper - public
return this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, this.generator().getTypeNameForDataFixer(), chunkcoordintpair, this.level);
// CraftBukkit end
}
@@ -1153,7 +763,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
player.setChunkTrackingView(ChunkTrackingView.EMPTY);
- this.updateChunkTracking(player);
+ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system
this.addPlayerToDistanceMaps(player); // Paper - distance maps
} else {
SectionPos sectionposition = player.getLastSectionPos();
@@ -1164,7 +774,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
this.removePlayerFromDistanceMaps(player); // Paper - distance maps
- this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY);
+ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.removePlayerFromDistanceMaps(this.level, player); // Paper - rewrite chunk system
}
}
@@ -1212,71 +822,31 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.playerMap.unIgnorePlayer(player);
}
- this.updateChunkTracking(player);
+ // Paper - rewrite chunk system
}
this.updateMaps(player); // Paper - distance maps
+ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.updateMaps(this.level, player); // Paper - rewrite chunk system
}
private void updateChunkTracking(ServerPlayer player) {
- ChunkPos chunkcoordintpair = player.chunkPosition();
- int i = this.getPlayerViewDistance(player);
- ChunkTrackingView chunktrackingview = player.getChunkTrackingView();
-
- if (chunktrackingview instanceof ChunkTrackingView.Positioned chunktrackingview_a) {
- if (chunktrackingview_a.center().equals(chunkcoordintpair) && chunktrackingview_a.viewDistance() == i) {
- return;
- }
- }
-
- this.applyChunkTrackingView(player, ChunkTrackingView.of(chunkcoordintpair, i));
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void applyChunkTrackingView(ServerPlayer player, ChunkTrackingView chunkFilter) {
- if (player.level() == this.level) {
- ChunkTrackingView chunktrackingview1 = player.getChunkTrackingView();
-
- if (chunkFilter instanceof ChunkTrackingView.Positioned) {
- label15:
- {
- ChunkTrackingView.Positioned chunktrackingview_a = (ChunkTrackingView.Positioned) chunkFilter;
-
- if (chunktrackingview1 instanceof ChunkTrackingView.Positioned) {
- ChunkTrackingView.Positioned chunktrackingview_a1 = (ChunkTrackingView.Positioned) chunktrackingview1;
-
- if (chunktrackingview_a1.center().equals(chunktrackingview_a.center())) {
- break label15;
- }
- }
-
- player.connection.send(new ClientboundSetChunkCacheCenterPacket(chunktrackingview_a.center().x, chunktrackingview_a.center().z));
- }
- }
-
- ChunkTrackingView.difference(chunktrackingview1, chunkFilter, (chunkcoordintpair) -> {
- this.markChunkPendingToSend(player, chunkcoordintpair);
- }, (chunkcoordintpair) -> {
- ChunkMap.dropChunk(player, chunkcoordintpair);
- });
- player.setChunkTrackingView(chunkFilter);
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
public List<ServerPlayer> getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) {
- Set<ServerPlayer> set = this.playerMap.getAllPlayers();
- Builder<ServerPlayer> builder = ImmutableList.builder();
- Iterator iterator = set.iterator();
-
- while (iterator.hasNext()) {
- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-
- if (onlyOnWatchDistanceEdge && this.isChunkOnTrackedBorder(entityplayer, chunkPos.x, chunkPos.z) || !onlyOnWatchDistanceEdge && this.isChunkTracked(entityplayer, chunkPos.x, chunkPos.z)) {
- builder.add(entityplayer);
- }
+ // Paper start - rewrite chunk system
+ final ChunkHolder holder = this.getVisibleChunkIfPresent(chunkPos.toLong());
+ if (holder == null) {
+ return new ArrayList<>();
+ } else {
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)holder).moonrise$getPlayers(onlyOnWatchDistanceEdge);
}
-
- return builder.build();
+ // Paper end - rewrite chunk system
}
public void addEntity(Entity entity) {
@@ -1347,13 +917,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
protected void tick() {
- Iterator iterator = this.playerMap.getAllPlayers().iterator();
-
- while (iterator.hasNext()) {
- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
-
- this.updateChunkTracking(entityplayer);
- }
+ // Paper - rewrite chunk system
List<ServerPlayer> list = Lists.newArrayList();
List<ServerPlayer> list1 = this.level.players();
@@ -1460,27 +1024,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public void waitForLightBeforeSending(ChunkPos centerPos, int radius) {
- int j = radius + 1;
-
- ChunkPos.rangeClosed(centerPos, j).forEach((chunkcoordintpair1) -> {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(chunkcoordintpair1.toLong());
-
- if (playerchunk != null) {
- playerchunk.addSendDependency(this.lightEngine.waitForPendingTasks(chunkcoordintpair1.x, chunkcoordintpair1.z));
- }
-
- });
+ // Paper - rewrite chunk system
}
- public class ChunkDistanceManager extends DistanceManager { // Paper - public
+ public class ChunkDistanceManager extends DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager { // Paper - public // Paper - rewrite chunk system
protected ChunkDistanceManager(final Executor workerExecutor, final Executor mainThreadExecutor) {
super(workerExecutor, mainThreadExecutor, ChunkMap.this); // Paper
}
+ // Paper start - rewrite chunk system
+ @Override
+ public final ChunkMap moonrise$getChunkMap() {
+ return ChunkMap.this;
+ }
+ // Paper end - rewrite chunk system
+
@Override
protected boolean isChunkToRemove(long pos) {
- return ChunkMap.this.toDrop.contains(pos);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Nullable
diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
index cbabbfbb9967ddf9a56f3be24a88e0fcd4415aa2..71abe25cfb73af3857cbc85980aa32d0201aab62 100644
--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
@@ -36,66 +36,36 @@ import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.LevelChunk;
import org.slf4j.Logger;
-public abstract class DistanceManager {
+public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager { // Paper - rewrite chunk system
static final Logger LOGGER = LogUtils.getLogger();
static final int PLAYER_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING);
private static final int INITIAL_TICKET_LIST_CAPACITY = 4;
final Long2ObjectMap<ObjectSet<ServerPlayer>> playersPerChunk = new Long2ObjectOpenHashMap();
- public final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> tickets = new Long2ObjectOpenHashMap();
- private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker();
+ // Paper - rewrite chunk system
private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8);
- private final TickingTracker tickingTicketsTracker = new TickingTracker();
- private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(32);
- final Set<ChunkHolder> chunksToUpdateFutures = Sets.newHashSet();
- final ChunkTaskPriorityQueueSorter ticketThrottler;
- final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> ticketThrottlerInput;
- final ProcessorHandle<ChunkTaskPriorityQueueSorter.Release> ticketThrottlerReleaser;
- final LongSet ticketsToRelease = new LongOpenHashSet();
- final Executor mainThreadExecutor;
+ // Paper - rewrite chunk system
private long ticketTickCounter;
- public int simulationDistance = 10;
+ // Paper - rewrite chunk system
private final ChunkMap chunkMap; // Paper
+ // Paper start - rewrite chunk system
+ public ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager getChunkHolderManager() {
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getChunkTaskScheduler().chunkHolderManager;
+ }
+ // Paper end - rewrite chunk system
+
protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor, ChunkMap chunkMap) {
Objects.requireNonNull(mainThreadExecutor);
ProcessorHandle<Runnable> mailbox = ProcessorHandle.of("player ticket throttler", mainThreadExecutor::execute);
ChunkTaskPriorityQueueSorter chunktaskqueuesorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(mailbox), workerExecutor, 4);
- this.ticketThrottler = chunktaskqueuesorter;
- this.ticketThrottlerInput = chunktaskqueuesorter.getProcessor(mailbox, true);
- this.ticketThrottlerReleaser = chunktaskqueuesorter.getReleaseProcessor(mailbox);
- this.mainThreadExecutor = mainThreadExecutor;
+ // Paper - rewrite chunk system
this.chunkMap = chunkMap; // Paper
}
protected void purgeStaleTickets() {
- ++this.ticketTickCounter;
- ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
-
- while (objectiterator.hasNext()) {
- Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next();
- Iterator<Ticket<?>> iterator = ((SortedArraySet) entry.getValue()).iterator();
- boolean flag = false;
-
- while (iterator.hasNext()) {
- Ticket<?> ticket = (Ticket) iterator.next();
-
- if (ticket.timedOut(this.ticketTickCounter)) {
- iterator.remove();
- flag = true;
- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket);
- }
- }
-
- if (flag) {
- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false);
- }
-
- if (((SortedArraySet) entry.getValue()).isEmpty()) {
- objectiterator.remove();
- }
- }
+ this.getChunkHolderManager().tick(); // Paper - rewrite chunk system
}
@@ -112,86 +82,15 @@ public abstract class DistanceManager {
protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k);
public boolean runAllUpdates(ChunkMap chunkLoadingManager) {
- this.naturalSpawnChunkCounter.runAllUpdates();
- this.tickingTicketsTracker.runAllUpdates();
- this.playerTicketManager.runAllUpdates();
- int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE);
- boolean flag = i != 0;
-
- if (flag) {
- ;
- }
-
- if (!this.chunksToUpdateFutures.isEmpty()) {
- this.chunksToUpdateFutures.forEach((playerchunk) -> {
- playerchunk.updateHighestAllowedStatus(chunkLoadingManager);
- });
- this.chunksToUpdateFutures.forEach((playerchunk) -> {
- playerchunk.updateFutures(chunkLoadingManager, this.mainThreadExecutor);
- });
- this.chunksToUpdateFutures.clear();
- return true;
- } else {
- if (!this.ticketsToRelease.isEmpty()) {
- LongIterator longiterator = this.ticketsToRelease.iterator();
-
- while (longiterator.hasNext()) {
- long j = longiterator.nextLong();
-
- if (this.getTickets(j).stream().anyMatch((ticket) -> {
- return ticket.getType() == TicketType.PLAYER;
- })) {
- ChunkHolder playerchunk = chunkLoadingManager.getUpdatingChunkIfPresent(j);
-
- if (playerchunk == null) {
- throw new IllegalStateException();
- }
-
- CompletableFuture<ChunkResult<LevelChunk>> completablefuture = playerchunk.getEntityTickingChunkFuture();
-
- completablefuture.thenAccept((chunkresult) -> {
- this.mainThreadExecutor.execute(() -> {
- this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> {
- }, j, false));
- });
- });
- }
- }
-
- this.ticketsToRelease.clear();
- }
-
- return flag;
- }
+ return this.getChunkHolderManager().processTicketUpdates(); // Paper - rewrite chunk system
}
boolean addTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
- SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i);
- int j = DistanceManager.getTicketLevelAt(arraysetsorted);
- Ticket<?> ticket1 = (Ticket) arraysetsorted.addOrGet(ticket);
-
- ticket1.setCreatedTick(this.ticketTickCounter);
- if (ticket.getTicketLevel() < j) {
- this.ticketTracker.update(i, ticket.getTicketLevel(), true);
- }
-
- return ticket == ticket1; // CraftBukkit
+ return this.getChunkHolderManager().addTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system
}
boolean removeTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean
- SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i);
-
- boolean removed = false; // CraftBukkit
- if (arraysetsorted.remove(ticket)) {
- removed = true; // CraftBukkit
- }
-
- if (arraysetsorted.isEmpty()) {
- this.tickets.remove(i);
- }
-
- this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false);
- return removed; // CraftBukkit
+ return this.getChunkHolderManager().removeTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system
}
public <T> void addTicket(TicketType<T> type, ChunkPos pos, int level, T argument) {
@@ -210,13 +109,7 @@ public abstract class DistanceManager {
}
public <T> boolean addRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) {
- // CraftBukkit end
- Ticket<T> ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0);
- long j = chunkcoordintpair.toLong();
-
- boolean added = this.addTicket(j, ticket); // CraftBukkit
- this.tickingTicketsTracker.addTicket(j, ticket);
- return added; // CraftBukkit
+ return this.getChunkHolderManager().addTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system
}
public <T> void removeRegionTicket(TicketType<T> type, ChunkPos pos, int radius, T argument) {
@@ -225,32 +118,21 @@ public abstract class DistanceManager {
}
public <T> boolean removeRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) {
- // CraftBukkit end
- Ticket<T> ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0);
- long j = chunkcoordintpair.toLong();
-
- boolean removed = this.removeTicket(j, ticket); // CraftBukkit
- this.tickingTicketsTracker.removeTicket(j, ticket);
- return removed; // CraftBukkit
+ return this.getChunkHolderManager().removeTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system
}
private SortedArraySet<Ticket<?>> getTickets(long position) {
- return (SortedArraySet) this.tickets.computeIfAbsent(position, (j) -> {
- return SortedArraySet.create(4);
- });
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
protected void updateChunkForced(ChunkPos pos, boolean forced) {
- Ticket<ChunkPos> ticket = new Ticket<>(TicketType.FORCED, ChunkMap.FORCED_TICKET_LEVEL, pos);
- long i = pos.toLong();
-
+ // Paper start - rewrite chunk system
if (forced) {
- this.addTicket(i, ticket);
- this.tickingTicketsTracker.addTicket(i, ticket);
+ this.getChunkHolderManager().addTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos);
} else {
- this.removeTicket(i, ticket);
- this.tickingTicketsTracker.removeTicket(i, ticket);
+ this.getChunkHolderManager().removeTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos);
}
+ // Paper end - rewrite chunk system
}
@@ -262,8 +144,7 @@ public abstract class DistanceManager {
return new ObjectOpenHashSet();
})).add(player);
this.naturalSpawnChunkCounter.update(i, 0, true);
- this.playerTicketManager.update(i, 0, true);
- this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair);
+ // Paper - rewrite chunk system
}
public void removePlayer(SectionPos pos, ServerPlayer player) {
@@ -276,39 +157,39 @@ public abstract class DistanceManager {
if (objectset == null || objectset.isEmpty()) { // Paper
this.playersPerChunk.remove(i);
this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false);
- this.playerTicketManager.update(i, Integer.MAX_VALUE, false);
- this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair);
+ // Paper - rewrite chunk system
}
}
private int getPlayerTicketLevel() {
- return Math.max(0, ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING) - this.simulationDistance);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public boolean inEntityTickingRange(long chunkPos) {
- return ChunkLevel.isEntityTicking(this.tickingTicketsTracker.getLevel(chunkPos));
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.getChunkHolderManager().getChunkHolder(chunkPos);
+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
+ // Paper end - rewrite chunk system
}
public boolean inBlockTickingRange(long chunkPos) {
- return ChunkLevel.isBlockTicking(this.tickingTicketsTracker.getLevel(chunkPos));
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.getChunkHolderManager().getChunkHolder(chunkPos);
+ return chunkHolder != null && chunkHolder.isTickingReady();
+ // Paper end - rewrite chunk system
}
protected String getTicketDebugString(long pos) {
- SortedArraySet<Ticket<?>> arraysetsorted = (SortedArraySet) this.tickets.get(pos);
-
- return arraysetsorted != null && !arraysetsorted.isEmpty() ? ((Ticket) arraysetsorted.first()).toString() : "no_ticket";
+ return this.getChunkHolderManager().getTicketDebugString(pos); // Paper - rewrite chunk system
}
protected void updatePlayerTickets(int viewDistance) {
- this.playerTicketManager.updateViewDistance(viewDistance);
+ this.moonrise$getChunkMap().setServerViewDistance(viewDistance); // Paper - rewrite chunk system
}
public void updateSimulationDistance(int simulationDistance) {
- if (simulationDistance != this.simulationDistance) {
- this.simulationDistance = simulationDistance;
- this.tickingTicketsTracker.replacePlayerTicketsLevel(this.getPlayerTicketLevel());
- }
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getPlayerChunkLoader().setTickDistance(simulationDistance); // Paper - rewrite chunk system
}
@@ -323,103 +204,35 @@ public abstract class DistanceManager {
}
public String getDebugStatus() {
- return this.ticketThrottler.getDebugStatus();
+ return "No DistanceManager stats available"; // Paper - rewrite chunk system
}
private void dumpTickets(String path) {
- try {
- FileOutputStream fileoutputstream = new FileOutputStream(new File(path));
-
- try {
- ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().iterator();
-
- while (objectiterator.hasNext()) {
- Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next();
- ChunkPos chunkcoordintpair = new ChunkPos(entry.getLongKey());
- Iterator iterator = ((SortedArraySet) entry.getValue()).iterator();
-
- while (iterator.hasNext()) {
- Ticket<?> ticket = (Ticket) iterator.next();
-
- fileoutputstream.write((chunkcoordintpair.x + "\t" + chunkcoordintpair.z + "\t" + String.valueOf(ticket.getType()) + "\t" + ticket.getTicketLevel() + "\t\n").getBytes(StandardCharsets.UTF_8));
- }
- }
- } catch (Throwable throwable) {
- try {
- fileoutputstream.close();
- } catch (Throwable throwable1) {
- throwable.addSuppressed(throwable1);
- }
-
- throw throwable;
- }
-
- fileoutputstream.close();
- } catch (IOException ioexception) {
- DistanceManager.LOGGER.error("Failed to dump tickets to {}", path, ioexception);
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@VisibleForTesting
TickingTracker tickingTracker() {
- return this.tickingTicketsTracker;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void removeTicketsOnClosing() {
- 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()) {
- Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next();
- Iterator<Ticket<?>> iterator = ((SortedArraySet) entry.getValue()).iterator();
- boolean flag = false;
-
- while (iterator.hasNext()) {
- Ticket<?> ticket = (Ticket) iterator.next();
-
- if (!immutableset.contains(ticket.getType())) {
- iterator.remove();
- flag = true;
- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket);
- }
- }
-
- if (flag) {
- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false);
- }
-
- if (((SortedArraySet) entry.getValue()).isEmpty()) {
- objectiterator.remove();
- }
- }
+ // Paper - rewrite chunk system
}
public boolean hasTickets() {
- return !this.tickets.isEmpty();
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
// CraftBukkit start
public <T> void removeAllTicketsFor(TicketType<T> ticketType, int ticketLevel, T ticketIdentifier) {
- Ticket<T> target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier);
-
- for (java.util.Iterator<Entry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
- Entry<SortedArraySet<Ticket<?>>> entry = iterator.next();
- SortedArraySet<Ticket<?>> tickets = entry.getValue();
- if (tickets.remove(target)) {
- // copied from removeTicket
- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false);
-
- // can't use entry after it's removed
- if (tickets.isEmpty()) {
- iterator.remove();
- }
- }
- }
+ this.getChunkHolderManager().removeAllTicketsFor(ticketType, ticketLevel, ticketIdentifier); // Paper - rewrite chunk system
}
// CraftBukkit end
+ /* // Paper - rewrite chunk system
private class ChunkTicketTracker extends ChunkTracker {
private static final int MAX_LEVEL = ChunkLevel.MAX_LEVEL + 1;
@@ -465,7 +278,7 @@ public abstract class DistanceManager {
public int runDistanceUpdates(int distance) {
return this.runUpdates(distance);
}
- }
+ }*/ // Paper - rewrite chunk system
private class FixedPlayerDistanceChunkTracker extends ChunkTracker {
@@ -545,6 +358,7 @@ public abstract class DistanceManager {
}
}
+ /* // Paper - rewrite chunk system
private class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker {
private int viewDistance = 0;
@@ -639,5 +453,5 @@ public abstract class DistanceManager {
private boolean haveTicketFor(int distance) {
return distance <= this.viewDistance;
}
- }
+ }*/ // Paper - rewrite chunk system
}
diff --git a/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java b/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java
index 3dc1daa3c6a04d3ff1a2353773b465fc380994a2..3575782f13a7f3c52e64dc5046803305d5c8ce12 100644
--- a/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java
@@ -27,249 +27,105 @@ public abstract class GenerationChunkHolder {
public static final ChunkResult<ChunkAccess> UNLOADED_CHUNK = ChunkResult.error("Unloaded chunk");
public static final CompletableFuture<ChunkResult<ChunkAccess>> UNLOADED_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_CHUNK);
protected final ChunkPos pos;
- @Nullable
- private volatile ChunkStatus highestAllowedStatus;
- private final AtomicReference<ChunkStatus> startedWork = new AtomicReference<>();
- private final AtomicReferenceArray<CompletableFuture<ChunkResult<ChunkAccess>>> futures = new AtomicReferenceArray<>(CHUNK_STATUSES.size());
- private final AtomicReference<ChunkGenerationTask> task = new AtomicReference<>();
- private final AtomicInteger generationRefCount = new AtomicInteger();
+ // Paper - rewrite chunk system
public GenerationChunkHolder(ChunkPos pos) {
this.pos = pos;
}
public CompletableFuture<ChunkResult<ChunkAccess>> scheduleChunkGenerationTask(ChunkStatus requestedStatus, ChunkMap chunkLoadingManager) {
- if (this.isStatusDisallowed(requestedStatus)) {
- return UNLOADED_CHUNK_FUTURE;
- } else {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.getOrCreateFuture(requestedStatus);
- if (completableFuture.isDone()) {
- return completableFuture;
- } else {
- ChunkGenerationTask chunkGenerationTask = this.task.get();
- if (chunkGenerationTask == null || requestedStatus.isAfter(chunkGenerationTask.targetStatus)) {
- this.rescheduleChunkTask(chunkLoadingManager, requestedStatus);
- }
-
- return completableFuture;
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
CompletableFuture<ChunkResult<ChunkAccess>> applyStep(ChunkStep step, GeneratingChunkMap chunkLoadingManager, StaticCache2D<GenerationChunkHolder> chunks) {
- if (this.isStatusDisallowed(step.targetStatus())) {
- return UNLOADED_CHUNK_FUTURE;
- } else {
- return this.acquireStatusBump(step.targetStatus()) ? chunkLoadingManager.applyStep(this, step, chunks).handle((chunk, throwable) -> {
- if (throwable != null) {
- CrashReport crashReport = CrashReport.forThrowable(throwable, "Exception chunk generation/loading");
- MinecraftServer.setFatalException(new ReportedException(crashReport));
- } else {
- this.completeFuture(step.targetStatus(), chunk);
- }
-
- return ChunkResult.of(chunk);
- }) : this.getOrCreateFuture(step.targetStatus());
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
protected void updateHighestAllowedStatus(ChunkMap chunkLoadingManager) {
- ChunkStatus chunkStatus = this.highestAllowedStatus;
- ChunkStatus chunkStatus2 = ChunkLevel.generationStatus(this.getTicketLevel());
- this.highestAllowedStatus = chunkStatus2;
- boolean bl = chunkStatus != null && (chunkStatus2 == null || chunkStatus2.isBefore(chunkStatus));
- if (bl) {
- this.failAndClearPendingFuturesBetween(chunkStatus2, chunkStatus);
- if (this.task.get() != null) {
- this.rescheduleChunkTask(chunkLoadingManager, this.findHighestStatusWithPendingFuture(chunkStatus2));
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void replaceProtoChunk(ImposterProtoChunk chunk) {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = CompletableFuture.completedFuture(ChunkResult.of(chunk));
-
- for (int i = 0; i < this.futures.length() - 1; i++) {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture2 = this.futures.get(i);
- Objects.requireNonNull(completableFuture2);
- ChunkAccess chunkAccess = completableFuture2.getNow(NOT_DONE_YET).orElse(null);
- if (!(chunkAccess instanceof ProtoChunk)) {
- throw new IllegalStateException("Trying to replace a ProtoChunk, but found " + chunkAccess);
- }
-
- if (!this.futures.compareAndSet(i, completableFuture2, completableFuture)) {
- throw new IllegalStateException("Future changed by other thread while trying to replace it");
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
void removeTask(ChunkGenerationTask loader) {
- this.task.compareAndSet(loader, null);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void rescheduleChunkTask(ChunkMap chunkLoadingManager, @Nullable ChunkStatus requestedStatus) {
- ChunkGenerationTask chunkGenerationTask;
- if (requestedStatus != null) {
- chunkGenerationTask = chunkLoadingManager.scheduleGenerationTask(requestedStatus, this.getPos());
- } else {
- chunkGenerationTask = null;
- }
-
- ChunkGenerationTask chunkGenerationTask3 = this.task.getAndSet(chunkGenerationTask);
- if (chunkGenerationTask3 != null) {
- chunkGenerationTask3.markForCancellation();
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private CompletableFuture<ChunkResult<ChunkAccess>> getOrCreateFuture(ChunkStatus status) {
- if (this.isStatusDisallowed(status)) {
- return UNLOADED_CHUNK_FUTURE;
- } else {
- int i = status.getIndex();
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i);
-
- while (completableFuture == null) {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture2 = new CompletableFuture<>();
- completableFuture = this.futures.compareAndExchange(i, null, completableFuture2);
- if (completableFuture == null) {
- if (this.isStatusDisallowed(status)) {
- this.failAndClearPendingFuture(i, completableFuture2);
- return UNLOADED_CHUNK_FUTURE;
- }
-
- return completableFuture2;
- }
- }
-
- return completableFuture;
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void failAndClearPendingFuturesBetween(@Nullable ChunkStatus from, ChunkStatus to) {
- int i = from == null ? 0 : from.getIndex() + 1;
- int j = to.getIndex();
-
- for (int k = i; k <= j; k++) {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(k);
- if (completableFuture != null) {
- this.failAndClearPendingFuture(k, completableFuture);
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void failAndClearPendingFuture(int statusIndex, CompletableFuture<ChunkResult<ChunkAccess>> previousFuture) {
- if (previousFuture.complete(UNLOADED_CHUNK) && !this.futures.compareAndSet(statusIndex, previousFuture, null)) {
- throw new IllegalStateException("Nothing else should replace the future here");
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void completeFuture(ChunkStatus status, ChunkAccess chunk) {
- ChunkResult<ChunkAccess> chunkResult = ChunkResult.of(chunk);
- int i = status.getIndex();
-
- while (true) {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i);
- if (completableFuture == null) {
- if (this.futures.compareAndSet(i, null, CompletableFuture.completedFuture(chunkResult))) {
- return;
- }
- } else {
- if (completableFuture.complete(chunkResult)) {
- return;
- }
-
- if (completableFuture.getNow(NOT_DONE_YET).isSuccess()) {
- throw new IllegalStateException("Trying to complete a future but found it to be completed successfully already");
- }
-
- Thread.yield();
- }
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Nullable
private ChunkStatus findHighestStatusWithPendingFuture(@Nullable ChunkStatus checkUpperBound) {
- if (checkUpperBound == null) {
- return null;
- } else {
- ChunkStatus chunkStatus = checkUpperBound;
-
- for (ChunkStatus chunkStatus2 = this.startedWork.get();
- chunkStatus2 == null || chunkStatus.isAfter(chunkStatus2);
- chunkStatus = chunkStatus.getParent()
- ) {
- if (this.futures.get(chunkStatus.getIndex()) != null) {
- return chunkStatus;
- }
-
- if (chunkStatus == ChunkStatus.EMPTY) {
- break;
- }
- }
-
- return null;
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private boolean acquireStatusBump(ChunkStatus nextStatus) {
- ChunkStatus chunkStatus = nextStatus == ChunkStatus.EMPTY ? null : nextStatus.getParent();
- ChunkStatus chunkStatus2 = this.startedWork.compareAndExchange(chunkStatus, nextStatus);
- if (chunkStatus2 == chunkStatus) {
- return true;
- } else if (chunkStatus2 != null && !nextStatus.isAfter(chunkStatus2)) {
- return false;
- } else {
- throw new IllegalStateException("Unexpected last startedWork status: " + chunkStatus2 + " while trying to start: " + nextStatus);
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private boolean isStatusDisallowed(ChunkStatus status) {
- ChunkStatus chunkStatus = this.highestAllowedStatus;
- return chunkStatus == null || status.isAfter(chunkStatus);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void increaseGenerationRefCount() {
- this.generationRefCount.incrementAndGet();
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void decreaseGenerationRefCount() {
- int i = this.generationRefCount.decrementAndGet();
- if (i < 0) {
- throw new IllegalStateException("More releases than claims. Count: " + i);
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public int getGenerationRefCount() {
- return this.generationRefCount.get();
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Nullable
public ChunkAccess getChunkIfPresentUnchecked(ChunkStatus requestedStatus) {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(requestedStatus.getIndex());
- return completableFuture == null ? null : completableFuture.getNow(NOT_DONE_YET).orElse(null);
+ // Paper start - rewrite chunk system
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresentUnchecked(requestedStatus);
+ // Paper end - rewrite chunk system
}
@Nullable
public ChunkAccess getChunkIfPresent(ChunkStatus requestedStatus) {
- return this.isStatusDisallowed(requestedStatus) ? null : this.getChunkIfPresentUnchecked(requestedStatus);
+ // Paper start - rewrite chunk system
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresent(requestedStatus);
+ // Paper end - rewrite chunk system
}
@Nullable
public ChunkAccess getLatestChunk() {
- ChunkStatus chunkStatus = this.startedWork.get();
- if (chunkStatus == null) {
- return null;
- } else {
- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(chunkStatus);
- return chunkAccess != null ? chunkAccess : this.getChunkIfPresentUnchecked(chunkStatus.getParent());
- }
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion();
+ return lastCompletion == null ? null : lastCompletion.chunk();
+ // Paper end - rewrite chunk system
}
@Nullable
public ChunkStatus getPersistedStatus() {
- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(ChunkStatus.EMPTY.getIndex());
- ChunkAccess chunkAccess = completableFuture == null ? null : completableFuture.getNow(NOT_DONE_YET).orElse(null);
- return chunkAccess == null ? null : chunkAccess.getPersistedStatus();
+ // Paper start - rewrite chunk system
+ final ChunkAccess chunk = this.getLatestChunk();
+ return chunk == null ? null : chunk.getPersistedStatus();
+ // Paper end - rewrite chunk system
}
public ChunkPos getPos() {
@@ -277,7 +133,7 @@ public abstract class GenerationChunkHolder {
}
public FullChunkStatus getFullStatus() {
- return ChunkLevel.fullStatus(this.getTicketLevel());
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkStatus(); // Paper - rewrite chunk system
}
public abstract int getTicketLevel();
@@ -286,26 +142,15 @@ public abstract class GenerationChunkHolder {
@VisibleForDebug
public List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> getAllFutures() {
- List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> list = new ArrayList<>();
-
- for (int i = 0; i < CHUNK_STATUSES.size(); i++) {
- list.add(Pair.of(CHUNK_STATUSES.get(i), this.futures.get(i)));
- }
-
- return list;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Nullable
@VisibleForDebug
public ChunkStatus getLatestStatus() {
- for (int i = CHUNK_STATUSES.size() - 1; i >= 0; i--) {
- ChunkStatus chunkStatus = CHUNK_STATUSES.get(i);
- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(chunkStatus);
- if (chunkAccess != null) {
- return chunkStatus;
- }
- }
-
- return null;
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion();
+ return lastCompletion == null ? null : lastCompletion.genStatus();
+ // Paper end - rewrite chunk system
}
}
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
index be9604a0f267558c95125852d86761a2f175732a..67eb2fb32de3555b3afb4b4b7a3a47a164158ac8 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -46,7 +46,7 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp
import net.minecraft.world.level.storage.DimensionDataStorage;
import net.minecraft.world.level.storage.LevelStorageSource;
-public class ServerChunkCache extends ChunkSource {
+public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache { // Paper - rewrite chunk system
public static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); // Paper
private static final List<ChunkStatus> CHUNK_STATUSES = ChunkStatus.getStatusList();
@@ -73,6 +73,61 @@ public class ServerChunkCache extends ChunkSource {
private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<net.minecraft.world.level.chunk.LevelChunk> fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>();
long chunkFutureAwaitCounter;
// Paper end
+ // Paper start - rewrite chunk system
+
+ @Override
+ public final void moonrise$setFullChunk(final int chunkX, final int chunkZ, final LevelChunk chunk) {
+ final long key = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ if (chunk == null) {
+ this.fullChunks.remove(key);
+ } else {
+ this.fullChunks.put(key, chunk);
+ }
+ }
+
+ @Override
+ public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) {
+ return this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+
+ private ChunkAccess syncLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus) {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler();
+ final CompletableFuture<ChunkAccess> completable = new CompletableFuture<>();
+ chunkTaskScheduler.scheduleChunkLoad(
+ chunkX, chunkZ, toStatus, true, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING,
+ completable::complete
+ );
+
+ if (io.papermc.paper.util.TickThread.isTickThreadFor(this.level, chunkX, chunkZ)) {
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.pushChunkWait(this.level, chunkX, chunkZ);
+ this.mainThreadProcessor.managedBlock(completable::isDone);
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.popChunkWait();
+ }
+
+ final ChunkAccess ret = completable.join();
+ if (ret == null) {
+ throw new IllegalStateException("Chunk not loaded when requested");
+ }
+
+ return ret;
+ }
+
+ private ChunkAccess getChunkFallback(final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+ final boolean load) {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler();
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager;
+
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder currentChunk = chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ final ChunkAccess ifPresent = currentChunk == null ? null : currentChunk.getChunkIfPresent(toStatus);
+
+ if (ifPresent != null && (toStatus != ChunkStatus.FULL || currentChunk.isFullChunkReady())) {
+ return ifPresent;
+ }
+
+ return load ? this.syncLoad(chunkX, chunkZ, toStatus) : null;
+ }
+ // Paper end - rewrite chunk system
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;
@@ -99,13 +154,7 @@ public class ServerChunkCache extends ChunkSource {
}
// 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);
- }
+ // Paper - rewrite chunk system
@Nullable
public ChunkAccess getChunkAtImmediately(int x, int z) {
@@ -176,63 +225,25 @@ public class ServerChunkCache extends ChunkSource {
@Nullable
@Override
public ChunkAccess getChunk(int x, int z, ChunkStatus leastStatus, boolean create) {
- if (Thread.currentThread() != this.mainThread) {
- return (ChunkAccess) CompletableFuture.supplyAsync(() -> {
- return this.getChunk(x, z, leastStatus, create);
- }, this.mainThreadProcessor).join();
- } else {
- // Paper start - Perf: Optimise getChunkAt calls for loaded chunks
- LevelChunk ifLoaded = this.getChunkAtIfLoadedMainThread(x, z);
- if (ifLoaded != null) {
- return ifLoaded;
- }
- // Paper end - Perf: Optimise getChunkAt calls for loaded chunks
- ProfilerFiller gameprofilerfiller = this.level.getProfiler();
+ // Paper start - rewrite chunk system
+ if (leastStatus == ChunkStatus.FULL) {
+ final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x, z));
- gameprofilerfiller.incrementCounter("getChunk");
- long k = ChunkPos.asLong(x, z);
-
- for (int l = 0; l < 4; ++l) {
- if (k == this.lastChunkPos[l] && leastStatus == this.lastChunkStatus[l]) {
- ChunkAccess ichunkaccess = this.lastChunk[l];
-
- if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime
- return ichunkaccess;
- }
- }
+ if (ret != null) {
+ return ret;
}
- gameprofilerfiller.incrementCounter("getChunkCacheMiss");
- CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create);
- ServerChunkCache.MainThreadExecutor chunkproviderserver_b = this.mainThreadProcessor;
-
- Objects.requireNonNull(completablefuture);
- if (!completablefuture.isDone()) { // Paper
- com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads
- this.level.timings.syncChunkLoad.startTiming(); // Paper
- chunkproviderserver_b.managedBlock(completablefuture::isDone);
- this.level.timings.syncChunkLoad.stopTiming(); // Paper
- } // Paper
- ChunkResult<ChunkAccess> chunkresult = (ChunkResult) completablefuture.join();
- ChunkAccess ichunkaccess1 = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error
-
- if (ichunkaccess1 == null && create) {
- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Chunk not there when requested: " + chunkresult.getError()));
- } else {
- this.storeInCache(k, ichunkaccess1, leastStatus);
- return ichunkaccess1;
- }
+ return create ? this.getChunkFallback(x, z, leastStatus, create) : null;
}
+
+ return this.getChunkFallback(x, z, leastStatus, create);
+ // Paper end - rewrite chunk system
}
@Nullable
@Override
public LevelChunk getChunkNow(int chunkX, int chunkZ) {
- if (Thread.currentThread() != this.mainThread) {
- return null;
- } else {
- return this.getChunkAtIfLoadedMainThread(chunkX, chunkZ); // Paper - Perf: Optimise getChunkAt calls for loaded chunks
- }
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getFullChunkIfLoaded(chunkX, chunkZ); // Paper - rewrite chunk system
}
private void clearCache() {
@@ -263,56 +274,59 @@ public class ServerChunkCache extends ChunkSource {
}
private CompletableFuture<ChunkResult<ChunkAccess>> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
- ChunkPos chunkcoordintpair = new ChunkPos(chunkX, chunkZ);
- long k = chunkcoordintpair.toLong();
- int l = ChunkLevel.byStatus(leastStatus);
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k);
+ // Paper start - rewrite chunk system
+ io.papermc.paper.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Scheduling chunk load off-main");
- // CraftBukkit start - don't add new ticket for currently unloading chunk
- boolean currentlyUnloading = false;
- if (playerchunk != null) {
- FullChunkStatus oldChunkState = ChunkLevel.fullStatus(playerchunk.oldTicketLevel);
- FullChunkStatus currentChunkState = ChunkLevel.fullStatus(playerchunk.getTicketLevel());
- currentlyUnloading = (oldChunkState.isOrAfter(FullChunkStatus.FULL) && !currentChunkState.isOrAfter(FullChunkStatus.FULL));
+ final int minLevel = ChunkLevel.byStatus(leastStatus);
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
+
+ final boolean needsFullScheduling = leastStatus == ChunkStatus.FULL && (chunkHolder == null || !chunkHolder.getChunkStatus().isOrAfter(FullChunkStatus.FULL));
+
+ if ((chunkHolder == null || chunkHolder.getTicketLevel() > minLevel || needsFullScheduling) && !create) {
+ return ChunkHolder.UNLOADED_CHUNK_FUTURE;
}
- if (create && !currentlyUnloading) {
- // CraftBukkit end
- this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair);
- if (this.chunkAbsent(playerchunk, l)) {
- ProfilerFiller gameprofilerfiller = this.level.getProfiler();
-
- gameprofilerfiller.push("chunkLoad");
- this.runDistanceManagerUpdates();
- playerchunk = this.getVisibleChunkIfPresent(k);
- gameprofilerfiller.pop();
- if (this.chunkAbsent(playerchunk, l)) {
- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("No chunk holder after ticket has been added"));
+
+ final ChunkAccess ifPresent = chunkHolder == null ? null : chunkHolder.getChunkIfPresent(leastStatus);
+ if (needsFullScheduling || ifPresent == null) {
+ // schedule
+ final CompletableFuture<ChunkResult<ChunkAccess>> ret = new CompletableFuture<>();
+ final Consumer<ChunkAccess> complete = (ChunkAccess chunk) -> {
+ if (chunk == null) {
+ ret.complete(ChunkHolder.UNLOADED_CHUNK);
+ } else {
+ ret.complete(ChunkResult.of(chunk));
}
- }
- }
+ };
- return this.chunkAbsent(playerchunk, l) ? GenerationChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.scheduleChunkGenerationTask(leastStatus, this.chunkMap);
- }
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(
+ chunkX, chunkZ, leastStatus, true,
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER,
+ complete
+ );
- private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) {
- return holder == null || holder.oldTicketLevel > maxLevel; // CraftBukkit using oldTicketLevel for isLoaded checks
+ return ret;
+ } else {
+ // can return now
+ return CompletableFuture.completedFuture(ChunkResult.of(ifPresent));
+ }
+ // Paper end - rewrite chunk system
}
@Override
public boolean hasChunk(int x, int z) {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent((new ChunkPos(x, z)).toLong());
- int k = ChunkLevel.byStatus(ChunkStatus.FULL);
-
- return !this.chunkAbsent(playerchunk, k);
+ return this.getChunkNow(x, z) != null; // Paper - rewrite chunk system
}
@Nullable
@Override
public LightChunk getChunkForLighting(int chunkX, int chunkZ) {
- long k = ChunkPos.asLong(chunkX, chunkZ);
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k);
-
- return playerchunk == null ? null : playerchunk.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent());
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
+ if (newChunkHolder == null) {
+ return null;
+ }
+ return newChunkHolder.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent());
+ // Paper end - rewrite chunk system
}
@Override
@@ -325,16 +339,7 @@ public class ServerChunkCache extends ChunkSource {
}
public boolean runDistanceManagerUpdates() { // Paper - public
- boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
- boolean flag1 = this.chunkMap.promoteChunkMap();
-
- this.chunkMap.runGenerationTasks();
- if (!flag && !flag1) {
- return false;
- } else {
- this.clearCache();
- return true;
- }
+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // Paper - rewrite chunk system
}
// Paper start
@@ -344,13 +349,14 @@ public class ServerChunkCache extends ChunkSource {
// Paper end
public boolean isPositionTicking(long pos) {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
-
- return playerchunk == null ? false : (!this.level.shouldTickBlocksAt(pos) ? false : ((ChunkResult) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).isSuccess());
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos);
+ return newChunkHolder != null && newChunkHolder.isTickingReady();
+ // Paper end - rewrite chunk system
}
public void save(boolean flush) {
- this.runDistanceManagerUpdates();
+ // Paper - rewrite chunk system
try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings
this.chunkMap.saveAllChunks(flush);
} // Paper - Timings
@@ -363,12 +369,7 @@ public class ServerChunkCache extends ChunkSource {
}
public void close(boolean save) throws IOException {
- if (save) {
- this.save(true);
- }
- // CraftBukkit end
- this.lightEngine.close();
- this.chunkMap.close();
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.close(save, true); // Paper - rewrite chunk system
}
// CraftBukkit start - modelled on below
@@ -396,6 +397,7 @@ public class ServerChunkCache extends ChunkSource {
this.level.getProfiler().popPush("chunks");
if (tickChunks) {
this.level.timings.chunks.startTiming(); // Paper - timings
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().tick(); // Paper - rewrite chunk system
this.tickChunks();
this.level.timings.chunks.stopTiming(); // Paper - timings
this.chunkMap.tick();
@@ -410,6 +412,7 @@ public class ServerChunkCache extends ChunkSource {
}
private void tickChunks() {
+ long chunksTicked = 0; // Paper - rewrite chunk system
long i = this.level.getGameTime();
long j = i - this.lastInhabitedUpdate;
@@ -470,6 +473,11 @@ public class ServerChunkCache extends ChunkSource {
if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) {
this.level.tickChunk(chunk1, l);
+ // Paper start - rewrite chunk system
+ if ((++chunksTicked & 7L) == 0L) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.level.getServer()).moonrise$executeMidTickTasks();
+ }
+ // Paper end - rewrite chunk system
}
}
}
@@ -495,11 +503,12 @@ public class ServerChunkCache extends ChunkSource {
}
private void getFullChunk(long pos, Consumer<LevelChunk> chunkConsumer) {
- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
-
- if (playerchunk != null) {
- ((ChunkResult) playerchunk.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).ifSuccess(chunkConsumer);
+ // Paper start - rewrite chunk system
+ final LevelChunk fullChunk = this.getChunkNow(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos));
+ if (fullChunk != null) {
+ chunkConsumer.accept(fullChunk);
}
+ // Paper end - rewrite chunk system
}
@@ -593,6 +602,12 @@ public class ServerChunkCache extends ChunkSource {
this.chunkMap.setServerViewDistance(watchDistance);
}
+ // Paper start - rewrite chunk system
+ public void setSendViewDistance(int viewDistance) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().setSendDistance(viewDistance);
+ }
+ // Paper end - rewrite chunk system
+
public void setSimulationDistance(int simulationDistance) {
this.distanceManager.updateSimulationDistance(simulationDistance);
}
@@ -671,16 +686,14 @@ public class ServerChunkCache extends ChunkSource {
@Override
// CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
public boolean pollTask() {
- try {
- if (ServerChunkCache.this.runDistanceManagerUpdates()) {
+ // Paper start - rewrite chunk system
+ final ServerChunkCache serverChunkCache = ServerChunkCache.this;
+ if (serverChunkCache.runDistanceManagerUpdates()) {
return true;
} else {
- ServerChunkCache.this.lightEngine.tryScheduleUpdate();
- return super.pollTask();
+ return super.pollTask() | ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)serverChunkCache.level).moonrise$getChunkTaskScheduler().executeMainThreadTask();
}
- } finally {
- ServerChunkCache.this.chunkMap.callbackExecutor.run();
- }
+ // Paper end - rewrite chunk system
// CraftBukkit end
}
}
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index 4d7e234d379a451c4bb53bc2fcdf22cb191f8d1a..cf33e22ae85cd30b4f5d526dbfececca87d4ee40 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -184,7 +184,7 @@ import org.bukkit.event.weather.LightningStrikeEvent;
import org.bukkit.event.world.TimeSkipEvent;
// CraftBukkit end
-public class ServerLevel extends Level implements WorldGenLevel {
+public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader { // Paper - rewrite chunk system
public static final BlockPos END_SPAWN_POINT = new BlockPos(100, 50, 0);
public static final IntProvider RAIN_DELAY = UniformInt.of(12000, 180000);
@@ -200,7 +200,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
public final PrimaryLevelData serverLevelData; // CraftBukkit - type
private int lastSpawnChunkRadius;
final EntityTickList entityTickList;
- public final PersistentEntitySectionManager<Entity> entityManager;
+ // Paper - rewrite chunk system
private final GameEventDispatcher gameEventDispatcher;
public boolean noSave;
private final SleepStatus sleepStatus;
@@ -339,6 +339,179 @@ public class ServerLevel extends Level implements WorldGenLevel {
return player != null && player.level() == this ? player : null;
}
// Paper end - optimise getPlayerByUUID
+ // Paper start - rewrite chunk system
+ private boolean markedClosing;
+ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder();
+ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader chunkLoader = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader((ServerLevel)(Object)this);
+ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController entityDataController;
+ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController poiDataController;
+ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController chunkDataController;
+ private final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler;
+ private long lastMidTickFailure;
+ private long tickedBlocksOrFluids;
+
+ @Override
+ public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ if (newChunkHolder == null || !newChunkHolder.isFullChunkReady()) {
+ return null;
+ }
+
+ if (newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) {
+ return levelChunk;
+ }
+ // race condition: chunk unloaded, only happens off-main
+ return null;
+ }
+
+ @Override
+ public final ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ if (newChunkHolder == null) {
+ return null;
+ }
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion();
+ return lastCompletion == null ? null : lastCompletion.chunk();
+ }
+
+ @Override
+ public final ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus leastStatus) {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
+ if (newChunkHolder == null) {
+ return null;
+ }
+ return newChunkHolder.getChunkIfPresentUnchecked(leastStatus);
+ }
+
+ @Override
+ public final void moonrise$midTickTasks() {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
+ }
+
+ @Override
+ public final ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus status) {
+ return this.moonrise$getChunkTaskScheduler().syncLoadNonFull(chunkX, chunkZ, status);
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler moonrise$getChunkTaskScheduler() {
+ return this.chunkTaskScheduler;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.ChunkDataController moonrise$getChunkDataController() {
+ return this.chunkDataController;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.ChunkDataController moonrise$getPoiChunkDataController() {
+ return this.poiDataController;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.ChunkDataController moonrise$getEntityChunkDataController() {
+ return this.entityDataController;
+ }
+
+ @Override
+ public final int moonrise$getRegionChunkShift() {
+ return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift();
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader() {
+ return this.chunkLoader;
+ }
+
+ @Override
+ public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
+ final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+ this.moonrise$loadChunksAsync(
+ (pos.getX() - radiusBlocks) >> 4,
+ (pos.getX() + radiusBlocks) >> 4,
+ (pos.getZ() - radiusBlocks) >> 4,
+ (pos.getZ() + radiusBlocks) >> 4,
+ priority, onLoad
+ );
+ }
+
+ @Override
+ public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
+ final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+ this.moonrise$loadChunksAsync(
+ (pos.getX() - radiusBlocks) >> 4,
+ (pos.getX() + radiusBlocks) >> 4,
+ (pos.getZ() - radiusBlocks) >> 4,
+ (pos.getZ() + radiusBlocks) >> 4,
+ chunkStatus, priority, onLoad
+ );
+ }
+
+ @Override
+ public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
+ final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+ this.moonrise$loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, priority, onLoad);
+ }
+
+ @Override
+ public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
+ final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
+ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = this.moonrise$getChunkTaskScheduler();
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager;
+
+ final int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
+ final java.util.concurrent.atomic.AtomicInteger loadedChunks = new java.util.concurrent.atomic.AtomicInteger();
+ final Long holderIdentifier = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getNextChunkLoadId();
+ final int ticketLevel = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getTicketLevel(chunkStatus);
+
+ final List<ChunkAccess> ret = new ArrayList<>(requiredChunks);
+
+ final java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (final ChunkAccess chunk) -> {
+ if (chunk != null) {
+ synchronized (ret) {
+ ret.add(chunk);
+ }
+ chunkHolderManager.addTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunk.getPos(), ticketLevel, holderIdentifier);
+ }
+ if (loadedChunks.incrementAndGet() == requiredChunks) {
+ try {
+ onLoad.accept(java.util.Collections.unmodifiableList(ret));
+ } finally {
+ for (int i = 0, len = ret.size(); i < len; ++i) {
+ final ChunkPos chunkPos = ret.get(i).getPos();
+
+ chunkHolderManager.removeTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunkPos, ticketLevel, holderIdentifier);
+ }
+ }
+ }
+ };
+
+ for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
+ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
+ chunkTaskScheduler.scheduleChunkLoad(cx, cz, chunkStatus, true, priority, consumer);
+ }
+ }
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() {
+ return this.viewDistanceHolder;
+ }
+
+ @Override
+ public final long moonrise$getLastMidTickFailure() {
+ return this.lastMidTickFailure;
+ }
+
+ @Override
+ public final void moonrise$setLastMidTickFailure(final long time) {
+ this.lastMidTickFailure = time;
+ }
+ // Paper end - rewrite chunk system
// 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) {
@@ -385,14 +558,13 @@ public class ServerLevel extends Level implements WorldGenLevel {
DataFixer datafixer = minecraftserver.getFixerUpper();
EntityPersistentStorage<Entity> entitypersistentstorage = new EntityStorage(new SimpleRegionStorage(new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"), convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, DataFixTypes.ENTITY_CHUNK), this, minecraftserver);
- this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage);
+ // Paper - rewrite chunk system
StructureTemplateManager structuretemplatemanager = minecraftserver.getStructureManager();
int j = this.spigotConfig.viewDistance; // Spigot
int k = this.spigotConfig.simulationDistance; // Spigot
- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager;
+ // Paper - rewrite chunk system
- Objects.requireNonNull(this.entityManager);
- this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, persistententitysectionmanager::updateChunkStatus, () -> {
+ this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, null, () -> { // Paper - rewrite chunk system
return minecraftserver.overworld().getDataStorage();
});
this.chunkSource.getGeneratorState().ensureStructuresGenerated();
@@ -420,6 +592,19 @@ public class ServerLevel extends Level implements WorldGenLevel {
this.randomSequences = (RandomSequences) Objects.requireNonNullElseGet(randomsequences, () -> {
return (RandomSequences) this.getDataStorage().computeIfAbsent(RandomSequences.factory(l), "random_sequences");
});
+ // Paper start - rewrite chunk system
+ this.entityDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController(
+ new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController.EntityRegionFileStorage(
+ new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"),
+ convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"),
+ minecraftserver.forceSynchronousWrites()
+ )
+ );
+ this.poiDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController((ServerLevel)(Object)this);
+ this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this);
+ this.moonrise$setEntityLookup(new ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup((ServerLevel)(Object)this, ((ServerLevel)(Object)this).new EntityCallbacks()));
+ this.chunkTaskScheduler = new ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler((ServerLevel)(Object)this, ca.spottedleaf.moonrise.common.util.MoonriseCommon.WORKER_POOL);
+ // Paper end - rewrite chunk system
this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit
}
@@ -553,7 +738,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
gameprofilerfiller.push("checkDespawn");
entity.checkDespawn();
gameprofilerfiller.pop();
- if (this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) {
+ if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - rewrite chunk system
Entity entity1 = entity.getVehicle();
if (entity1 != null) {
@@ -578,13 +763,16 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
gameprofilerfiller.push("entityManagement");
- this.entityManager.tick();
+ // Paper - rewrite chunk system
gameprofilerfiller.pop();
}
@Override
public boolean shouldTickBlocksAt(long chunkPos) {
- return this.chunkSource.chunkMap.getDistanceManager().inBlockTickingRange(chunkPos);
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos);
+ return holder != null && holder.isTickingReady();
+ // Paper end - rewrite chunk system
}
protected void tickTime() {
@@ -976,6 +1164,11 @@ public class ServerLevel extends Level implements WorldGenLevel {
if (fluid1.is(fluid)) {
fluid1.tick(this, pos);
}
+ // Paper start - rewrite chunk system
+ if ((++this.tickedBlocksOrFluids & 7L) != 0L) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
+ }
+ // Paper end - rewrite chunk system
}
@@ -985,6 +1178,11 @@ public class ServerLevel extends Level implements WorldGenLevel {
if (iblockdata.is(block)) {
iblockdata.tick(this, pos, this.random);
}
+ // Paper start - rewrite chunk system
+ if ((++this.tickedBlocksOrFluids & 7L) != 0L) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
+ }
+ // Paper end - rewrite chunk system
}
@@ -1061,6 +1259,11 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) {
+ // Paper start - add close param
+ this.save(progressListener, flush, savingDisabled, false);
+ }
+ public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled, boolean close) {
+ // Paper end - add close param
ServerChunkCache chunkproviderserver = this.getChunkSource();
if (!savingDisabled) {
@@ -1076,16 +1279,21 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
timings.worldSaveChunks.startTiming(); // Paper
- chunkproviderserver.save(flush);
+ if (!close) { chunkproviderserver.save(flush); } // Paper - add close param
timings.worldSaveChunks.stopTiming(); // Paper
}// Paper
- if (flush) {
- this.entityManager.saveAll();
- } else {
- this.entityManager.autoSave();
- }
+ // Paper - rewrite chunk system
}
+ // Paper start - add close param
+ if (close) {
+ try {
+ chunkproviderserver.close(!savingDisabled);
+ } catch (IOException never) {
+ throw new RuntimeException(never);
+ }
+ }
+ // Paper end - add close param
// CraftBukkit start - moved from MinecraftServer.saveChunks
ServerLevel worldserver1 = this;
@@ -1218,7 +1426,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
this.removePlayerImmediately((ServerPlayer) entity, Entity.RemovalReason.DISCARDED);
}
- this.entityManager.addNewEntity(player);
+ this.moonrise$getEntityLookup().addNewEntity(player); // Paper - rewrite chunk system
}
// CraftBukkit start
@@ -1249,7 +1457,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
// CraftBukkit end
- return this.entityManager.addNewEntity(entity);
+ return this.moonrise$getEntityLookup().addNewEntity(entity); // Paper - rewrite chunk system
}
}
@@ -1260,11 +1468,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
// CraftBukkit end
- Stream<UUID> stream = entity.getSelfAndPassengers().map(Entity::getUUID); // CraftBukkit - decompile error
- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager;
-
- Objects.requireNonNull(this.entityManager);
- if (stream.anyMatch(persistententitysectionmanager::isLoaded)) {
+ if (entity.getSelfAndPassengers().map(Entity::getUUID).anyMatch(this.moonrise$getEntityLookup()::hasEntity)) { // Paper - rewrite chunk system
return false;
} else {
this.addFreshEntityWithPassengers(entity, reason); // CraftBukkit
@@ -1850,7 +2054,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
}
}
- bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityManager.gatherStats()));
+ bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.moonrise$getEntityLookup().getDebugInfo())); // Paper - rewrite chunk system
bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size()));
bufferedwriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count()));
bufferedwriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count()));
@@ -1899,7 +2103,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
BufferedWriter bufferedwriter2 = Files.newBufferedWriter(path1);
try {
- playerchunkmap.dumpChunks(bufferedwriter2);
+ //playerchunkmap.dumpChunks(bufferedwriter2); // Paper - rewrite chunk system
} catch (Throwable throwable4) {
if (bufferedwriter2 != null) {
try {
@@ -1920,7 +2124,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
BufferedWriter bufferedwriter3 = Files.newBufferedWriter(path2);
try {
- this.entityManager.dumpSections(bufferedwriter3);
+ //this.entityManager.dumpSections(bufferedwriter3); // Paper - rewrite chunk system
} catch (Throwable throwable6) {
if (bufferedwriter3 != null) {
try {
@@ -2062,7 +2266,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
@VisibleForTesting
public String getWatchdogStats() {
- return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.entityManager.gatherStats(), ServerLevel.getTypeCount(this.entityManager.getEntityGetter().getAll(), (entity) -> {
+ return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.moonrise$getEntityLookup().getDebugInfo(), ServerLevel.getTypeCount(this.moonrise$getEntityLookup().getAll(), (entity) -> { // Paper - rewrite chunk system
return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString();
}), this.blockEntityTickers.size(), ServerLevel.getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), this.getBlockTicks().count(), this.getFluidTicks().count(), this.gatherChunkSourceStats());
}
@@ -2092,15 +2296,25 @@ public class ServerLevel extends Level implements WorldGenLevel {
@Override
public LevelEntityGetter<Entity> getEntities() {
org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
- return this.entityManager.getEntityGetter();
+ return this.moonrise$getEntityLookup(); // Paper - rewrite chunk system
}
public void addLegacyChunkEntities(Stream<Entity> entities) {
- this.entityManager.addLegacyChunkEntities(entities);
+ // Paper start - add chunkpos param
+ this.addLegacyChunkEntities(entities, null);
+ }
+ public void addLegacyChunkEntities(Stream<Entity> entities, ChunkPos chunkPos) {
+ // Paper end - add chunkpos param
+ this.moonrise$getEntityLookup().addLegacyChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system
}
public void addWorldGenChunkEntities(Stream<Entity> entities) {
- this.entityManager.addWorldGenChunkEntities(entities);
+ // Paper start - add chunkpos param
+ this.addWorldGenChunkEntities(entities, null);
+ }
+ public void addWorldGenChunkEntities(Stream<Entity> entities, ChunkPos chunkPos) {
+ // Paper end - add chunkpos param
+ this.moonrise$getEntityLookup().addWorldGenChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system
}
public void startTickingChunk(LevelChunk chunk) {
@@ -2120,34 +2334,47 @@ public class ServerLevel extends Level implements WorldGenLevel {
@Override
public void close() throws IOException {
super.close();
- this.entityManager.close();
+ // Paper - rewrite chunk system
}
@Override
public String gatherChunkSourceStats() {
String s = this.chunkSource.gatherStats();
- return "Chunks[S] W: " + s + " E: " + this.entityManager.gatherStats();
+ return "Chunks[S] W: " + s + " E: " + this.moonrise$getEntityLookup().getDebugInfo(); // Paper - rewrite chunk system
}
public boolean areEntitiesLoaded(long chunkPos) {
- return this.entityManager.areEntitiesLoaded(chunkPos);
+ return this.moonrise$getAnyChunkIfLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)) != null; // Paper - rewrite chunk system
}
private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) {
- return this.areEntitiesLoaded(chunkPos) && this.chunkSource.isPositionTicking(chunkPos);
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos);
+ // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded
+ return chunkHolder != null && chunkHolder.isTickingReady();
+ // Paper end - rewrite chunk system
}
public boolean isPositionEntityTicking(BlockPos pos) {
- return this.entityManager.canPositionTick(pos) && this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(ChunkPos.asLong(pos));
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
+ // Paper end - rewrite chunk system
}
public boolean isNaturalSpawningAllowed(BlockPos pos) {
- return this.entityManager.canPositionTick(pos);
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
+ // Paper end - rewrite chunk system
}
public boolean isNaturalSpawningAllowed(ChunkPos pos) {
- return this.entityManager.canPositionTick(pos);
+ // Paper start - rewrite chunk system
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
+ return chunkHolder != null && chunkHolder.isEntityTickingReady();
+ // Paper end - rewrite chunk system
}
@Override
@@ -2173,7 +2400,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
CrashReportCategory crashreportsystemdetails = super.fillReportDetails(report);
crashreportsystemdetails.setDetail("Loaded entity count", () -> {
- return String.valueOf(this.entityManager.count());
+ return String.valueOf(this.moonrise$getEntityLookup().getEntityCount()); // Paper - rewrite chunk system
});
return crashreportsystemdetails;
}
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
index 191dfbd0f15c3a21278f3c4f9ce29f1698e0836c..ba64e42a58b4b760815f54228ebf7a46fd14734e 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -199,7 +199,7 @@ import org.bukkit.event.player.PlayerToggleSneakEvent;
import org.bukkit.inventory.MainHand;
// CraftBukkit end
-public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+public class ServerPlayer extends net.minecraft.world.entity.player.Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system
private static final Logger LOGGER = LogUtils.getLogger();
private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32;
@@ -297,6 +297,36 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
public @Nullable String clientBrandName = null; // Paper - Brand support
public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event
+ // Paper start - rewrite chunk system
+ private ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader;
+ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder();
+
+ @Override
+ public final boolean moonrise$isRealPlayer() {
+ return this.isRealPlayer;
+ }
+
+ @Override
+ public final void moonrise$setRealPlayer(final boolean real) {
+ this.isRealPlayer = real;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData moonrise$getChunkLoader() {
+ return this.chunkLoader;
+ }
+
+ @Override
+ public final void moonrise$setChunkLoader(final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData loader) {
+ this.chunkLoader = loader;
+ }
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() {
+ return this.viewDistanceHolder;
+ }
+ // Paper end - rewrite chunk system
+
public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);
this.chatVisibility = ChatVisiblity.FULL;
diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
index 63fae619e9b4ed49585f88ea7c167b0ee5efd859..cc779de06773451d51f54040fc899e4f45110bc1 100644
--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
@@ -23,15 +23,128 @@ import net.minecraft.world.level.chunk.LightChunkGetter;
import net.minecraft.world.level.lighting.LevelLightEngine;
import org.slf4j.Logger;
-public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable {
+public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable, ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider { // Paper - rewrite chunk system
public static final int DEFAULT_BATCH_SIZE = 1000;
private static final Logger LOGGER = LogUtils.getLogger();
- private final ProcessorMailbox<Runnable> taskMailbox;
- private final ObjectList<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> lightTasks = new ObjectArrayList<>();
+ // Paper - rewrite chunk sytem
private final ChunkMap chunkMap;
- private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> sorterMailbox;
+ // Paper - rewrite chunk sytem
private final int taskPerBatch = 1000;
- private final AtomicBoolean scheduled = new AtomicBoolean();
+ // Paper - rewrite chunk sytem
+
+ // Paper start - rewrite chunk system
+ private final java.util.concurrent.atomic.AtomicLong chunkWorkCounter = new java.util.concurrent.atomic.AtomicLong();
+ private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ,
+ final java.util.function.Supplier<ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.LightQueue.ChunkTasks> supplier) {
+ final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld();
+
+ final ChunkAccess center = this.starlight$getLightEngine().getAnyChunkNow(chunkX, chunkZ);
+ if (center == null || !center.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) {
+ // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing
+ // chunk scheduling, we could be lighting and generating a chunk at the same time
+ return;
+ }
+
+ final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.ServerLightQueue.ServerChunkTasks scheduledTask = (ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.ServerLightQueue.ServerChunkTasks)supplier.get();
+
+ if (scheduledTask == null) {
+ // not scheduled
+ return;
+ }
+
+ if (!scheduledTask.markTicketAdded()) {
+ // ticket already added
+ return;
+ }
+
+ final Long ticketId = Long.valueOf(this.chunkWorkCounter.getAndIncrement());
+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
+ world.getChunkSource().addRegionTicket(ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId);
+
+ scheduledTask.queueOrRunTask(() -> {
+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId);
+ });
+ }
+
+ @Override
+ public final int starlight$serverRelightChunks(final java.util.Collection<net.minecraft.world.level.ChunkPos> chunks0,
+ final java.util.function.Consumer<net.minecraft.world.level.ChunkPos> chunkLightCallback,
+ final java.util.function.IntConsumer onComplete) {
+ final java.util.Set<net.minecraft.world.level.ChunkPos> chunks = new java.util.LinkedHashSet<>(chunks0);
+ final java.util.Map<net.minecraft.world.level.ChunkPos, Long> ticketIds = new java.util.HashMap<>();
+ final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld();
+
+ for (final java.util.Iterator<net.minecraft.world.level.ChunkPos> iterator = chunks.iterator(); iterator.hasNext();) {
+ final ChunkPos pos = iterator.next();
+
+ final Long id = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getNextChunkRelightId();
+ world.getChunkSource().addRegionTicket(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, id);
+ ticketIds.put(pos, id);
+
+ final ChunkAccess chunk = (ChunkAccess)world.getChunkSource().getChunkForLighting(pos.x, pos.z);
+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) {
+ // cannot relight this chunk
+ iterator.remove();
+ ticketIds.remove(pos);
+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, id);
+ continue;
+ }
+ }
+
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().radiusAwareScheduler.queueInfiniteRadiusTask(() -> {
+ ThreadedLevelLightEngine.this.starlight$getLightEngine().relightChunks(
+ chunks,
+ (final ChunkPos pos) -> {
+ if (chunkLightCallback != null) {
+ chunkLightCallback.accept(pos);
+ }
+
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().scheduleChunkTask(pos.x, pos.z, () -> {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(
+ pos.x, pos.z
+ );
+
+ if (chunkHolder == null) {
+ return;
+ }
+
+ final java.util.List<ServerPlayer> players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)chunkHolder.vanillaChunkHolder).moonrise$getPlayers(false);
+
+ if (players.isEmpty()) {
+ return;
+ }
+
+ final net.minecraft.network.protocol.Packet<?> relightPacket = new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(
+ pos, (ThreadedLevelLightEngine)(Object)ThreadedLevelLightEngine.this,
+ null, null
+ );
+
+ for (final ServerPlayer player : players) {
+ final net.minecraft.server.network.ServerGamePacketListenerImpl conn = player.connection;
+ if (conn != null) {
+ conn.send(relightPacket);
+ }
+ }
+ });
+ },
+ (final int relight) -> {
+ if (onComplete != null) {
+ onComplete.accept(relight);
+ }
+
+ for (final java.util.Map.Entry<ChunkPos, Long> entry : ticketIds.entrySet()) {
+ world.getChunkSource().removeRegionTicket(
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, entry.getKey(),
+ ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, entry.getValue()
+ );
+ }
+ }
+ );
+ });
+
+ return chunks.size();
+ }
+ // Paper end - rewrite chunk system
public ThreadedLevelLightEngine(
LightChunkGetter chunkProvider,
@@ -42,8 +155,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
) {
super(chunkProvider, true, hasBlockLight);
this.chunkMap = chunkLoadingManager;
- this.sorterMailbox = executor;
- this.taskMailbox = processor;
+ // Paper - rewrite chunk sytem
}
@Override
@@ -57,164 +169,73 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
@Override
public void checkBlock(BlockPos pos) {
- BlockPos blockPos = pos.immutable();
- this.addTask(
- SectionPos.blockToSectionCoord(pos.getX()),
- SectionPos.blockToSectionCoord(pos.getZ()),
- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
- Util.name(() -> super.checkBlock(blockPos), () -> "checkBlock " + blockPos)
- );
+ // Paper start - rewrite chunk system
+ final BlockPos posCopy = pos.immutable();
+ this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> {
+ return ThreadedLevelLightEngine.this.starlight$getLightEngine().blockChange(posCopy);
+ });
+ // Paper end - rewrite chunk system
}
protected void updateChunkStatus(ChunkPos pos) {
- this.addTask(pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
- super.retainData(pos, false);
- super.setLightEnabled(pos, false);
-
- for (int i = this.getMinLightSection(); i < this.getMaxLightSection(); i++) {
- super.queueSectionData(LightLayer.BLOCK, SectionPos.of(pos, i), null);
- super.queueSectionData(LightLayer.SKY, SectionPos.of(pos, i), null);
- }
-
- for (int j = this.levelHeightAccessor.getMinSection(); j < this.levelHeightAccessor.getMaxSection(); j++) {
- super.updateSectionStatus(SectionPos.of(pos, j), true);
- }
- }, () -> "updateChunkStatus " + pos + " true"));
+ // Paper - rewrite chunk system
}
@Override
public void updateSectionStatus(SectionPos pos, boolean notReady) {
- this.addTask(
- pos.x(),
- pos.z(),
- () -> 0,
- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
- Util.name(() -> super.updateSectionStatus(pos, notReady), () -> "updateSectionStatus " + pos + " " + notReady)
- );
+ // Paper start - rewrite chunk system
+ this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> {
+ return ThreadedLevelLightEngine.this.starlight$getLightEngine().sectionChange(pos, notReady);
+ });
+ // Paper end - rewrite chunk system
}
@Override
public void propagateLightSources(ChunkPos chunkPos) {
- this.addTask(
- chunkPos.x,
- chunkPos.z,
- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
- Util.name(() -> super.propagateLightSources(chunkPos), () -> "propagateLight " + chunkPos)
- );
+ // Paper - rewrite chunk system
}
@Override
public void setLightEnabled(ChunkPos pos, boolean retainData) {
- this.addTask(
- pos.x,
- pos.z,
- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
- Util.name(() -> super.setLightEnabled(pos, retainData), () -> "enableLight " + pos + " " + retainData)
- );
+ // Paper start - rewrite chunk system
}
@Override
public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) {
- this.addTask(
- pos.x(),
- pos.z(),
- () -> 0,
- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
- Util.name(() -> super.queueSectionData(lightType, pos, nibbles), () -> "queueData " + pos)
- );
+ // Paper start - rewrite chunk system
}
private void addTask(int x, int z, ThreadedLevelLightEngine.TaskType stage, Runnable task) {
- this.addTask(x, z, this.chunkMap.getChunkQueueLevel(ChunkPos.asLong(x, z)), stage, task);
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
private void addTask(int x, int z, IntSupplier completedLevelSupplier, ThreadedLevelLightEngine.TaskType stage, Runnable task) {
- this.sorterMailbox.tell(ChunkTaskPriorityQueueSorter.message(() -> {
- this.lightTasks.add(Pair.of(stage, task));
- if (this.lightTasks.size() >= 1000) {
- this.runUpdate();
- }
- }, ChunkPos.asLong(x, z), completedLevelSupplier));
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
@Override
public void retainData(ChunkPos pos, boolean retainData) {
- this.addTask(
- pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> super.retainData(pos, retainData), () -> "retainData " + pos)
- );
+ // Paper start - rewrite chunk system
}
public CompletableFuture<ChunkAccess> initializeLight(ChunkAccess chunk, boolean bl) {
- ChunkPos chunkPos = chunk.getPos();
- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
- LevelChunkSection[] levelChunkSections = chunk.getSections();
-
- for (int i = 0; i < chunk.getSectionsCount(); i++) {
- LevelChunkSection levelChunkSection = levelChunkSections[i];
- if (!levelChunkSection.hasOnlyAir()) {
- int j = this.levelHeightAccessor.getSectionYFromSectionIndex(i);
- super.updateSectionStatus(SectionPos.of(chunkPos, j), false);
- }
- }
- }, () -> "initializeLight: " + chunkPos));
- return CompletableFuture.supplyAsync(() -> {
- super.setLightEnabled(chunkPos, bl);
- super.retainData(chunkPos, false);
- return chunk;
- }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task));
+ return CompletableFuture.completedFuture(chunk); // Paper start - rewrite chunk system
}
public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean excludeBlocks) {
- ChunkPos chunkPos = chunk.getPos();
- chunk.setLightCorrect(false);
- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
- if (!excludeBlocks) {
- super.propagateLightSources(chunkPos);
- }
- }, () -> "lightChunk " + chunkPos + " " + excludeBlocks));
- return CompletableFuture.supplyAsync(() -> {
- chunk.setLightCorrect(true);
- return chunk;
- }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task));
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void tryScheduleUpdate() {
- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) {
- this.taskMailbox.tell(() -> {
- this.runUpdate();
- this.scheduled.set(false);
- });
- }
+ // Paper - rewrite chunk system
}
private void runUpdate() {
- int i = Math.min(this.lightTasks.size(), 1000);
- ObjectListIterator<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> objectListIterator = this.lightTasks.iterator();
-
- int j;
- for (j = 0; objectListIterator.hasNext() && j < i; j++) {
- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair = objectListIterator.next();
- if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.PRE_UPDATE) {
- pair.getSecond().run();
- }
- }
-
- objectListIterator.back(j);
- super.runLightUpdates();
-
- for (int var5 = 0; objectListIterator.hasNext() && var5 < i; var5++) {
- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair2 = objectListIterator.next();
- if (pair2.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) {
- pair2.getSecond().run();
- }
-
- objectListIterator.remove();
- }
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public CompletableFuture<?> waitForPendingTasks(int x, int z) {
- return CompletableFuture.runAsync(() -> {
- }, callback -> this.addTask(x, z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, callback));
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
static enum TaskType {
diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java
index eba83b085435150e5954fd5d41dda9ce1d0601ad..daf543b51d8875b374688957ae4bc466f5512bcd 100644
--- a/src/main/java/net/minecraft/server/level/Ticket.java
+++ b/src/main/java/net/minecraft/server/level/Ticket.java
@@ -2,13 +2,25 @@ package net.minecraft.server.level;
import java.util.Objects;
-public final class Ticket<T> implements Comparable<Ticket<?>> {
+public final class Ticket<T> implements Comparable<Ticket<?>>, ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket<T> { // Paper - rewrite chunk system
private final TicketType<T> type;
private final int ticketLevel;
public final T key;
- private long createdTick;
+ // Paper start - rewrite chunk system
+ private long removeDelay;
- protected Ticket(TicketType<T> type, int level, T argument) {
+ @Override
+ public final long moonrise$getRemoveDelay() {
+ return this.removeDelay;
+ }
+
+ @Override
+ public final void moonrise$setRemoveDelay(final long removeDelay) {
+ this.removeDelay = removeDelay;
+ }
+ // Paper end - rewerite chunk system
+
+ public Ticket(TicketType<T> type, int level, T argument) { // Paper - public
this.type = type;
this.ticketLevel = level;
this.key = argument;
@@ -41,7 +53,7 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
@Override
public String toString() {
- return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] at " + this.createdTick;
+ return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] to die in " + this.removeDelay; // Paper - rewrite chunk system
}
public TicketType<T> getType() {
@@ -53,11 +65,10 @@ public final class Ticket<T> implements Comparable<Ticket<?>> {
}
protected void setCreatedTick(long tickCreated) {
- this.createdTick = tickCreated;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
protected boolean timedOut(long currentTick) {
- long l = this.type.timeout();
- return l != 0L && currentTick - this.createdTick > l;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
}
diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
index b26a4a38144ec1b171db911bbf949b53ed35708f..5a8a33638ceb1d980ffc3e6dd86e7eb11dfd9375 100644
--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java
+++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
@@ -85,6 +85,36 @@ public class WorldGenRegion implements WorldGenLevel {
private final AtomicLong subTickCount = new AtomicLong();
private static final ResourceLocation WORLDGEN_REGION_RANDOM = ResourceLocation.withDefaultNamespace("worldgen_region_random");
+ // Paper start - rewrite chunk system
+ /**
+ * During feature generation, light data is not initialised and will always return 15 in Starlight. Vanilla
+ * can possibly return 0 if partially initialised, which allows some mushroom blocks to generate.
+ * In general, the brightness value from the light engine should not be used until the chunk is ready. To emulate
+ * Vanilla behavior better, we return 0 as the brightness during world gen unless the target chunk is finished
+ * lighting.
+ */
+ @Override
+ public int getBrightness(final net.minecraft.world.level.LightLayer lightLayer, final BlockPos blockPos) {
+ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4);
+ if (!chunk.isLightCorrect()) {
+ return 0;
+ }
+ return this.getLightEngine().getLayerListener(lightLayer).getLightValue(blockPos);
+ }
+
+ /**
+ * See above
+ */
+ @Override
+ public int getRawBrightness(final BlockPos blockPos, final int subtract) {
+ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4);
+ if (!chunk.isLightCorrect()) {
+ return 0;
+ }
+ return this.getLightEngine().getRawBrightness(blockPos, subtract);
+ }
+ // Paper end - rewrite chunk system
+
public WorldGenRegion(ServerLevel world, StaticCache2D<GenerationChunkHolder> chunks, ChunkStep generationStep, ChunkAccess centerPos) {
this.generatingStep = generationStep;
this.cache = chunks;
diff --git a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java
index cdd66e6ce96e2613afe7f06ca8da3cfaa6704b2d..32634e45ac8433648e49e47e20081e15ad41ff15 100644
--- a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java
+++ b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java
@@ -78,7 +78,7 @@ public class PlayerChunkSender {
}
}
- private static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) {
+ public static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) { // Paper - public
handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.getLightEngine(), null, null));
// Paper start - PlayerChunkLoadEvent
if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) {
diff --git a/src/main/java/net/minecraft/util/SortedArraySet.java b/src/main/java/net/minecraft/util/SortedArraySet.java
index ea72dcb064a35bc6245bc5c94d592efedd8faf41..87ee8e51dfa7657ed7d83fcbceef48bf857043e1 100644
--- a/src/main/java/net/minecraft/util/SortedArraySet.java
+++ b/src/main/java/net/minecraft/util/SortedArraySet.java
@@ -8,12 +8,89 @@ import java.util.Iterator;
import java.util.NoSuchElementException;
import javax.annotation.Nullable;
-public class SortedArraySet<T> extends AbstractSet<T> {
+public class SortedArraySet<T> extends AbstractSet<T> implements ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet<T> { // Paper - rewrite chunk system
private static final int DEFAULT_INITIAL_CAPACITY = 10;
private final Comparator<T> comparator;
T[] contents;
int size;
+ // Paper start - rewrite chunk system
+ @Override
+ public final boolean removeIf(final java.util.function.Predicate<? super T> filter) {
+ // prev. impl used an iterator, which could be n^2 and creates garbage
+ int i = 0;
+ final int len = this.size;
+ final T[] backingArray = this.contents;
+
+ for (;;) {
+ if (i >= len) {
+ return false;
+ }
+ if (!filter.test(backingArray[i])) {
+ ++i;
+ continue;
+ }
+ break;
+ }
+
+ // we only want to write back to backingArray if we really need to
+
+ int lastIndex = i; // this is where new elements are shifted to
+
+ for (; i < len; ++i) {
+ final T curr = backingArray[i];
+ if (!filter.test(curr)) { // if test throws we're screwed
+ backingArray[lastIndex++] = curr;
+ }
+ }
+
+ // cleanup end
+ Arrays.fill(backingArray, lastIndex, len, null);
+ this.size = lastIndex;
+ return true;
+ }
+
+ @Override
+ public final T moonrise$replace(final T object) {
+ final int index = this.findIndex(object);
+ if (index >= 0) {
+ final T old = this.contents[index];
+ this.contents[index] = object;
+ return old;
+ } else {
+ this.addInternal(object, getInsertionPosition(index));
+ return object;
+ }
+ }
+
+ @Override
+ public final T moonrise$removeAndGet(final T object) {
+ int i = this.findIndex(object);
+ if (i >= 0) {
+ final T ret = this.contents[i];
+ this.removeInternal(i);
+ return ret;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public final SortedArraySet<T> moonrise$copy() {
+ final SortedArraySet<T> ret = SortedArraySet.create(this.comparator, 0);
+
+ ret.size = this.size;
+ ret.contents = Arrays.copyOf(this.contents, this.size);
+
+ return ret;
+ }
+
+ @Override
+ public Object[] moonrise$copyBackingArray() {
+ return this.contents.clone();
+ }
+ // Paper end - rewrite chunk system
+
private SortedArraySet(int initialCapacity, Comparator<T> comparator) {
this.comparator = comparator;
if (initialCapacity < 0) {
diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
index 2e2101274f3afebbae783fa119f5cae8104de45d..a7deceb2b9caad47f7f641ba4302d622d7127651 100644
--- a/src/main/java/net/minecraft/world/entity/Entity.java
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
@@ -167,7 +167,7 @@ import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.plugin.PluginManager;
// CraftBukkit end
-public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, CommandSource, ScoreHolder {
+public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, CommandSource, ScoreHolder, ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity { // Paper - rewrite chunk system
// CraftBukkit start
private static final int CURRENT_LEVEL = 2;
@@ -456,6 +456,77 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
return this.dimensions.makeBoundingBox(x, y, z);
}
// Paper end
+ // Paper start - rewrite chunk system
+ private final boolean isHardColliding = this.moonrise$isHardCollidingUncached();
+ private net.minecraft.server.level.FullChunkStatus chunkStatus;
+ private int sectionX = Integer.MIN_VALUE;
+ private int sectionY = Integer.MIN_VALUE;
+ private int sectionZ = Integer.MIN_VALUE;
+ private boolean updatingSectionStatus;
+
+ @Override
+ public final boolean moonrise$isHardColliding() {
+ return this.isHardColliding;
+ }
+
+ @Override
+ public final net.minecraft.server.level.FullChunkStatus moonrise$getChunkStatus() {
+ return this.chunkStatus;
+ }
+
+ @Override
+ public final void moonrise$setChunkStatus(final net.minecraft.server.level.FullChunkStatus status) {
+ this.chunkStatus = status;
+ }
+
+ @Override
+ public final int moonrise$getSectionX() {
+ return this.sectionX;
+ }
+
+ @Override
+ public final void moonrise$setSectionX(final int x) {
+ this.sectionX = x;
+ }
+
+ @Override
+ public final int moonrise$getSectionY() {
+ return this.sectionY;
+ }
+
+ @Override
+ public final void moonrise$setSectionY(final int y) {
+ this.sectionY = y;
+ }
+
+ @Override
+ public final int moonrise$getSectionZ() {
+ return this.sectionZ;
+ }
+
+ @Override
+ public final void moonrise$setSectionZ(final int z) {
+ this.sectionZ = z;
+ }
+
+ @Override
+ public final boolean moonrise$isUpdatingSectionStatus() {
+ return this.updatingSectionStatus;
+ }
+
+ @Override
+ public final void moonrise$setUpdatingSectionStatus(final boolean to) {
+ this.updatingSectionStatus = to;
+ }
+
+ @Override
+ public final boolean moonrise$hasAnyPlayerPassengers() {
+ if (this.passengers.isEmpty()) {
+ return false;
+ }
+ return this.getIndirectPassengersStream().anyMatch((entity) -> entity instanceof Player);
+ }
+ // Paper end - rewrite chunk system
public Entity(EntityType<?> type, Level world) {
this.id = Entity.ENTITY_COUNTER.incrementAndGet();
@@ -4397,6 +4468,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
this.setPosRaw(x, y, z, false);
}
public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) {
+ // Paper start - rewrite chunk system
+ if (this.updatingSectionStatus) {
+ LOGGER.error(
+ "Refusing to update position for entity " + this + " to position " + new Vec3(x, y, z)
+ + " since it is processing a section status update", new Throwable()
+ );
+ return;
+ }
+ // Paper end - rewrite chunk system
if (!checkPosition(this, x, y, z)) {
return;
}
@@ -4528,6 +4608,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
@Override
public final void setRemoved(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
+ // Paper start - rewrite chunk system
+ if (!((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this.level).moonrise$getEntityLookup().canRemoveEntity((Entity)(Object)this)) {
+ LOGGER.warn("Entity " + this + " is currently prevented from being removed from the world since it is processing section status updates", new Throwable());
+ return;
+ }
+ // Paper end - rewrite chunk system
CraftEventFactory.callEntityRemoveEvent(this, cause);
// CraftBukkit end
final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers
@@ -4539,7 +4625,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
this.stopRiding();
}
- this.getPassengers().forEach(Entity::stopRiding);
+ if (this.removalReason != Entity.RemovalReason.UNLOADED_TO_CHUNK) { this.getPassengers().forEach(Entity::stopRiding); } // Paper - rewrite chunk system
this.levelCallback.onRemove(entity_removalreason);
// Paper start - Folia schedulers
if (!(this instanceof ServerPlayer) && entity_removalreason != RemovalReason.CHANGED_DIMENSION && !alreadyRemoved) {
@@ -4570,7 +4656,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
@Override
public boolean shouldBeSaved() {
- return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !this.hasExactlyOnePlayerPassenger());
+ return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)this).moonrise$hasAnyPlayerPassengers()); // Paper - rewrite chunk system
}
@Override
diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
index fb63036d26d2b5370472b741b23bebd71e247463..274ddf479d38495d84838f9cd73c13d2841c3b44 100644
--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
@@ -38,12 +38,153 @@ import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
import net.minecraft.world.level.chunk.storage.SectionStorage;
import net.minecraft.world.level.chunk.storage.SimpleRegionStorage;
-public class PoiManager extends SectionStorage<PoiSection> {
+public class PoiManager extends SectionStorage<PoiSection> implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager { // Paper - rewrite chunk system
public static final int MAX_VILLAGE_DISTANCE = 6;
public static final int VILLAGE_SECTION_SIZE = 1;
private final PoiManager.DistanceTracker distanceTracker;
private final LongSet loadedChunks = new LongOpenHashSet();
+ // Paper start - rewrite chunk system
+ private final net.minecraft.server.level.ServerLevel world;
+
+ // the vanilla tracker needs to be replaced because it does not support level removes, and we need level removes
+ // to support poi unloading
+ private final ca.spottedleaf.moonrise.common.misc.Delayed26WayDistancePropagator3D villageDistanceTracker = new ca.spottedleaf.moonrise.common.misc.Delayed26WayDistancePropagator3D();
+
+ private static final int POI_DATA_SOURCE = 7;
+
+ private static int convertBetweenLevels(final int level) {
+ return POI_DATA_SOURCE - level;
+ }
+
+ private void updateDistanceTracking(long section) {
+ if (this.isVillageCenter(section)) {
+ this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE);
+ } else {
+ this.villageDistanceTracker.removeSource(section);
+ }
+ }
+
+ @Override
+ public Optional<PoiSection> get(final long pos) {
+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
+ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
+
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main");
+
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager;
+ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true);
+
+ return ret == null ? Optional.empty() : ret.getSectionForVanilla(chunkY);
+ }
+
+ @Override
+ public Optional<PoiSection> getOrLoad(final long pos) {
+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
+ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
+
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main");
+
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager;
+
+ if (chunkY >= ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this.world) && chunkY <= ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this.world)) {
+ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true);
+ if (ret != null) {
+ return ret.getSectionForVanilla(chunkY);
+ } else {
+ return manager.loadPoiChunk(chunkX, chunkZ).getSectionForVanilla(chunkY);
+ }
+ }
+ // retain vanilla behavior: do not load section if out of bounds!
+ return Optional.empty();
+ }
+
+ @Override
+ protected PoiSection getOrCreate(final long pos) {
+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
+ final int chunkY = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionY(pos);
+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
+
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main");
+
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager;
+
+ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true);
+ if (ret != null) {
+ return ret.getOrCreateSection(chunkY);
+ } else {
+ return manager.loadPoiChunk(chunkX, chunkZ).getOrCreateSection(chunkY);
+ }
+ }
+
+ @Override
+ public final net.minecraft.server.level.ServerLevel moonrise$getWorld() {
+ return this.world;
+ }
+
+ @Override
+ public final void moonrise$onUnload(final long coordinate) { // Paper - rewrite chunk system
+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(coordinate);
+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(coordinate);
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Unloading poi chunk off-main");
+ for (int section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) {
+ final long sectionPos = SectionPos.asLong(chunkX, section, chunkZ);
+ this.updateDistanceTracking(sectionPos);
+ }
+ }
+
+ @Override
+ public final void moonrise$loadInPoiChunk(final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk poiChunk) {
+ final int chunkX = poiChunk.chunkX;
+ final int chunkZ = poiChunk.chunkZ;
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Loading poi chunk off-main");
+ for (int sectionY = this.levelHeightAccessor.getMinSection(); sectionY < this.levelHeightAccessor.getMaxSection(); ++sectionY) {
+ final PoiSection section = poiChunk.getSection(sectionY);
+ if (section != null && !((ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection)section).moonrise$isEmpty()) {
+ this.onSectionLoad(SectionPos.asLong(chunkX, sectionY, chunkZ));
+ }
+ }
+ }
+
+ @Override
+ public final void moonrise$checkConsistency(final net.minecraft.world.level.chunk.ChunkAccess chunk) {
+ final int chunkX = chunk.getPos().x;
+ final int chunkZ = chunk.getPos().z;
+
+ final int minY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(chunk);
+ final int maxY = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(chunk);
+ final LevelChunkSection[] sections = chunk.getSections();
+ for (int section = minY; section <= maxY; ++section) {
+ this.checkConsistencyWithBlocks(SectionPos.of(chunkX, section, chunkZ), sections[section - minY]);
+ }
+ }
+
+ @Override
+ public final void moonrise$close() throws java.io.IOException {}
+
+ @Override
+ public final net.minecraft.nbt.CompoundTag moonrise$read(final int chunkX, final int chunkZ) throws java.io.IOException {
+ if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) {
+ return ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.loadData(
+ this.world, chunkX, chunkZ, ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.POI_DATA,
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread()
+ );
+ }
+ return this.moonrise$getRegionStorage().read(new ChunkPos(chunkX, chunkZ));
+ }
+
+ @Override
+ public final void moonrise$write(final int chunkX, final int chunkZ, final net.minecraft.nbt.CompoundTag data) throws java.io.IOException {
+ if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) {
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.scheduleSave(this.world, chunkX, chunkZ, data, ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.POI_DATA);
+ return;
+ }
+ this.moonrise$getRegionStorage().write(new ChunkPos(chunkX, chunkZ), data);
+ }
+ // Paper end - rewrite chunk system
+
public PoiManager(
RegionStorageInfo storageKey,
Path directory,
@@ -62,6 +203,7 @@ public class PoiManager extends SectionStorage<PoiSection> {
world
);
this.distanceTracker = new PoiManager.DistanceTracker();
+ this.world = (net.minecraft.server.level.ServerLevel)world; // Paper - rewrite chunk system
}
public void add(BlockPos pos, Holder<PoiType> type) {
@@ -195,8 +337,8 @@ public class PoiManager extends SectionStorage<PoiSection> {
}
public int sectionsToVillage(SectionPos pos) {
- this.distanceTracker.runAllUpdates();
- return this.distanceTracker.getLevel(pos.asLong());
+ this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system
+ return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(pos))); // Paper - rewrite chunk system
}
boolean isVillageCenter(long pos) {
@@ -210,19 +352,26 @@ public class PoiManager extends SectionStorage<PoiSection> {
@Override
public void tick(BooleanSupplier shouldKeepTicking) {
- super.tick(shouldKeepTicking);
- this.distanceTracker.runAllUpdates();
+ this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system
}
@Override
- protected void setDirty(long pos) {
- super.setDirty(pos);
- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
+ public void setDirty(long pos) { // Paper - public
+ // Paper start - rewrite chunk system
+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionX(pos);
+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionZ(pos);
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager;
+ final ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk chunk = manager.getPoiChunkIfLoaded(chunkX, chunkZ, false);
+ if (chunk != null) {
+ chunk.setDirty(true);
+ }
+ this.updateDistanceTracking(pos);
+ // Paper end - rewrite chunk system
}
@Override
protected void onSectionLoad(long pos) {
- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false);
+ this.updateDistanceTracking(pos); // Paper - rewrite chunk system
}
public void checkConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection chunkSection) {
@@ -259,7 +408,7 @@ public class PoiManager extends SectionStorage<PoiSection> {
.map(sectionPos -> Pair.of(sectionPos, this.getOrLoad(sectionPos.asLong())))
.filter(pair -> !pair.getSecond().map(PoiSection::isValid).orElse(false))
.map(pair -> pair.getFirst().chunk())
- .filter(chunkPos -> this.loadedChunks.add(chunkPos.toLong()))
+ // Paper - rewrite chunk system
.forEach(chunkPos -> world.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.EMPTY));
}
diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
index 971fb29a2c3dc713cb8ab1d2eed054cc16f9c93c..a6c0e89cb645693034f8e90ac2de8f2da457453c 100644
--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
@@ -23,7 +23,7 @@ import net.minecraft.core.SectionPos;
import net.minecraft.util.VisibleForDebug;
import org.slf4j.Logger;
-public class PoiSection {
+public class PoiSection implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection { // Paper - rewrite chunk system
private static final Logger LOGGER = LogUtils.getLogger();
private final Short2ObjectMap<PoiRecord> records = new Short2ObjectOpenHashMap<>();
private final Map<Holder<PoiType>, Set<PoiRecord>> byType = Maps.newHashMap();
@@ -42,6 +42,20 @@ public class PoiSection {
.orElseGet(Util.prefix("Failed to read POI section: ", LOGGER::error), () -> new PoiSection(updateListener, false, ImmutableList.of()));
}
+ // Paper start - rewrite chunk system
+ private final Optional<PoiSection> noAllocOptional = Optional.of((PoiSection)(Object)this);;
+
+ @Override
+ public final boolean moonrise$isEmpty() {
+ return this.isValid && this.records.isEmpty() && this.byType.isEmpty();
+ }
+
+ @Override
+ public final Optional<PoiSection> moonrise$asOptional() {
+ return this.noAllocOptional;
+ }
+ // Paper end - rewrite chunk system
+
public PoiSection(Runnable updateListener) {
this(updateListener, true, ImmutableList.of());
}
diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java
index bd20bea7f76a7307f1698fb2dfef37125032d166..70c2017400168d4fef3c14462798edcfed58d4bf 100644
--- a/src/main/java/net/minecraft/world/level/EntityGetter.java
+++ b/src/main/java/net/minecraft/world/level/EntityGetter.java
@@ -18,7 +18,7 @@ import net.minecraft.world.phys.shapes.BooleanOp;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
-public interface EntityGetter {
+public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter { // Paper - rewrite chunk system
List<Entity> getEntities(@Nullable Entity except, AABB box, Predicate<? super Entity> predicate);
<T extends Entity> List<T> getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate);
@@ -33,6 +33,13 @@ public interface EntityGetter {
return this.getEntities(except, box, EntitySelector.NO_SPECTATORS);
}
+ // Paper start - rewrite chunk system
+ @Override
+ default List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate) {
+ return this.getEntities(entity, box, predicate);
+ }
+ // Paper end - rewrite chunk system
+
default boolean isUnobstructed(@Nullable Entity except, VoxelShape shape) {
if (shape.isEmpty()) {
return true;
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
index e27d3547d1e19c137e05e6b8d075127a8bafb237..a022bbaa16054c56696a7a03e71ef042340d88ec 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
@@ -102,7 +102,7 @@ import org.bukkit.entity.SpawnCategory;
import org.bukkit.event.block.BlockPhysicsEvent;
// CraftBukkit end
-public abstract class Level implements LevelAccessor, AutoCloseable {
+public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel, ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter { // Paper - rewrite chunk system
public static final Codec<ResourceKey<Level>> RESOURCE_KEY_CODEC = ResourceKey.codec(Registries.DIMENSION);
public static final ResourceKey<Level> OVERWORLD = ResourceKey.create(Registries.DIMENSION, ResourceLocation.withDefaultNamespace("overworld"));
@@ -199,6 +199,63 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
public abstract ResourceKey<LevelStem> getTypeKey();
+ // Paper start - rewrite chunk system
+ private ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup;
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup moonrise$getEntityLookup() {
+ return this.entityLookup;
+ }
+
+ @Override
+ public void moonrise$setEntityLookup(final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup) {
+ if (this.entityLookup != null && !(this.entityLookup instanceof ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl.DefaultEntityLookup)) {
+ throw new IllegalStateException("Entity lookup already initialised");
+ }
+ this.entityLookup = entityLookup;
+ }
+
+ @Override
+ public final <T extends Entity> List<T> getEntitiesOfClass(final Class<T> entityClass, final AABB boundingBox, final Predicate<? super T> predicate) {
+ this.getProfiler().incrementCounter("getEntities");
+ final List<T> ret = new java.util.ArrayList<>();
+
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(entityClass, null, boundingBox, ret, predicate);
+
+ return ret;
+ }
+
+ @Override
+ public final List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate) {
+ this.getProfiler().incrementCounter("getEntities");
+ final List<Entity> ret = new java.util.ArrayList<>();
+
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getHardCollidingEntities(entity, box, ret, predicate);
+
+ return ret;
+ }
+
+ @Override
+ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) {
+ return this.getChunkSource().getChunk(chunkX, chunkZ, false);
+ }
+
+ @Override
+ public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) {
+ return this.getChunkSource().getChunk(chunkX, chunkZ, ChunkStatus.EMPTY, false);
+ }
+
+ @Override
+ public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus) {
+ return this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, false);
+ }
+
+ @Override
+ public void moonrise$midTickTasks() {
+ // no-op on ClientLevel
+ }
+ // Paper end - rewrite chunk system
+
protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, Supplier<ProfilerFiller> supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator) { // Paper - create paper world config
this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot
this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config
@@ -281,6 +338,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
this.timings = new co.aikar.timings.WorldTimingsHandler(this); // Paper - code below can generate new world and access timings
this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime);
this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime);
+ this.entityLookup = new ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl.DefaultEntityLookup(this); // Paper - rewrite chunk system
}
// Paper start - Cancel hit for vanished players
@@ -549,7 +607,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
this.setBlocksDirty(blockposition, iblockdata1, iblockdata2);
}
- if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement
+ if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.FULL)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement // Paper - rewrite chunk system - change from ticking to full
this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i);
}
@@ -813,6 +871,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
// Iterator<TickingBlockEntity> iterator = this.blockEntityTickers.iterator();
boolean flag = this.tickRateManager().runsNormally();
+ int tickedEntities = 0; // Paper - rewrite chunk system
+
int tilesThisCycle = 0;
var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<TickingBlockEntity>(); // Paper - Fix MC-117075; use removeAll
toRemove.add(null); // Paper - Fix MC-117075
@@ -828,6 +888,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
// Spigot end
} else if (flag && this.shouldTickBlocksAt(tickingblockentity.getPos())) {
tickingblockentity.tick();
+ // Paper start - rewrite chunk system
+ if ((++tickedEntities & 7) == 0) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)(Level)(Object)this).moonrise$midTickTasks();
+ }
+ // Paper end - rewrite chunk system
}
}
this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075
@@ -850,6 +915,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD);
// Paper end - Prevent block entity and entity crashes
}
+ this.moonrise$midTickTasks(); // Paper - rewrite chunk system
}
// Paper start - Option to prevent armor stands from doing entity lookups
@Override
@@ -949,7 +1015,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
}
// Paper end - Perf: Optimize capturedTileEntities lookup
// CraftBukkit end
- return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && Thread.currentThread() != this.thread ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE));
+ return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && !io.papermc.paper.util.TickThread.isTickThread() ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); // Paper - rewrite chunk system
}
public void setBlockEntity(BlockEntity blockEntity) {
@@ -1039,28 +1105,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
@Override
public List<Entity> getEntities(@Nullable Entity except, AABB box, Predicate<? super Entity> predicate) {
this.getProfiler().incrementCounter("getEntities");
- List<Entity> list = Lists.newArrayList();
+ // Paper start - rewrite chunk system
+ final List<Entity> ret = new java.util.ArrayList<>();
- this.getEntities().get(box, (entity1) -> {
- if (entity1 != except && predicate.test(entity1)) {
- list.add(entity1);
- }
-
- if (entity1 instanceof EnderDragon) {
- EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity1).getSubEntities();
- int i = aentitycomplexpart.length;
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(except, box, ret, predicate);
- for (int j = 0; j < i; ++j) {
- EnderDragonPart entitycomplexpart = aentitycomplexpart[j];
-
- if (entity1 != except && predicate.test(entitycomplexpart)) {
- list.add(entitycomplexpart);
- }
- }
- }
-
- });
- return list;
+ return ret;
+ // Paper end - rewrite chunk system
}
@Override
@@ -1075,36 +1126,77 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
this.getEntities(filter, box, predicate, result, Integer.MAX_VALUE);
}
- public <T extends Entity> void getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate, List<? super T> result, int limit) {
+ // Paper start - rewrite chunk system
+ public <T extends Entity> void getEntities(final EntityTypeTest<Entity, T> entityTypeTest,
+ final AABB boundingBox, final Predicate<? super T> predicate,
+ final List<? super T> into, final int maxCount) {
this.getProfiler().incrementCounter("getEntities");
- this.getEntities().get(filter, box, (entity) -> {
- if (predicate.test(entity)) {
- result.add(entity);
- if (result.size() >= limit) {
- return AbortableIterationConsumer.Continuation.ABORT;
- }
+
+ if (entityTypeTest instanceof net.minecraft.world.entity.EntityType<T> byType) {
+ if (maxCount != Integer.MAX_VALUE) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate, maxCount);
+ return;
+ } else {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(byType, boundingBox, into, predicate);
+ return;
}
+ }
- if (entity instanceof EnderDragon entityenderdragon) {
- EnderDragonPart[] aentitycomplexpart = entityenderdragon.getSubEntities();
- int j = aentitycomplexpart.length;
+ if (entityTypeTest == null) {
+ if (maxCount != Integer.MAX_VALUE) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate, maxCount);
+ return;
+ } else {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)predicate);
+ return;
+ }
+ }
- for (int k = 0; k < j; ++k) {
- EnderDragonPart entitycomplexpart = aentitycomplexpart[k];
- T t0 = filter.tryCast(entitycomplexpart); // CraftBukkit - decompile error
+ final Class<? extends Entity> base = entityTypeTest.getBaseClass();
- if (t0 != null && predicate.test(t0)) {
- result.add(t0);
- if (result.size() >= limit) {
- return AbortableIterationConsumer.Continuation.ABORT;
- }
- }
+ final Predicate<? super T> modifiedPredicate;
+ if (predicate == null) {
+ modifiedPredicate = (final T obj) -> {
+ return entityTypeTest.tryCast(obj) != null;
+ };
+ } else {
+ modifiedPredicate = (final Entity obj) -> {
+ final T casted = entityTypeTest.tryCast(obj);
+ if (casted == null) {
+ return false;
}
+
+ return predicate.test(casted);
+ };
+ }
+
+ if (base == null || base == Entity.class) {
+ if (maxCount != Integer.MAX_VALUE) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount);
+ return;
+ } else {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate);
+ return;
}
+ } else {
+ if (maxCount != Integer.MAX_VALUE) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate, maxCount);
+ return;
+ } else {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(base, null, boundingBox, (List)into, (Predicate)modifiedPredicate);
+ return;
+ }
+ }
+ }
- return AbortableIterationConsumer.Continuation.CONTINUE;
- });
+ public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) {
+ ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices slices = ((ServerLevel)this).moonrise$getEntityLookup().getChunk(chunkX, chunkZ);
+ if (slices == null) {
+ return new org.bukkit.entity.Entity[0];
+ }
+ return slices.getChunkEntities();
}
+ // Paper end - rewrite chunk system
@Nullable
public abstract Entity getEntity(int id);
diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java
index a0ae26d6197e1069ca09982b4f8b706c55ae8491..1a4dc4b2561dbaf01246b4fb46266b1ac84008b8 100644
--- a/src/main/java/net/minecraft/world/level/LevelReader.java
+++ b/src/main/java/net/minecraft/world/level/LevelReader.java
@@ -22,7 +22,18 @@ import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.AABB;
-public interface LevelReader extends BlockAndTintGetter, CollisionGetter, SignalGetter, BiomeManager.NoiseBiomeSource {
+public interface LevelReader extends ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader, BlockAndTintGetter, CollisionGetter, SignalGetter, BiomeManager.NoiseBiomeSource { // Paper - rewrite chunk system
+
+ // Paper start - rewrite chunk system
+ @Override
+ public default ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status) {
+ if (status == null || status.isOrAfter(ChunkStatus.FULL)) {
+ throw new IllegalArgumentException("Status: " + status.toString());
+ }
+ return ((LevelReader)this).getChunk(chunkX, chunkZ, status, true);
+ }
+ // Paper end - rewrite chunk system
+
@Nullable
ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create);
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 6c4a339be29bb9c07b741a1ca12de2217c8687ba..a768b07dae4bf75b68e3bc1d3de4b68fc7d23842 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
@@ -762,7 +762,7 @@ public abstract class BlockBehaviour implements FeatureElement {
boolean test(BlockState state, BlockGetter world, BlockPos pos);
}
- public abstract static class BlockStateBase extends StateHolder<Block, BlockState> {
+ public abstract static class BlockStateBase extends StateHolder<Block, BlockState> implements ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState { // Paper - rewrite chunk system
private final int lightEmission;
private final boolean useShapeForLightOcclusion;
@@ -794,6 +794,21 @@ public abstract class BlockBehaviour implements FeatureElement {
private FluidState fluidState;
private boolean isRandomlyTicking;
+ // Paper start - rewrite chunk system
+ private int opacityIfCached;
+ private boolean isConditionallyFullOpaque;
+
+ @Override
+ public final boolean starlight$isConditionallyFullOpaque() {
+ return this.isConditionallyFullOpaque;
+ }
+
+ @Override
+ public final int starlight$getOpacityIfCached() {
+ return this.opacityIfCached;
+ }
+ // Paper end - rewrite chunk system
+
protected BlockStateBase(Block block, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<BlockState> codec) {
super(block, propertyMap, codec);
this.fluidState = Fluids.EMPTY.defaultFluidState();
@@ -864,6 +879,10 @@ public abstract class BlockBehaviour implements FeatureElement {
this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here
this.legacySolid = this.calculateSolid();
+ // Paper start - rewrite chunk system
+ this.isConditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion;
+ this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque ? -1 : this.cache.lightBlock;
+ // Paper end - rewrite chunk system
}
public Block getBlock() {
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 db4d95ce98eb1490d5306d1f74b282d27264871a..fb7bdf43fdc4d816b1c1f1f063bc170561c9544f 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
@@ -57,7 +57,7 @@ import net.minecraft.world.ticks.SerializableTickContainer;
import net.minecraft.world.ticks.TickContainerAccess;
import org.slf4j.Logger;
-public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiomeSource, LightChunk, StructureAccess {
+public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiomeSource, LightChunk, StructureAccess, ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
public static final int NO_FILLED_SECTION = -1;
private static final Logger LOGGER = LogUtils.getLogger();
@@ -77,7 +77,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
@Nullable
protected BlendingData blendingData;
public final Map<Heightmap.Types, Heightmap> heightmaps = Maps.newEnumMap(Heightmap.Types.class);
- protected ChunkSkyLightSources skyLightSources;
+ // Paper - rewrite chunk system
private final Map<Structure, StructureStart> structureStarts = Maps.newHashMap();
private final Map<Structure, LongSet> structuresRefences = Maps.newHashMap();
protected final Map<BlockPos, CompoundTag> pendingBlockEntities = Maps.newHashMap();
@@ -90,6 +90,53 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(ChunkAccess.DATA_TYPE_REGISTRY);
// CraftBukkit end
+ // Paper start - rewrite chunk system
+ private volatile ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles;
+ private volatile ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] skyNibbles;
+ private volatile boolean[] skyEmptinessMap;
+ private volatile boolean[] blockEmptinessMap;
+
+ @Override
+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() {
+ return this.blockNibbles;
+ }
+
+ @Override
+ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
+ this.blockNibbles = nibbles;
+ }
+
+ @Override
+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() {
+ return this.skyNibbles;
+ }
+
+ @Override
+ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
+ this.skyNibbles = nibbles;
+ }
+
+ @Override
+ public boolean[] starlight$getSkyEmptinessMap() {
+ return this.skyEmptinessMap;
+ }
+
+ @Override
+ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) {
+ this.skyEmptinessMap = emptinessMap;
+ }
+
+ @Override
+ public boolean[] starlight$getBlockEmptinessMap() {
+ return this.blockEmptinessMap;
+ }
+
+ @Override
+ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) {
+ this.blockEmptinessMap = emptinessMap;
+ }
+ // Paper end - rewrite chunk system
+
public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry<Biome> biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) {
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
@@ -99,7 +146,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
this.inhabitedTime = inhabitedTime;
this.postProcessing = new ShortList[heightLimitView.getSectionsCount()];
this.blendingData = blendingData;
- this.skyLightSources = new ChunkSkyLightSources(heightLimitView);
+ // Paper - rewrite chunk system
if (sectionArray != null) {
if (this.sections.length == sectionArray.length) {
System.arraycopy(sectionArray, 0, this.sections, 0, this.sections.length);
@@ -111,6 +158,12 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
ChunkAccess.replaceMissingSections(biomeRegistry, this.sections);
// CraftBukkit start
this.biomeRegistry = biomeRegistry;
+ // Paper start - rewrite chunk system
+ if (!((Object)this instanceof ImposterProtoChunk)) {
+ this.starlight$setBlockNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(heightLimitView));
+ this.starlight$setSkyNibbles(ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(heightLimitView));
+ }
+ // Paper end - rewrite chunk system
}
public final Registry<Biome> biomeRegistry;
// CraftBukkit end
@@ -514,12 +567,12 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
}
public void initializeLightSources() {
- this.skyLightSources.fillFrom(this);
+ // Paper - rewrite chunk system
}
@Override
public ChunkSkyLightSources getSkyLightSources() {
- return this.skyLightSources;
+ return null; // Paper - rewrite chunk system
}
public static record TicksToSave(SerializableTickContainer<Block> blocks, SerializableTickContainer<Fluid> fluids) {
diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
index 29697fad32dad3377eebc82d280ba48d3c1ad516..488938c32a48437721a71d294c77468f00c035b9 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
@@ -119,7 +119,7 @@ public abstract class ChunkGenerator {
return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("init_biomes", () -> {
chunk.fillBiomesFromNoise(this.biomeSource, noiseConfig.sampler());
return chunk;
- }), Util.backgroundExecutor());
+ }), Runnable::run); // Paper - rewrite chunk system
}
public abstract void applyCarvers(WorldGenRegion chunkRegion, long seed, RandomState noiseConfig, BiomeManager biomeAccess, StructureManager structureAccessor, ChunkAccess chunk, GenerationStep.Carving carverStep);
@@ -314,7 +314,7 @@ public abstract class ChunkGenerator {
return Pair.of(placement.getLocatePos(pos), holder);
}
- ChunkAccess ichunkaccess = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS);
+ ChunkAccess ichunkaccess = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader)world).moonrise$syncLoadNonFull(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); // Paper - rewrite chunk system
structurestart = structureAccessor.getStartForStructure(SectionPos.bottomOf(ichunkaccess), (Structure) holder.value(), ichunkaccess);
} while (structurestart == null);
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 dcc0acd259920463a4464213b9a5e793603852f9..ef4161884574d3d137e12591d983dc95a960cb19 100644
--- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.material.Fluids;
-public class EmptyLevelChunk extends LevelChunk {
+public class EmptyLevelChunk extends LevelChunk implements ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
private final Holder<Biome> biome;
public EmptyLevelChunk(Level world, ChunkPos pos, Holder<Biome> biomeEntry) {
@@ -21,6 +21,40 @@ public class EmptyLevelChunk extends LevelChunk {
this.biome = biomeEntry;
}
+ // Paper start - rewrite chunk system
+ @Override
+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() {
+ return ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(this.getLevel());
+ }
+
+ @Override
+ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {}
+
+ @Override
+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() {
+ return ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(this.getLevel());
+ }
+
+ @Override
+ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {}
+
+ @Override
+ public boolean[] starlight$getSkyEmptinessMap() {
+ return null;
+ }
+
+ @Override
+ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) {}
+
+ @Override
+ public boolean[] starlight$getBlockEmptinessMap() {
+ return null;
+ }
+
+ @Override
+ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) {}
+ // Paper end - rewrite chunk system
+
@Override
public BlockState getBlockState(BlockPos pos) {
return Blocks.VOID_AIR.defaultBlockState();
diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
index 365074be989aa4a178114fd5e9810f1a68640196..4af698930712389881601069a921f054c07935f2 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
@@ -31,7 +31,7 @@ import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.ticks.BlackholeTickAccess;
import net.minecraft.world.ticks.TickContainerAccess;
-public class ImposterProtoChunk extends ProtoChunk {
+public class ImposterProtoChunk extends ProtoChunk implements ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
private final LevelChunk wrapped;
private final boolean allowWrites;
@@ -47,6 +47,48 @@ public class ImposterProtoChunk extends ProtoChunk {
this.allowWrites = propagateToWrapped;
}
+ // Paper start - rewrite chunk system
+ @Override
+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() {
+ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getBlockNibbles();
+ }
+
+ @Override
+ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setBlockNibbles(nibbles);
+ }
+
+ @Override
+ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() {
+ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getSkyNibbles();
+ }
+
+ @Override
+ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {
+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setSkyNibbles(nibbles);
+ }
+
+ @Override
+ public boolean[] starlight$getSkyEmptinessMap() {
+ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getSkyEmptinessMap();
+ }
+
+ @Override
+ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) {
+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setSkyEmptinessMap(emptinessMap);
+ }
+
+ @Override
+ public boolean[] starlight$getBlockEmptinessMap() {
+ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getBlockEmptinessMap();
+ }
+
+ @Override
+ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) {
+ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setBlockEmptinessMap(emptinessMap);
+ }
+ // Paper end - rewrite chunk system
+
@Nullable
@Override
public BlockEntity getBlockEntity(BlockPos pos) {
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 602ad80c2b93d320bf2a25832d25a58cb8c72e4b..443e5e1b1c0e7c93f61c1905c78c29a17860989c 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
@@ -53,7 +53,7 @@ import net.minecraft.world.ticks.LevelChunkTicks;
import net.minecraft.world.ticks.TickContainerAccess;
import org.slf4j.Logger;
-public class LevelChunk extends ChunkAccess {
+public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk, ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system
static final Logger LOGGER = LogUtils.getLogger();
private static final TickingBlockEntity NULL_TICKER = new TickingBlockEntity() {
@@ -119,6 +119,14 @@ public class LevelChunk extends ChunkAccess {
// Paper start
boolean loadedTicketLevel;
// Paper end
+ // Paper start - rewrite chunk system
+ private boolean postProcessingDone;
+
+ @Override
+ public final boolean moonrise$isPostProcessingDone() {
+ return this.postProcessingDone;
+ }
+ // Paper end - rewrite chunk system
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());
@@ -148,13 +156,19 @@ public class LevelChunk extends ChunkAccess {
}
}
- this.skyLightSources = protoChunk.skyLightSources;
+ // Paper - rewrite chunk system
this.setLightCorrect(protoChunk.isLightCorrect());
this.unsaved = true;
this.needsDecoration = true; // CraftBukkit
// CraftBukkit start
this.persistentDataContainer = protoChunk.persistentDataContainer; // SPIGOT-6814: copy PDC to account for 1.17 to 1.18 chunk upgrading.
// CraftBukkit end
+ // Paper start - rewrite chunk system
+ this.starlight$setBlockNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getBlockNibbles());
+ this.starlight$setSkyNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getSkyNibbles());
+ this.starlight$setSkyEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getSkyEmptinessMap());
+ this.starlight$setBlockEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getBlockEmptinessMap());
+ // Paper end - rewrite chunk system
}
@Override
@@ -337,7 +351,7 @@ public class LevelChunk extends ChunkAccess {
ProfilerFiller gameprofilerfiller = this.level.getProfiler();
gameprofilerfiller.push("updateSkyLightSources");
- this.skyLightSources.update(this, j, i, l);
+ // Paper - rewrite chunk system
gameprofilerfiller.popPush("queueCheckLight");
this.level.getChunkSource().getLightEngine().checkBlock(blockposition);
gameprofilerfiller.pop();
@@ -597,11 +611,12 @@ public class LevelChunk extends ChunkAccess {
// CraftBukkit start
public void loadCallback() {
+ if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Paper
// Paper start
this.loadedTicketLevel = true;
// Paper end
org.bukkit.Server server = this.level.getCraftServer();
- this.level.getChunkSource().addLoadedChunk(this); // Paper
+ // Paper - rewrite chunk system
if (server != null) {
/*
* If it's a new world, the first few chunks are generated inside
@@ -610,6 +625,7 @@ public class LevelChunk extends ChunkAccess {
*/
org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(bukkitChunk, this.needsDecoration));
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.locX, this.locZ).getEntityChunk().callEntitiesLoadEvent(); // Paper - rewrite chunk system
if (this.needsDecoration) {
try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper
@@ -638,13 +654,15 @@ public class LevelChunk extends ChunkAccess {
}
public void unloadCallback() {
+ if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper
org.bukkit.Server server = this.level.getCraftServer();
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.locX, this.locZ).getEntityChunk().callEntitiesUnloadEvent(); // Paper - rewrite chunk system
org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
- org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, this.isUnsaved());
+ org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, true); // Paper - rewrite chunk system - force save to true so that mustNotSave is correctly set below
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 - rewrite chunk system
// Paper start
this.loadedTicketLevel = false;
// Paper end
@@ -652,8 +670,27 @@ public class LevelChunk extends ChunkAccess {
@Override
public boolean isUnsaved() {
- return super.isUnsaved() && !this.mustNotSave;
+ // Paper start - rewrite chunk system
+ final long gameTime = this.level.getGameTime();
+ if (((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$isDirty(gameTime)
+ || ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$isDirty(gameTime)) {
+ return true;
+ }
+
+ return super.isUnsaved();
+ // Paper end - rewrite chunk system
+ }
+
+ // Paper start - rewrite chunk system
+ @Override
+ public void setUnsaved(final boolean needsSaving) {
+ if (!needsSaving) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$clearDirty();
+ ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$clearDirty();
+ }
+ super.setUnsaved(needsSaving);
}
+ // Paper end - rewrite chunk system
// CraftBukkit end
public boolean isEmpty() {
@@ -759,6 +796,7 @@ public class LevelChunk extends ChunkAccess {
this.pendingBlockEntities.clear();
this.upgradeData.upgrade(this);
+ this.postProcessingDone = true; // Paper - rewrite chunk system
}
@Nullable
diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
index 2fa0097a9374a89177e4f1068d1bfed30b8ff122..fa9df6ebcd90d4e9e5836a37212b1f60665783b1 100644
--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
@@ -155,7 +155,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
return this.get(this.strategy.getIndex(x, y, z));
}
- protected T get(int index) {
+ public T get(int index) { // Paper - public
PalettedContainer.Data<T> data = this.data;
return data.palette.valueFor(data.storage.get(index));
}
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 7f302405a88766c2112539d24d3dd2e513f94985..207dc31afcf5ca5a59ab27ee263aa10f94a79559 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
@@ -143,7 +143,7 @@ public class ProtoChunk extends ChunkAccess {
}
if (LightEngine.hasDifferentLightProperties(this, pos, blockState, state)) {
- this.skyLightSources.update(this, m, j, o);
+ // Paper - rewrite chunk system
this.lightEngine.checkBlock(pos);
}
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java
index b1058bf0dcda544a074f4d3772d7899b94f98927..b7bf82f6b6023bd628d3e7ea84d2d6755a0d931a 100644
--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java
+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java
@@ -54,7 +54,7 @@ public record ChunkPyramid(ImmutableList<ChunkStep> steps) {
.step(ChunkStatus.CARVERS, builder -> builder)
.step(ChunkStatus.FEATURES, builder -> builder)
.step(ChunkStatus.INITIALIZE_LIGHT, builder -> builder.setTask(ChunkStatusTasks::initializeLight))
- .step(ChunkStatus.LIGHT, builder -> builder.addRequirement(ChunkStatus.INITIALIZE_LIGHT, 1).setTask(ChunkStatusTasks::light))
+ .step(ChunkStatus.LIGHT, builder -> builder.setTask(ChunkStatusTasks::light)) // Paper - rewrite chunk system - starlight does not need neighbours
.step(ChunkStatus.SPAWN, builder -> builder)
.step(ChunkStatus.FULL, builder -> builder.setTask(ChunkStatusTasks::full))
.build();
diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
index 0baa4adf2a4401f9c955352f27e6f99957d1dff4..3723c07183e7b894cccf4d01bedf1d0d832c1910 100644
--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
@@ -11,7 +11,7 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.levelgen.Heightmap;
import org.jetbrains.annotations.VisibleForTesting;
-public class ChunkStatus {
+public class ChunkStatus implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus { // Paper - rewrite chunk system
public static final int MAX_STRUCTURE_DISTANCE = 8;
private static final EnumSet<Heightmap.Types> WORLDGEN_HEIGHTMAPS = EnumSet.of(Heightmap.Types.OCEAN_FLOOR_WG, Heightmap.Types.WORLD_SURFACE_WG);
public static final EnumSet<Heightmap.Types> FINAL_HEIGHTMAPS = EnumSet.of(
@@ -51,8 +51,68 @@ public class ChunkStatus {
return list;
}
+ // Paper start - rewrite chunk system
+ private boolean isParallelCapable;
+ private boolean emptyLoadTask;
+ private int writeRadius;
+ private ChunkStatus nextStatus;
+ private java.util.concurrent.atomic.AtomicBoolean warnedAboutNoImmediateComplete;
+
+ @Override
+ public final boolean moonrise$isParallelCapable() {
+ return this.isParallelCapable;
+ }
+
+ @Override
+ public final void moonrise$setParallelCapable(final boolean value) {
+ this.isParallelCapable = value;
+ }
+
+ @Override
+ public final int moonrise$getWriteRadius() {
+ return this.writeRadius;
+ }
+
+ @Override
+ public final void moonrise$setWriteRadius(final int value) {
+ this.writeRadius = value;
+ }
+
+ @Override
+ public final ChunkStatus moonrise$getNextStatus() {
+ return this.nextStatus;
+ }
+
+ @Override
+ public final boolean moonrise$isEmptyLoadStatus() {
+ return this.emptyLoadTask;
+ }
+
+ @Override
+ public void moonrise$setEmptyLoadStatus(final boolean value) {
+ this.emptyLoadTask = value;
+ }
+
+ @Override
+ public final boolean moonrise$isEmptyGenStatus() {
+ return (Object)this == ChunkStatus.EMPTY;
+ }
+
+ @Override
+ public final java.util.concurrent.atomic.AtomicBoolean moonrise$getWarnedAboutNoImmediateComplete() {
+ return this.warnedAboutNoImmediateComplete;
+ }
+ // Paper end - rewrite chunk system
+
@VisibleForTesting
protected ChunkStatus(@Nullable ChunkStatus previous, EnumSet<Heightmap.Types> heightMapTypes, ChunkType chunkType) {
+ this.isParallelCapable = false;
+ this.writeRadius = -1;
+ this.nextStatus = (ChunkStatus)(Object)this;
+ if (previous != null) {
+ previous.nextStatus = (ChunkStatus)(Object)this;
+ }
+ this.warnedAboutNoImmediateComplete = new java.util.concurrent.atomic.AtomicBoolean();
this.parent = previous == null ? this : previous;
this.chunkType = chunkType;
this.heightmapsAfter = heightMapTypes;
diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
index ae16cf5c803caae636860dd9b1a83abe479ca5a4..b993c4b2595e2879b25753c2e34530f3622c18fa 100644
--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
@@ -154,7 +154,7 @@ public class ChunkStatusTasks {
chunk1 = ((ImposterProtoChunk) protochunk).getWrapped();
} else {
chunk1 = new LevelChunk(worldserver, protochunk, ($) -> { // Paper - decompile fix
- ChunkStatusTasks.postLoadProtoChunk(worldserver, protochunk.getEntities());
+ ChunkStatusTasks.postLoadProtoChunk(worldserver, protochunk.getEntities(), protochunk.getPos()); // Paper - rewrite chunk system
});
generationchunkholder.replaceProtoChunk(new ImposterProtoChunk(chunk1, false));
}
@@ -175,7 +175,7 @@ public class ChunkStatusTasks {
});
}
- private static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities) {
+ public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities, ChunkPos pos) { // Paper - public, add ChunkPos param
if (!entities.isEmpty()) {
// CraftBukkit start - these are spawned serialized (DefinedStructure) and we don't call an add event below at the moment due to ordering complexities
world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(entities, world).filter((entity) -> {
@@ -191,7 +191,7 @@ public class ChunkStatusTasks {
}
checkDupeUUID(world, entity); // Paper - duplicate uuid resolving
return !needsRemoval;
- }));
+ }), pos); // Paper - rewrite chunk system
// CraftBukkit end
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java
index f6e08a8334633ff1532616d051bed46b702d0091..4e56398a6fb8b97199f4c74ebebc1055fb718dcf 100644
--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java
+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStep.java
@@ -11,9 +11,50 @@ import net.minecraft.util.profiling.jfr.callback.ProfiledDuration;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ProtoChunk;
-public record ChunkStep(
- ChunkStatus targetStatus, ChunkDependencies directDependencies, ChunkDependencies accumulatedDependencies, int blockStateWriteRadius, ChunkStatusTask task
-) {
+// Paper start - rewerite chunk system - convert record to class
+public final class ChunkStep implements ca.spottedleaf.moonrise.patches.chunk_system.status.ChunkSystemChunkStep { // Paper - rewrite chunk system
+ private final ChunkStatus targetStatus;
+ private final ChunkDependencies directDependencies;
+ private final ChunkDependencies accumulatedDependencies;
+ private final int blockStateWriteRadius;
+ private final ChunkStatusTask task;
+
+ private final ChunkStatus[] byRadius; // Paper - rewrite chunk system
+
+ public ChunkStep(
+ ChunkStatus targetStatus, ChunkDependencies directDependencies, ChunkDependencies accumulatedDependencies, int blockStateWriteRadius, ChunkStatusTask task
+ ) {
+ this.targetStatus = targetStatus;
+ this.directDependencies = directDependencies;
+ this.accumulatedDependencies = accumulatedDependencies;
+ this.blockStateWriteRadius = blockStateWriteRadius;
+ this.task = task;
+
+ // Paper start - rewrite chunk system
+ this.byRadius = new ChunkStatus[this.getAccumulatedRadiusOf(ChunkStatus.EMPTY) + 1];
+ this.byRadius[0] = targetStatus.getParent();
+
+ for (ChunkStatus status = targetStatus.getParent(); status != ChunkStatus.EMPTY; status = status.getParent()) {
+ final int radius = this.getAccumulatedRadiusOf(status);
+
+ for (int j = 0; j <= radius; ++j) {
+ if (this.byRadius[j] == null) {
+ this.byRadius[j] = status;
+ }
+ }
+ }
+ // Paper end - rewrite chunk system
+ }
+
+ // Paper start - rewrite chunk system
+ @Override
+ public final ChunkStatus moonrise$getRequiredStatusAtRadius(final int radius) {
+ return this.byRadius[radius];
+ }
+ // Paper end - rewrite chunk system
+
+ // Paper start - rewerite chunk system - convert record to class
+
public int getAccumulatedRadiusOf(ChunkStatus status) {
return status == this.targetStatus ? 0 : this.accumulatedDependencies.getRadiusOf(status);
}
@@ -39,6 +80,56 @@ public record ChunkStep(
return chunk;
}
+ // Paper start - rewerite chunk system - convert record to class
+ public ChunkStatus targetStatus() {
+ return targetStatus;
+ }
+
+ public ChunkDependencies directDependencies() {
+ return directDependencies;
+ }
+
+ public ChunkDependencies accumulatedDependencies() {
+ return accumulatedDependencies;
+ }
+
+ public int blockStateWriteRadius() {
+ return blockStateWriteRadius;
+ }
+
+ public ChunkStatusTask task() {
+ return task;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) return true;
+ if (obj == null || obj.getClass() != this.getClass()) return false;
+ var that = (net.minecraft.world.level.chunk.status.ChunkStep) obj;
+ return java.util.Objects.equals(this.targetStatus, that.targetStatus) &&
+ java.util.Objects.equals(this.directDependencies, that.directDependencies) &&
+ java.util.Objects.equals(this.accumulatedDependencies, that.accumulatedDependencies) &&
+ this.blockStateWriteRadius == that.blockStateWriteRadius &&
+ java.util.Objects.equals(this.task, that.task);
+ }
+
+ @Override
+ public int hashCode() {
+ return java.util.Objects.hash(targetStatus, directDependencies, accumulatedDependencies, blockStateWriteRadius, task);
+ }
+
+ @Override
+ public String toString() {
+ return "ChunkStep[" +
+ "targetStatus=" + targetStatus + ", " +
+ "directDependencies=" + directDependencies + ", " +
+ "accumulatedDependencies=" + accumulatedDependencies + ", " +
+ "blockStateWriteRadius=" + blockStateWriteRadius + ", " +
+ "task=" + task + ']';
+ }
+ // Paper end - rewerite chunk system - convert record to class
+
+
public static class Builder {
private final ChunkStatus status;
@Nullable
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
index d42585bccb03f8ee1be5e37cfbe8520af4cc5454..977bebe8657abc5cb84ede8276d6781cde20e847 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
@@ -165,7 +165,7 @@ public class ChunkSerializer {
achunksection[k] = chunksection;
SectionPos sectionposition = SectionPos.of(chunkPos, b0);
- poiStorage.checkConsistencyWithBlocks(sectionposition, chunksection);
+ // Paper - rewrite chunk system - moved to final load stage
}
boolean flag3 = nbttagcompound1.contains("BlockLight", 7);
@@ -287,6 +287,8 @@ public class ChunkSerializer {
}
}
+ ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.loadLightHook(world, chunkPos, nbt, (ChunkAccess)object1); // Paper - rewrite chunk system - note: it's ok to pass the raw value instead of wrapped
+
if (chunktype == ChunkType.LEVELCHUNK) {
return new ImposterProtoChunk((LevelChunk) object1, false);
} else {
@@ -341,14 +343,44 @@ public class ChunkSerializer {
}
// CraftBukkit end
+ // Paper start - async chunk saving
+ // must be called sync
+ public static ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData getAsyncSaveData(ServerLevel world, ChunkAccess chunk) {
+ io.papermc.paper.util.TickThread.ensureTickThread(world, chunk.locX, chunk.locZ, "Preparing async chunk save data");
+
+ final CompoundTag tickLists = new CompoundTag();
+ ChunkSerializer.saveTicks(world, tickLists, chunk.getTicksForSerialization());
+
+ ListTag blockEntitiesSerialized = new ListTag();
+ for (final BlockPos blockPos : chunk.getBlockEntitiesPos()) {
+ final CompoundTag blockEntityNbt = chunk.getBlockEntityNbtForSaving(blockPos, world.registryAccess());
+ if (blockEntityNbt != null) {
+ blockEntitiesSerialized.add(blockEntityNbt);
+ }
+ }
+
+ return new ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData(
+ tickLists.get(BLOCK_TICKS_TAG),
+ tickLists.get(FLUID_TICKS_TAG),
+ blockEntitiesSerialized,
+ world.getGameTime()
+ );
+ }
+ // Paper end - async chunk saving
+
public static CompoundTag write(ServerLevel world, ChunkAccess chunk) {
+ // Paper start - async chunk saving
+ return saveChunk(world, chunk, null);
+ }
+ public static CompoundTag saveChunk(ServerLevel world, ChunkAccess chunk, ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData asyncsavedata) {
+ // Paper end - async chunk saving
ChunkPos chunkcoordintpair = chunk.getPos();
CompoundTag nbttagcompound = NbtUtils.addCurrentDataVersion(new CompoundTag());
nbttagcompound.putInt("xPos", chunkcoordintpair.x);
nbttagcompound.putInt("yPos", chunk.getMinSection());
nbttagcompound.putInt("zPos", chunkcoordintpair.z);
- nbttagcompound.putLong("LastUpdate", world.getGameTime());
+ nbttagcompound.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime() : world.getGameTime()); // Paper - async chunk saving
nbttagcompound.putLong("InhabitedTime", chunk.getInhabitedTime());
nbttagcompound.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString());
BlendingData blendingdata = chunk.getBlendingData();
@@ -424,8 +456,17 @@ public class ChunkSerializer {
nbttagcompound.putBoolean("isLightOn", true);
}
- ListTag nbttaglist1 = new ListTag();
- Iterator iterator = chunk.getBlockEntitiesPos().iterator();
+ // Paper start - async chunk saving
+ ListTag nbttaglist1;
+ Iterator<BlockPos> iterator;
+ if (asyncsavedata != null) {
+ nbttaglist1 = asyncsavedata.blockEntities();
+ iterator = java.util.Collections.emptyIterator();
+ } else {
+ nbttaglist1 = new ListTag();
+ iterator = chunk.getBlockEntitiesPos().iterator();
+ }
+ // Paper end - async chunk saving
CompoundTag nbttagcompound2;
@@ -461,7 +502,14 @@ public class ChunkSerializer {
nbttagcompound.put("CarvingMasks", nbttagcompound2);
}
+ // Paper start
+ if (asyncsavedata != null) {
+ nbttagcompound.put(BLOCK_TICKS_TAG, asyncsavedata.blockTickList());
+ nbttagcompound.put(FLUID_TICKS_TAG, asyncsavedata.fluidTickList());
+ } else {
ChunkSerializer.saveTicks(world, nbttagcompound, chunk.getTicksForSerialization());
+ }
+ // Paper end
nbttagcompound.put("PostProcessing", ChunkSerializer.packOffsets(chunk.getPostProcessing()));
CompoundTag nbttagcompound3 = new CompoundTag();
Iterator iterator1 = chunk.getHeightmaps().iterator();
@@ -481,6 +529,7 @@ public class ChunkSerializer {
nbttagcompound.put("ChunkBukkitValues", chunk.persistentDataContainer.toTagCompound());
}
// CraftBukkit end
+ ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil.saveLightHook(world, chunk, nbttagcompound); // Paper - rewrite chunk system
return nbttagcompound;
}
@@ -506,7 +555,7 @@ public class ChunkSerializer {
return nbttaglist == null && nbttaglist1 == null ? null : (chunk) -> {
if (nbttaglist != null) {
- world.addLegacyChunkEntities(EntityType.loadEntitiesRecursive(nbttaglist, world));
+ world.addLegacyChunkEntities(EntityType.loadEntitiesRecursive(nbttaglist, world), chunk.getPos()); // Paper - rewrite chunk system
}
if (nbttaglist1 != null) {
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
index f0f5e9bb5ac65250f0a151f9f90b58468335a8c2..0cdc224656a2baa09b7dfbb249b6a96320ac43e0 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java
@@ -28,21 +28,31 @@ import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.levelgen.structure.LegacyStructureDataHandler;
import net.minecraft.world.level.storage.DimensionDataStorage;
-public class ChunkStorage implements AutoCloseable {
+public class ChunkStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkStorage { // Paper - rewrite chunk system
public static final int LAST_MONOLYTH_STRUCTURE_DATA_VERSION = 1493;
- private final IOWorker worker;
+ // Paper - rewrite chunk system
protected final DataFixer fixerUpper;
@Nullable
private volatile LegacyStructureDataHandler legacyStructureHandler;
+ // Paper start - rewrite chunk system
+ private static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger();
+ private final RegionFileStorage storage;
+
+ @Override
+ public final RegionFileStorage moonrise$getRegionStorage() {
+ return this.storage;
+ }
+ // Paper end - rewrite chunk system
+
public ChunkStorage(RegionStorageInfo storageKey, Path directory, DataFixer dataFixer, boolean dsync) {
this.fixerUpper = dataFixer;
- this.worker = new IOWorker(storageKey, directory, dsync);
+ this.storage = new IOWorker(storageKey, directory, dsync).storage; // Paper - rewrite chunk system
}
public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) {
- return this.worker.isOldChunkAround(chunkPos, checkRadius);
+ return true; // Paper - rewrite chunk system
}
// CraftBukkit start
@@ -102,7 +112,9 @@ public class ChunkStorage implements AutoCloseable {
if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) {
LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier);
+ synchronized (persistentstructurelegacy) { // Paper - rewrite chunk system
nbttagcompound = persistentstructurelegacy.updateFromLegacy(nbttagcompound);
+ } // Paper - rewrite chunk system
}
}
@@ -169,7 +181,13 @@ public class ChunkStorage implements AutoCloseable {
}
public CompletableFuture<Optional<CompoundTag>> read(ChunkPos chunkPos) {
- return this.worker.loadAsync(chunkPos);
+ // Paper start - rewrite chunk system
+ try {
+ return CompletableFuture.completedFuture(Optional.ofNullable(this.storage.read(chunkPos)));
+ } catch (final Throwable throwable) {
+ return CompletableFuture.failedFuture(throwable);
+ }
+ // Paper end - rewrite chunk system
}
public CompletableFuture<Void> write(ChunkPos chunkPos, CompoundTag nbt) {
@@ -181,29 +199,54 @@ public class ChunkStorage implements AutoCloseable {
}
// Paper end - guard against serializing mismatching coordinates
this.handleLegacyStructureIndex(chunkPos);
- return this.worker.store(chunkPos, nbt);
+ // Paper start - rewrite chunk system
+ try {
+ this.storage.write(chunkPos, nbt);
+ return CompletableFuture.completedFuture(null);
+ } catch (final Throwable throwable) {
+ return CompletableFuture.failedFuture(throwable);
+ }
+ // Paper end - rewrite chunk system
}
protected void handleLegacyStructureIndex(ChunkPos chunkPos) {
if (this.legacyStructureHandler != null) {
+ synchronized (this.legacyStructureHandler) { // Paper - rewrite chunk system
this.legacyStructureHandler.removeIndex(chunkPos.toLong());
+ } // Paper - rewrite chunk system
}
}
public void flushWorker() {
- this.worker.synchronize(true).join();
+ // Paper start - rewrite chunk system
+ try {
+ this.storage.flush();
+ } catch (final IOException ex) {
+ LOGGER.error("Failed to flush chunk storage", ex);
+ }
+ // Paper end - rewrite chunk system
}
public void close() throws IOException {
- this.worker.close();
+ this.storage.close(); // Paper - rewrite chunk system
}
public ChunkScanAccess chunkScanner() {
- return this.worker;
+ // Paper start - rewrite chunk system
+ // TODO ChunkMap implementation?
+ return (chunkPos, streamTagVisitor) -> {
+ try {
+ this.storage.scanChunk(chunkPos, streamTagVisitor);
+ return java.util.concurrent.CompletableFuture.completedFuture(null);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ };
+ // Paper end - rewrite chunk system
}
- protected RegionStorageInfo storageInfo() {
- return this.worker.storageInfo();
+ public RegionStorageInfo storageInfo() { // Paper - public
+ return this.storage.info(); // Paper - rewrite chunk system
}
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java
index 36b8a9ac385e43f3212aca1b1f5bd7115bd00431..503ac0374e0c9f9993ad37bb8bd8cf1570d3615a 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java
@@ -70,12 +70,12 @@ public class EntityStorage implements EntityPersistentStorage<Entity> {
}
}
- private static ChunkPos readChunkPos(CompoundTag chunkNbt) {
+ public static ChunkPos readChunkPos(CompoundTag chunkNbt) { // Paper - public
int[] is = chunkNbt.getIntArray("Position");
return new ChunkPos(is[0], is[1]);
}
- private static void writeChunkPos(CompoundTag chunkNbt, ChunkPos pos) {
+ public static void writeChunkPos(CompoundTag chunkNbt, ChunkPos pos) { // Paper - public
chunkNbt.put("Position", new IntArrayTag(new int[]{pos.x, pos.z}));
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java b/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java
index 053504cc6c98be3b70bd1722e279d861694e015d..316bf111fe94ce7a71af71cd32c94fcf528d4365 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/IOWorker.java
@@ -32,7 +32,7 @@ public class IOWorker implements ChunkScanAccess, AutoCloseable {
private static final Logger LOGGER = LogUtils.getLogger();
private final AtomicBoolean shutdownRequested = new AtomicBoolean();
private final ProcessorMailbox<StrictQueue.IntRunnable> mailbox;
- private final RegionFileStorage storage;
+ public final RegionFileStorage storage; // Paper - public
private final Map<ChunkPos, IOWorker.PendingStore> pendingWrites = Maps.newLinkedHashMap();
private final Long2ObjectLinkedOpenHashMap<CompletableFuture<BitSet>> regionCacheForBlender = new Long2ObjectLinkedOpenHashMap<>();
private static final int REGION_CACHE_SIZE = 1024;
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
index 4c1212c6ef48594e766fa9e35a6e15916602d587..18054304e08c8a6346c0135a0e6a68e77fe5c37c 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
@@ -17,7 +17,7 @@ import net.minecraft.nbt.StreamTagVisitor;
import net.minecraft.util.ExceptionCollector;
import net.minecraft.world.level.ChunkPos;
-public final class RegionFileStorage implements AutoCloseable {
+public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage { // Paper - rewrite chunk system
public static final String ANVIL_EXTENSION = ".mca";
private static final int MAX_CACHE_SIZE = 256;
@@ -26,33 +26,122 @@ public final class RegionFileStorage implements AutoCloseable {
private final Path folder;
private final boolean sync;
- RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) {
+ // Paper start - rewrite chunk system
+ private static final int REGION_SHIFT = 5;
+ private static final int MAX_NON_EXISTING_CACHE = 1024 * 64;
+ private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet nonExistingRegionFiles = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet(MAX_NON_EXISTING_CACHE+1);
+ private static String getRegionFileName(final int chunkX, final int chunkZ) {
+ return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + ".mca";
+ }
+
+ private boolean doesRegionFilePossiblyExist(final long position) {
+ synchronized (this.nonExistingRegionFiles) {
+ if (this.nonExistingRegionFiles.contains(position)) {
+ this.nonExistingRegionFiles.addAndMoveToFirst(position);
+ return false;
+ }
+ return true;
+ }
+ }
+
+ private void createRegionFile(final long position) {
+ synchronized (this.nonExistingRegionFiles) {
+ this.nonExistingRegionFiles.remove(position);
+ }
+ }
+
+ private void markNonExisting(final long position) {
+ synchronized (this.nonExistingRegionFiles) {
+ if (this.nonExistingRegionFiles.addAndMoveToFirst(position)) {
+ while (this.nonExistingRegionFiles.size() >= MAX_NON_EXISTING_CACHE) {
+ this.nonExistingRegionFiles.removeLastLong();
+ }
+ }
+ }
+ }
+
+ @Override
+ public final boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ) {
+ return !this.doesRegionFilePossiblyExist(ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT));
+ }
+
+ @Override
+ public synchronized final RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) {
+ return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT));
+ }
+
+ @Override
+ public synchronized final RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException {
+ final long key = ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
+
+ RegionFile ret = this.regionCache.getAndMoveToFirst(key);
+ if (ret != null) {
+ return ret;
+ }
+
+ if (!this.doesRegionFilePossiblyExist(key)) {
+ return null;
+ }
+
+ if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper
+ this.regionCache.removeLast().close();
+ }
+
+ final Path regionPath = this.folder.resolve(getRegionFileName(chunkX, chunkZ));
+
+ if (!java.nio.file.Files.exists(regionPath)) {
+ this.markNonExisting(key);
+ return null;
+ }
+
+ this.createRegionFile(key);
+
+ FileUtil.createDirectoriesSafe(this.folder);
+
+ ret = new RegionFile(this.info, regionPath, this.folder, this.sync);
+
+ this.regionCache.putAndMoveToFirst(key, ret);
+
+ return ret;
+ }
+ // Paper end - rewrite chunk system
+
+ protected RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { // Paper - protected
this.folder = directory;
this.sync = dsync;
this.info = storageKey;
}
- private RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit
- long i = ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ());
- RegionFile regionfile = (RegionFile) this.regionCache.getAndMoveToFirst(i);
+ public RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit // Paper - public
+ // Paper start - rewrite chunk system
+ if (existingOnly) {
+ return this.moonrise$getRegionFileIfExists(chunkcoordintpair.x, chunkcoordintpair.z);
+ }
+ synchronized (this) {
+ final long key = ChunkPos.asLong(chunkcoordintpair.x >> REGION_SHIFT, chunkcoordintpair.z >> REGION_SHIFT);
- if (regionfile != null) {
- return regionfile;
- } else {
- if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - Sanitise RegionFileCache and make configurable
- ((RegionFile) this.regionCache.removeLast()).close();
+ RegionFile ret = this.regionCache.getAndMoveToFirst(key);
+ if (ret != null) {
+ return ret;
+ }
+
+ if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper
+ this.regionCache.removeLast().close();
}
+ final Path regionPath = this.folder.resolve(getRegionFileName(chunkcoordintpair.x, chunkcoordintpair.z));
+
+ this.createRegionFile(key);
+
FileUtil.createDirectoriesSafe(this.folder);
- Path path = this.folder;
- int j = chunkcoordintpair.getRegionX();
- Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca");
- if (existingOnly && !java.nio.file.Files.exists(path1)) return null; // CraftBukkit
- RegionFile regionfile1 = new RegionFile(this.info, path1, this.folder, this.sync);
- this.regionCache.putAndMoveToFirst(i, regionfile1);
- return regionfile1;
+ ret = new RegionFile(this.info, regionPath, this.folder, this.sync);
+
+ this.regionCache.putAndMoveToFirst(key, ret);
+
+ return ret;
}
+ // Paper end - rewrite chunk system
}
@Nullable
@@ -132,8 +221,14 @@ public final class RegionFileStorage implements AutoCloseable {
}
- protected void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException {
- RegionFile regionfile = this.getRegionFile(pos, false); // CraftBukkit
+ public void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException { // Paper - public
+ RegionFile regionfile = this.getRegionFile(pos, nbt == null); // CraftBukkit // Paper - rewrite chunk system
+ // Paper start - rewrite chunk system
+ if (regionfile == null) {
+ // if the RegionFile doesn't exist, no point in deleting from it
+ return;
+ }
+ // Paper end - rewrite chunk system
// Paper start - Chunk save reattempt
int attempts = 0;
Exception lastException = null;
@@ -182,30 +277,37 @@ public final class RegionFileStorage implements AutoCloseable {
}
public void close() throws IOException {
- ExceptionCollector<IOException> exceptionsuppressor = new ExceptionCollector<>();
- ObjectIterator objectiterator = this.regionCache.values().iterator();
-
- while (objectiterator.hasNext()) {
- RegionFile regionfile = (RegionFile) objectiterator.next();
-
- try {
- regionfile.close();
- } catch (IOException ioexception) {
- exceptionsuppressor.add(ioexception);
+ // Paper start - rewrite chunk system
+ synchronized (this) {
+ final ExceptionCollector<IOException> exceptionCollector = new ExceptionCollector<>();
+ for (final RegionFile regionFile : this.regionCache.values()) {
+ try {
+ regionFile.close();
+ } catch (final IOException ex) {
+ exceptionCollector.add(ex);
+ }
}
- }
- exceptionsuppressor.throwIfPresent();
+ exceptionCollector.throwIfPresent();
+ }
+ // Paper end - rewrite chunk system
}
public void flush() throws IOException {
- ObjectIterator objectiterator = this.regionCache.values().iterator();
-
- while (objectiterator.hasNext()) {
- RegionFile regionfile = (RegionFile) objectiterator.next();
+ // Paper start - rewrite chunk system
+ synchronized (this) {
+ final ExceptionCollector<IOException> exceptionCollector = new ExceptionCollector<>();
+ for (final RegionFile regionFile : this.regionCache.values()) {
+ try {
+ regionFile.flush();
+ } catch (final IOException ex) {
+ exceptionCollector.add(ex);
+ }
+ }
- regionfile.flush();
+ exceptionCollector.throwIfPresent();
}
+ // Paper end - rewrite chunk system
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
index 092773bd39d77a0dbe22db97c11aecb4a297111c..c7ed3eb80f6e8b918434153093644776866aa220 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
@@ -31,10 +31,10 @@ import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import org.slf4j.Logger;
-public class SectionStorage<R> implements AutoCloseable {
+public abstract class SectionStorage<R> implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage { // Paper - rewrite chunk system
private static final Logger LOGGER = LogUtils.getLogger();
private static final String SECTIONS_TAG = "Sections";
- private final SimpleRegionStorage simpleRegionStorage;
+ // Paper - rewrite chunk system
private final Long2ObjectMap<Optional<R>> storage = new Long2ObjectOpenHashMap<>();
private final LongLinkedOpenHashSet dirty = new LongLinkedOpenHashSet();
private final Function<Runnable, Codec<R>> codec;
@@ -43,6 +43,15 @@ public class SectionStorage<R> implements AutoCloseable {
private final ChunkIOErrorReporter errorReporter;
protected final LevelHeightAccessor levelHeightAccessor;
+ // Paper start - rewrite chunk system
+ private final RegionFileStorage regionStorage;
+
+ @Override
+ public final RegionFileStorage moonrise$getRegionStorage() {
+ return this.regionStorage;
+ }
+ // Paper end - rewrite chunk system
+
public SectionStorage(
SimpleRegionStorage storageAccess,
Function<Runnable, Codec<R>> codecFactory,
@@ -51,12 +60,13 @@ public class SectionStorage<R> implements AutoCloseable {
ChunkIOErrorReporter errorHandler,
LevelHeightAccessor world
) {
- this.simpleRegionStorage = storageAccess;
+ // Paper - rewrite chunk system
this.codec = codecFactory;
this.factory = factory;
this.registryAccess = registryManager;
this.errorReporter = errorHandler;
this.levelHeightAccessor = world;
+ this.regionStorage = storageAccess.worker.storage; // Paper - rewrite chunk system
}
protected void tick(BooleanSupplier shouldKeepTicking) {
@@ -121,44 +131,17 @@ public class SectionStorage<R> implements AutoCloseable {
}
private CompletableFuture<Optional<CompoundTag>> tryRead(ChunkPos pos) {
- return this.simpleRegionStorage.read(pos).exceptionally(throwable -> {
- if (throwable instanceof IOException iOException) {
- LOGGER.error("Error reading chunk {} data from disk", pos, iOException);
- this.errorReporter.reportChunkLoadFailure(iOException, this.simpleRegionStorage.storageInfo(), pos);
- return Optional.empty();
- } else {
- throw new CompletionException(throwable);
- }
- });
+ // Paper start - rewrite chunk system
+ try {
+ return CompletableFuture.completedFuture(Optional.ofNullable(this.moonrise$read(pos.x, pos.z)));
+ } catch (final Throwable thr) {
+ return CompletableFuture.failedFuture(thr);
+ }
+ // Paper end - rewrite chunk system
}
private void readColumn(ChunkPos pos, RegistryOps<Tag> ops, @Nullable CompoundTag nbt) {
- if (nbt == null) {
- for (int i = this.levelHeightAccessor.getMinSection(); i < this.levelHeightAccessor.getMaxSection(); i++) {
- this.storage.put(getKey(pos, i), Optional.empty());
- }
- } else {
- Dynamic<Tag> dynamic = new Dynamic<>(ops, nbt);
- int j = getVersion(dynamic);
- int k = SharedConstants.getCurrentVersion().getDataVersion().getVersion();
- boolean bl = j != k;
- Dynamic<Tag> dynamic2 = this.simpleRegionStorage.upgradeChunkTag(dynamic, j);
- OptionalDynamic<Tag> optionalDynamic = dynamic2.get("Sections");
-
- for (int l = this.levelHeightAccessor.getMinSection(); l < this.levelHeightAccessor.getMaxSection(); l++) {
- long m = getKey(pos, l);
- Optional<R> optional = optionalDynamic.get(Integer.toString(l))
- .result()
- .flatMap(dynamicx -> this.codec.apply(() -> this.setDirty(m)).parse(dynamicx).resultOrPartial(LOGGER::error));
- this.storage.put(m, optional);
- optional.ifPresent(sections -> {
- this.onSectionLoad(m);
- if (bl) {
- this.setDirty(m);
- }
- });
- }
- }
+ throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system
}
private void writeColumn(ChunkPos pos) {
@@ -166,10 +149,13 @@ public class SectionStorage<R> implements AutoCloseable {
Dynamic<Tag> dynamic = this.writeColumn(pos, registryOps);
Tag tag = dynamic.getValue();
if (tag instanceof CompoundTag) {
- this.simpleRegionStorage.write(pos, (CompoundTag)tag).exceptionally(throwable -> {
- this.errorReporter.reportChunkSaveFailure(throwable, this.simpleRegionStorage.storageInfo(), pos);
- return null;
- });
+ // Paper start - rewrite chunk system
+ try {
+ this.moonrise$write(pos.x, pos.z, (net.minecraft.nbt.CompoundTag)tag);
+ } catch (final IOException ex) {
+ LOGGER.error("Error writing poi chunk data to disk for chunk " + pos, ex);
+ }
+ // Paper end - rewrite chunk system
} else {
LOGGER.error("Expected compound tag, got {}", tag);
}
@@ -209,7 +195,7 @@ public class SectionStorage<R> implements AutoCloseable {
protected void onSectionLoad(long pos) {
}
- protected void setDirty(long pos) {
+ public void setDirty(long pos) { // Paper - public
Optional<R> optional = this.storage.get(pos);
if (optional != null && !optional.isEmpty()) {
this.dirty.add(pos);
@@ -236,6 +222,6 @@ public class SectionStorage<R> implements AutoCloseable {
@Override
public void close() throws IOException {
- this.simpleRegionStorage.close();
+ this.moonrise$close(); // Paper - rewrite chunk system
}
}
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
index e0e843f4f69013379ed70cb63d9b4f72163b828b..aafb05c5e63903f5790a6bcb862c8d79588be5a6 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
@@ -14,7 +14,7 @@ import net.minecraft.util.datafix.DataFixTypes;
import net.minecraft.world.level.ChunkPos;
public class SimpleRegionStorage implements AutoCloseable {
- private final IOWorker worker;
+ public final IOWorker worker; // Paper - public
private final DataFixer fixerUpper;
private final DataFixTypes dataFixType;
diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
index 74a285b8b018a9c94ccea519f1ce8b9e2ef3cb64..d8b4196adf955f8d414688dc451caac2d9c609d9 100644
--- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
+++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java
@@ -9,52 +9,38 @@ import javax.annotation.Nullable;
import net.minecraft.world.entity.Entity;
public class EntityTickList {
- private Int2ObjectMap<Entity> active = new Int2ObjectLinkedOpenHashMap<>();
- private Int2ObjectMap<Entity> passive = new Int2ObjectLinkedOpenHashMap<>();
- @Nullable
- private Int2ObjectMap<Entity> iterated;
+ private final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<net.minecraft.world.entity.Entity> entities = new ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<>(); // Paper - rewrite chunk system
private void ensureActiveIsNotIterated() {
- if (this.iterated == this.active) {
- this.passive.clear();
-
- for (Entry<Entity> entry : Int2ObjectMaps.fastIterable(this.active)) {
- this.passive.put(entry.getIntKey(), entry.getValue());
- }
-
- Int2ObjectMap<Entity> int2ObjectMap = this.active;
- this.active = this.passive;
- this.passive = int2ObjectMap;
- }
+ // Paper - rewrite chunk system
}
public void add(Entity entity) {
this.ensureActiveIsNotIterated();
- this.active.put(entity.getId(), entity);
+ this.entities.add(entity); // Paper - rewrite chunk system
}
public void remove(Entity entity) {
this.ensureActiveIsNotIterated();
- this.active.remove(entity.getId());
+ this.entities.remove(entity); // Paper - rewrite chunk system
}
public boolean contains(Entity entity) {
- return this.active.containsKey(entity.getId());
+ return this.entities.contains(entity); // Paper - rewrite chunk system
}
public void forEach(Consumer<Entity> action) {
- if (this.iterated != null) {
- throw new UnsupportedOperationException("Only one concurrent iteration supported");
- } else {
- this.iterated = this.active;
-
- try {
- for (Entity entity : this.active.values()) {
- action.accept(entity);
- }
- } finally {
- this.iterated = null;
+ // Paper start - rewrite chunk system
+ // To ensure nothing weird happens with dimension travelling, do not iterate over new entries...
+ // (by dfl iterator() is configured to not iterate over new entries)
+ final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet.Iterator<Entity> iterator = this.entities.iterator();
+ try {
+ while (iterator.hasNext()) {
+ action.accept(iterator.next());
}
+ } finally {
+ iterator.finishedIterating();
}
+ // Paper end - rewrite chunk system
}
}
diff --git a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
index fb0be805c86e311927f55e8f090592465195384e..996899cb18e6c29665b9de7a1cc97c9a4187924b 100644
--- a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
+++ b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
@@ -86,7 +86,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("init_biomes", () -> {
this.doCreateBiomes(blender, noiseConfig, structureAccessor, chunk);
return chunk;
- }), Util.backgroundExecutor());
+ }), Runnable::run); // Paper - rewrite chunk system
}
private void doCreateBiomes(Blender blender, RandomState noiseConfig, StructureManager structureAccessor, ChunkAccess chunk) {
@@ -311,7 +311,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator {
}
return ichunkaccess1;
- }), Util.backgroundExecutor());
+ }), Runnable::run); // Paper - rewrite chunk system
}
private ChunkAccess doFill(Blender blender, StructureManager structureAccessor, RandomState noiseConfig, ChunkAccess chunk, int minimumCellY, int cellHeight) {
diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
index c6181e14d85d454506534f9bbe856156c0d4a062..3694c5d2d522216cd2e6e91e502a56a08595ca84 100644
--- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
@@ -47,8 +47,13 @@ public class StructureCheck {
private final BiomeSource biomeSource;
private final long seed;
private final DataFixer fixerUpper;
- private final Long2ObjectMap<Object2IntMap<Structure>> loadedChunks = new Long2ObjectOpenHashMap<>();
- private final Map<Structure, Long2BooleanMap> featureChecks = new HashMap<>();
+ // Paper start - rewrite chunk system
+ // make sure to purge entries from the maps to prevent memory leaks
+ private static final int CHUNK_TOTAL_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups
+ private static final int PER_FEATURE_CHECK_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups
+ private final ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap<it.unimi.dsi.fastutil.objects.Object2IntMap<Structure>> loadedChunksSafe = new ca.spottedleaf.moonrise.common.map.SynchronisedLong2ObjectMap<>(CHUNK_TOTAL_LIMIT);
+ private final java.util.concurrent.ConcurrentHashMap<Structure, ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap> featureChecksSafe = new java.util.concurrent.ConcurrentHashMap<>();
+ // Paper end - rewrite chunk system
public StructureCheck(
ChunkScanAccess chunkIoWorker,
@@ -90,7 +95,7 @@ public class StructureCheck {
public StructureCheckResult checkStart(ChunkPos pos, Structure type, StructurePlacement placement, boolean skipReferencedStructures) {
long l = pos.toLong();
- Object2IntMap<Structure> object2IntMap = this.loadedChunks.get(l);
+ Object2IntMap<Structure> object2IntMap = this.loadedChunksSafe.get(l); // Paper - rewrite chunk system
if (object2IntMap != null) {
return this.checkStructureInfo(object2IntMap, type, skipReferencedStructures);
} else {
@@ -100,9 +105,11 @@ public class StructureCheck {
} else if (!placement.applyAdditionalChunkRestrictions(pos.x, pos.z, this.seed, this.getSaltOverride(type))) { // Paper - add missing structure seed configs
return StructureCheckResult.START_NOT_PRESENT;
} else {
- boolean bl = this.featureChecks
- .computeIfAbsent(type, structure2 -> new Long2BooleanOpenHashMap())
- .computeIfAbsent(l, chunkPos -> this.canCreateStructure(pos, type));
+ // Paper start - rewrite chunk system
+ boolean bl = this.featureChecksSafe
+ .computeIfAbsent(type, structure2 -> new ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap(PER_FEATURE_CHECK_LIMIT))
+ .getOrCompute(l, chunkPos -> this.canCreateStructure(pos, type));
+ // Paper end - rewrite chunk system
return !bl ? StructureCheckResult.START_NOT_PRESENT : StructureCheckResult.CHUNK_LOAD_NEEDED;
}
}
@@ -228,15 +235,25 @@ public class StructureCheck {
}
private void storeFullResults(long pos, Object2IntMap<Structure> referencesByStructure) {
- this.loadedChunks.put(pos, deduplicateEmptyMap(referencesByStructure));
- this.featureChecks.values().forEach(generationPossibilityByChunkPos -> generationPossibilityByChunkPos.remove(pos));
+ // Paper start - rewrite chunk system
+ this.loadedChunksSafe.put(pos, deduplicateEmptyMap(referencesByStructure));
+ // once we insert into loadedChunks, we don't really need to be very careful about removing everything
+ // from this map, as everything that checks this map uses loadedChunks first
+ // so, one way or another it's a race condition that doesn't matter
+ for (ca.spottedleaf.moonrise.common.map.SynchronisedLong2BooleanMap value : this.featureChecksSafe.values()) {
+ value.remove(pos);
+ }
+ // Paper end - rewrite chunk system
}
public void incrementReference(ChunkPos pos, Structure structure) {
- this.loadedChunks.compute(pos.toLong(), (posx, referencesByStructure) -> {
- if (referencesByStructure == null || referencesByStructure.isEmpty()) {
+ this.loadedChunksSafe.compute(pos.toLong(), (posx, referencesByStructure) -> { // Paper start - rewrite chunk system
+ if (referencesByStructure == null) {
referencesByStructure = new Object2IntOpenHashMap<>();
+ } else {
+ referencesByStructure = referencesByStructure instanceof Object2IntOpenHashMap<Structure> fastClone ? fastClone.clone() : new Object2IntOpenHashMap<>(referencesByStructure);
}
+ // Paper end - rewrite chunk system
referencesByStructure.computeInt(structure, (feature, references) -> references == null ? 1 : references + 1);
return referencesByStructure;
diff --git a/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java b/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java
index 82e4fad11121167445df97060fb717fa86191297..b3e2bb9245be1bb2f587117b0f6016cba18e217f 100644
--- a/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java
+++ b/src/main/java/net/minecraft/world/level/lighting/LevelLightEngine.java
@@ -9,145 +9,103 @@ import net.minecraft.world.level.LightLayer;
import net.minecraft.world.level.chunk.DataLayer;
import net.minecraft.world.level.chunk.LightChunkGetter;
-public class LevelLightEngine implements LightEventListener {
+public class LevelLightEngine implements LightEventListener, ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider {
public static final int LIGHT_SECTION_PADDING = 1;
protected final LevelHeightAccessor levelHeightAccessor;
- @Nullable
- private final LightEngine<?, ?> blockEngine;
- @Nullable
- private final LightEngine<?, ?> skyEngine;
+ // Paper start - rewrite chunk system
+ protected final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface lightEngine;
+
+ @Override
+ public final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface starlight$getLightEngine() {
+ return this.lightEngine;
+ }
+
+ @Override
+ public void starlight$clientUpdateLight(final LightLayer lightType, final SectionPos pos,
+ final DataLayer nibble, final boolean trustEdges) {
+ throw new IllegalStateException("This hook is for the CLIENT ONLY"); // Paper - not implemented on server
+ }
+
+ @Override
+ public void starlight$clientRemoveLightData(final ChunkPos chunkPos) {
+ throw new IllegalStateException("This hook is for the CLIENT ONLY"); // Paper - not implemented on server
+ }
+
+ @Override
+ public void starlight$clientChunkLoad(final ChunkPos pos, final net.minecraft.world.level.chunk.LevelChunk chunk) {
+ throw new IllegalStateException("This hook is for the CLIENT ONLY"); // Paper - not implemented on server
+ }
+ // Paper end - rewrite chunk system
public LevelLightEngine(LightChunkGetter chunkProvider, boolean hasBlockLight, boolean hasSkyLight) {
this.levelHeightAccessor = chunkProvider.getLevel();
- this.blockEngine = hasBlockLight ? new BlockLightEngine(chunkProvider) : null;
- this.skyEngine = hasSkyLight ? new SkyLightEngine(chunkProvider) : null;
+ // Paper start - rewrite chunk system
+ if (chunkProvider.getLevel() instanceof net.minecraft.world.level.Level) {
+ this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(chunkProvider, hasSkyLight, hasBlockLight, (LevelLightEngine)(Object)this);
+ } else {
+ this.lightEngine = new ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface(null, hasSkyLight, hasBlockLight, (LevelLightEngine)(Object)this);
+ }
+ // Paper end - rewrite chunk system
}
@Override
public void checkBlock(BlockPos pos) {
- if (this.blockEngine != null) {
- this.blockEngine.checkBlock(pos);
- }
-
- if (this.skyEngine != null) {
- this.skyEngine.checkBlock(pos);
- }
+ this.lightEngine.blockChange(pos.immutable()); // Paper - rewrite chunk system
}
@Override
public boolean hasLightWork() {
- return this.skyEngine != null && this.skyEngine.hasLightWork() || this.blockEngine != null && this.blockEngine.hasLightWork();
+ return this.lightEngine.hasUpdates(); // Paper - rewrite chunk system
}
@Override
public int runLightUpdates() {
- int i = 0;
- if (this.blockEngine != null) {
- i += this.blockEngine.runLightUpdates();
- }
-
- if (this.skyEngine != null) {
- i += this.skyEngine.runLightUpdates();
- }
-
- return i;
+ final boolean hadUpdates = this.hasLightWork();
+ this.lightEngine.propagateChanges();
+ return hadUpdates ? 1 : 0; // Paper - rewrite chunk system
}
@Override
public void updateSectionStatus(SectionPos pos, boolean notReady) {
- if (this.blockEngine != null) {
- this.blockEngine.updateSectionStatus(pos, notReady);
- }
-
- if (this.skyEngine != null) {
- this.skyEngine.updateSectionStatus(pos, notReady);
- }
+ this.lightEngine.sectionChange(pos, notReady); // Paper - rewrite chunk system
}
@Override
public void setLightEnabled(ChunkPos pos, boolean retainData) {
- if (this.blockEngine != null) {
- this.blockEngine.setLightEnabled(pos, retainData);
- }
-
- if (this.skyEngine != null) {
- this.skyEngine.setLightEnabled(pos, retainData);
- }
+ // Paper - rewrite chunk system
}
@Override
public void propagateLightSources(ChunkPos chunkPos) {
- if (this.blockEngine != null) {
- this.blockEngine.propagateLightSources(chunkPos);
- }
-
- if (this.skyEngine != null) {
- this.skyEngine.propagateLightSources(chunkPos);
- }
+ // Paper - rewrite chunk system
}
public LayerLightEventListener getLayerListener(LightLayer lightType) {
- if (lightType == LightLayer.BLOCK) {
- return (LayerLightEventListener)(this.blockEngine == null ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : this.blockEngine);
- } else {
- return (LayerLightEventListener)(this.skyEngine == null ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : this.skyEngine);
- }
+ return lightType == LightLayer.BLOCK ? this.lightEngine.getBlockReader() : this.lightEngine.getSkyReader(); // Paper - rewrite chunk system
}
public String getDebugData(LightLayer lightType, SectionPos pos) {
- if (lightType == LightLayer.BLOCK) {
- if (this.blockEngine != null) {
- return this.blockEngine.getDebugData(pos.asLong());
- }
- } else if (this.skyEngine != null) {
- return this.skyEngine.getDebugData(pos.asLong());
- }
-
- return "n/a";
+ return "n/a"; // Paper - rewrite chunk system
}
public LayerLightSectionStorage.SectionType getDebugSectionType(LightLayer lightType, SectionPos pos) {
- if (lightType == LightLayer.BLOCK) {
- if (this.blockEngine != null) {
- return this.blockEngine.getDebugSectionType(pos.asLong());
- }
- } else if (this.skyEngine != null) {
- return this.skyEngine.getDebugSectionType(pos.asLong());
- }
-
- return LayerLightSectionStorage.SectionType.EMPTY;
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system
}
public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) {
- if (lightType == LightLayer.BLOCK) {
- if (this.blockEngine != null) {
- this.blockEngine.queueSectionData(pos.asLong(), nibbles);
- }
- } else if (this.skyEngine != null) {
- this.skyEngine.queueSectionData(pos.asLong(), nibbles);
- }
+ // Paper - rewrite chunk system
}
public void retainData(ChunkPos pos, boolean retainData) {
- if (this.blockEngine != null) {
- this.blockEngine.retainData(pos, retainData);
- }
-
- if (this.skyEngine != null) {
- this.skyEngine.retainData(pos, retainData);
- }
+ // Paper - rewrite chunk system
}
public int getRawBrightness(BlockPos pos, int ambientDarkness) {
- int i = this.skyEngine == null ? 0 : this.skyEngine.getLightValue(pos) - ambientDarkness;
- int j = this.blockEngine == null ? 0 : this.blockEngine.getLightValue(pos);
- return Math.max(j, i);
+ return this.lightEngine.getRawBrightness(pos, ambientDarkness); // Paper - rewrite chunk system
}
public boolean lightOnInSection(SectionPos sectionPos) {
- long l = sectionPos.asLong();
- return this.blockEngine == null
- || this.blockEngine.storage.lightOnInSection(l) && (this.skyEngine == null || this.skyEngine.storage.lightOnInSection(l));
+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system // Paper - not implemented on server
}
public int getLightSectionCount() {
diff --git a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java
index 47c2b2da9799690291396effb9e1b06d71efc6fd..c42c0d1e4da30aa15f32d4ca524aeabd26fc50cf 100644
--- a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java
+++ b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java
@@ -18,7 +18,7 @@ import net.minecraft.core.BlockPos;
import net.minecraft.nbt.ListTag;
import net.minecraft.world.level.ChunkPos;
-public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickContainerAccess<T> {
+public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickContainerAccess<T>, ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks { // Paper - rewrite chunk system
private final Queue<ScheduledTick<T>> tickQueue = new PriorityQueue<>(ScheduledTick.DRAIN_ORDER);
@Nullable
private List<SavedTick<T>> pendingTicks;
@@ -26,6 +26,30 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
@Nullable
private BiConsumer<LevelChunkTicks<T>, ScheduledTick<T>> onTickAdded;
+ // Paper start - rewrite chunk system
+ /*
+ * Since ticks are saved using relative delays, we need to consider the entire tick list dirty when there are scheduled ticks
+ * and the last saved tick is not equal to the current tick
+ */
+ /*
+ * In general, it would be nice to be able to "re-pack" ticks once the chunk becomes non-ticking again, but that is a
+ * bit out of scope for the chunk system
+ */
+
+ private boolean dirty;
+ private long lastSaved = Long.MIN_VALUE;
+
+ @Override
+ public final boolean moonrise$isDirty(final long tick) {
+ return this.dirty || (!this.tickQueue.isEmpty() && tick != this.lastSaved);
+ }
+
+ @Override
+ public final void moonrise$clearDirty() {
+ this.dirty = false;
+ }
+ // Paper end - rewrite chunk system
+
public LevelChunkTicks() {
}
@@ -50,7 +74,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
public ScheduledTick<T> poll() {
ScheduledTick<T> scheduledTick = this.tickQueue.poll();
if (scheduledTick != null) {
- this.ticksPerPosition.remove(scheduledTick);
+ this.ticksPerPosition.remove(scheduledTick); this.dirty = true; // Paper - rewrite chunk system
}
return scheduledTick;
@@ -59,7 +83,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
@Override
public void schedule(ScheduledTick<T> orderedTick) {
if (this.ticksPerPosition.add(orderedTick)) {
- this.scheduleUnchecked(orderedTick);
+ this.scheduleUnchecked(orderedTick); this.dirty = true; // Paper - rewrite chunk system
}
}
@@ -81,7 +105,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
while (iterator.hasNext()) {
ScheduledTick<T> scheduledTick = iterator.next();
if (predicate.test(scheduledTick)) {
- iterator.remove();
+ iterator.remove(); this.dirty = true; // Paper - rewrite chunk system
this.ticksPerPosition.remove(scheduledTick);
}
}
@@ -98,6 +122,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
@Override
public ListTag save(long l, Function<T, String> function) {
+ this.lastSaved = l; // Paper - rewrite chunk system
ListTag listTag = new ListTag();
if (this.pendingTicks != null) {
for (SavedTick<T> savedTick : this.pendingTicks) {
@@ -114,6 +139,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon
public void unpack(long time) {
if (this.pendingTicks != null) {
+ this.lastSaved = time; // Paper - rewrite chunk system
int i = -this.pendingTicks.size();
for (SavedTick<T> savedTick : this.pendingTicks) {
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
index 69c7fe5bf5b914276a9f7a0e57ce668e569d91f9..33322b57b4c6922f4daad0f584733f0f24083911 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
@@ -82,6 +82,12 @@ public class CraftChunk implements Chunk {
}
public ChunkAccess getHandle(ChunkStatus chunkStatus) {
+ // Paper start - rewrite chunk system
+ net.minecraft.world.level.chunk.LevelChunk full = this.worldServer.getChunkIfLoaded(this.x, this.z);
+ if (full != null) {
+ return full;
+ }
+ // Paper end - rewrite chunk system
ChunkAccess chunkAccess = this.worldServer.getChunk(this.x, this.z, chunkStatus);
// SPIGOT-7332: Get unwrapped extension
@@ -116,60 +122,12 @@ public class CraftChunk implements Chunk {
@Override
public boolean isEntitiesLoaded() {
- return this.getCraftWorld().getHandle().entityManager.areEntitiesLoaded(ChunkPos.asLong(this.x, this.z));
+ return this.getCraftWorld().getHandle().areEntitiesLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(this.x, this.z)); // Paper - rewrite chunk system
}
@Override
public Entity[] getEntities() {
- if (!this.isLoaded()) {
- this.getWorld().getChunkAt(this.x, this.z); // Transient load for this tick
- }
-
- PersistentEntitySectionManager<net.minecraft.world.entity.Entity> entityManager = this.getCraftWorld().getHandle().entityManager;
- long pair = ChunkPos.asLong(this.x, this.z);
-
- if (entityManager.areEntitiesLoaded(pair)) {
- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream()
- .map(net.minecraft.world.entity.Entity::getBukkitEntity)
- .filter(Objects::nonNull).toArray(Entity[]::new);
- }
-
- entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading
-
- // SPIGOT-6772: Use entity mailbox and re-schedule entities if they get unloaded
- ProcessorMailbox<Runnable> mailbox = ((EntityStorage) entityManager.permanentStorage).entityDeserializerQueue;
- BooleanSupplier supplier = () -> {
- // only execute inbox if our entities are not present
- if (entityManager.areEntitiesLoaded(pair)) {
- return true;
- }
-
- if (!entityManager.isPending(pair)) {
- // Our entities got unloaded, this should normally not happen.
- entityManager.ensureChunkQueuedForLoad(pair); // Re-start entity loading
- }
-
- // tick loading inbox, which loads the created entities to the world
- // (if present)
- entityManager.tick();
- // check if our entities are loaded
- return entityManager.areEntitiesLoaded(pair);
- };
-
- // now we wait until the entities are loaded,
- // the converting from NBT to entity object is done on the main Thread which is why we wait
- while (!supplier.getAsBoolean()) {
- if (mailbox.size() != 0) {
- mailbox.run();
- } else {
- Thread.yield();
- LockSupport.parkNanos("waiting for entity loading", 100000L);
- }
- }
-
- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream()
- .map(net.minecraft.world.entity.Entity::getBukkitEntity)
- .filter(Objects::nonNull).toArray(Entity[]::new);
+ return this.getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - rewrite chunk system
}
@Override
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index 77f3ac4e45a691181a94831cf49f7840c9f88e3a..05e44a1448f30ceb8cecba2bed76f51aac5543f9 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -1419,7 +1419,7 @@ public final class CraftServer implements Server {
// Paper - Put world into worldlist before initing the world; move up
this.getServer().prepareLevels(internal.getChunkSource().chunkMap.progressListener, internal);
- internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API
+ // Paper - rewrite chunk system
this.pluginManager.callEvent(new WorldLoadEvent(internal.getWorld()));
return internal.getWorld();
@@ -1464,7 +1464,7 @@ public final class CraftServer implements Server {
}
handle.getChunkSource().close(save);
- handle.entityManager.close(save); // SPIGOT-6722: close entityManager
+ // Paper - rewrite chunk system
handle.convertable.close();
} catch (Exception ex) {
this.getLogger().log(Level.SEVERE, null, ex);
@@ -2500,7 +2500,7 @@ public final class CraftServer implements Server {
@Override
public boolean isPrimaryThread() {
- return Thread.currentThread().equals(this.console.serverThread) || this.console.hasStopped() || !org.spigotmc.AsyncCatcher.enabled; // All bets are off if we have shut down (e.g. due to watchdog)
+ return io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system
}
// Paper start - Adventure
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index 8f88ccec6b8947ca2738dc07c23aebe258145c83..cdc704364cf339084537d089e654f6078f8be783 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -456,10 +456,14 @@ public class CraftWorld extends CraftRegionAccessor implements World {
ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
if (playerChunk == null) return false;
- playerChunk.getTickingChunkFuture().thenAccept(either -> {
- either.ifSuccess(chunk -> {
+ // Paper start - chunk system
+ net.minecraft.world.level.chunk.LevelChunk chunk = playerChunk.getChunkToSend();
+ if (chunk == null) {
+ return false;
+ }
+ // Paper end - chunk system
List<ServerPlayer> playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false);
- if (playersInRange.isEmpty()) return;
+ if (playersInRange.isEmpty()) return true; // Paper - chunk system
ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, this.world.getLightEngine(), null, null);
for (ServerPlayer player : playersInRange) {
@@ -467,8 +471,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
player.connection.send(refreshPacket);
}
- });
- });
+ // Paper - chunk system
return true;
}
@@ -572,20 +575,8 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public Collection<Plugin> getPluginChunkTickets(int x, int z) {
DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
- SortedArraySet<Ticket<?>> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z));
- if (tickets == null) {
- return Collections.emptyList();
- }
-
- ImmutableList.Builder<Plugin> ret = ImmutableList.builder();
- for (Ticket<?> ticket : tickets) {
- if (ticket.getType() == TicketType.PLUGIN_TICKET) {
- ret.add((Plugin) ticket.key);
- }
- }
-
- return ret.build();
+ return chunkDistanceManager.getChunkHolderManager().getPluginChunkTickets(x, z); // Paper - rewrite chunk system
}
@Override
@@ -593,7 +584,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
Map<Plugin, ImmutableList.Builder<Chunk>> ret = new HashMap<>();
DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
- for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) {
+ for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system
long chunkKey = chunkTickets.getLongKey();
SortedArraySet<Ticket<?>> tickets = chunkTickets.getValue();
@@ -1290,12 +1281,12 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public int getViewDistance() {
- return this.world.getChunkSource().chunkMap.serverViewDistance;
+ return this.getHandle().moonrise$getPlayerChunkLoader().getAPIViewDistance(); // Paper - rewrite chunk system
}
@Override
public int getSimulationDistance() {
- return this.world.getChunkSource().chunkMap.getDistanceManager().simulationDistance;
+ return this.getHandle().moonrise$getPlayerChunkLoader().getAPITickDistance(); // Paper - rewrite chunk system
}
public BlockMetadataStore getBlockMetadata() {
@@ -2433,17 +2424,20 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public void setSimulationDistance(final int simulationDistance) {
- throw new UnsupportedOperationException("Not implemented yet");
+ if (simulationDistance < 2 || simulationDistance > 32) {
+ throw new IllegalArgumentException("Simulation distance " + simulationDistance + " is out of range of [2, 32]");
+ }
+ this.getHandle().chunkSource.setSimulationDistance(simulationDistance); // Paper - rewrite chunk system
}
@Override
public int getSendViewDistance() {
- return this.getViewDistance();
+ return this.getHandle().moonrise$getPlayerChunkLoader().getAPISendViewDistance(); // Paper - rewrite chunk system
}
@Override
public void setSendViewDistance(final int viewDistance) {
- throw new UnsupportedOperationException("Not implemented yet");
+ this.getHandle().chunkSource.setSendViewDistance(viewDistance); // Paper - rewrite chunk system
}
// Paper start - implement pointers
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
index 3480f06d4476c5c200246e6200e2eda2a5de1a5a..62bfde27a182be24710eb3b72448e82da58afc8f 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -3492,12 +3492,14 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@Override
public int getViewDistance() {
- return io.papermc.paper.chunk.system.ChunkSystem.getLoadViewDistance(this.getHandle());
+ return io.papermc.paper.chunk.system.ChunkSystem.getLoadViewDistance(this.getHandle()) - 1; // Paper - rewrite chunk system - TODO do this better
}
@Override
public void setViewDistance(final int viewDistance) {
- throw new UnsupportedOperationException("Not implemented yet");
+ // Paper - rewrite chunk system - TODO do this better
+ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
+ .moonrise$getViewDistanceHolder().setLoadViewDistance(viewDistance + 1);
}
@Override
@@ -3507,7 +3509,9 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@Override
public void setSimulationDistance(final int simulationDistance) {
- throw new UnsupportedOperationException("Not implemented yet");
+ // Paper - rewrite chunk system - TODO do this better
+ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
+ .moonrise$getViewDistanceHolder().setTickViewDistance(simulationDistance);
}
@Override
@@ -3517,6 +3521,8 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@Override
public void setSendViewDistance(final int viewDistance) {
- throw new UnsupportedOperationException("Not implemented yet");
+ // Paper - rewrite chunk system - TODO do this better
+ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
+ .moonrise$getViewDistanceHolder().setSendViewDistance(viewDistance);
}
}
diff --git a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
index 5717c0e1d6df07a4613356dc78d970d2101c68d7..cab7ca4218e5903b6a5e518af55457b9a1b5111c 100644
--- a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
+++ b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java
@@ -263,7 +263,7 @@ public class CustomChunkGenerator extends InternalChunkGenerator {
return ichunkaccess1;
};
- return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), net.minecraft.Util.backgroundExecutor()) : future.thenApply(function);
+ return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), Runnable::run) : future.thenApply(function); // Paper - rewrite chunk system
}
@Override
diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
index fceed3d08ee6f4c171685986bb19d2be592eedc6..bf18f9ad7dec2b09ebfcb5ec6566f2556e842f22 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
@@ -829,5 +829,12 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel {
public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) {
return this.handle.getChunkIfLoadedImmediately(x, z);
}
+
+ // Paper start - rewrite chunk system
+ @Override
+ public java.util.List<net.minecraft.world.entity.Entity> moonrise$getHardCollidingEntities(final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB box, final java.util.function.Predicate<? super net.minecraft.world.entity.Entity> predicate) {
+ return this.handle.moonrise$getHardCollidingEntities(entity, box, predicate);
+ }
+ // Paper end - rewrite chunk system
// Paper end
}
diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java
index e8e3cc48cf1c58bd8151d1f28df28781859cd0e3..67c8e90d3a2a93d858371d7fc1c3aaac3fdef71c 100644
--- a/src/main/java/org/spigotmc/AsyncCatcher.java
+++ b/src/main/java/org/spigotmc/AsyncCatcher.java
@@ -9,7 +9,7 @@ public class AsyncCatcher
public static void catchOp(String reason)
{
- if ( (AsyncCatcher.enabled || io.papermc.paper.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Paper
+ if (!io.papermc.paper.util.TickThread.isTickThread()) // Paper // Paper - rewrite chunk system
{
MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); // Paper
throw new IllegalStateException( "Asynchronous " + reason + "!" );
diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
index ad282d34919716b75acd10426cd071da9d064a51..7507e3058e7519a3e13b3be061746151a71b8f20 100644
--- a/src/main/java/org/spigotmc/WatchdogThread.java
+++ b/src/main/java/org/spigotmc/WatchdogThread.java
@@ -115,6 +115,7 @@ public class WatchdogThread extends Thread
// Paper end - Different message for short timeout
log.log( Level.SEVERE, "------------------------------" );
log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(MinecraftServer.getServer(), isLongTimeout); // Paper - rewrite chunk system
WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
log.log( Level.SEVERE, "------------------------------" );
//