13
0
geforkt von Mirrors/Paper
Paper/patches/server/0991-Chunk-System-Starlight-from-Moonrise.patch
Bjarne Koll 7df472527f
Configurable damage tick when blocking with shield (#10877)
A long standing bug in spigot and its derivatives was the fact that
players taking damage while blocking with a shield would not receive
invulnerability, while they do in vanilla.

This enabled the pvp technique of disabling a shield and immediately
attacking again to knock a player into the air.
While upstream fixed this and properly aligned itself with vanilla
damage logic (in this specific case) changing such long standing
behaviour has some downsides.

To allow players used to this specific bug to still use it, this patch
introduces a configuration option to re-introduce said bug.
As there is no easy way to *only* re-add this bug, the option is found
in the unsupported section as it may introduce other damage related
disparity from vanilla.
2024-06-15 23:17:51 +02:00

28757 Zeilen
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/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..e690549d08956676d6c2bc463732cc8067000618
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java
@@ -0,0 +1,151 @@
+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 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 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 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..eab09949c001fbfd708079fae83c45ab59fb25e7
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
@@ -0,0 +1,20 @@
+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);
+
+}
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..d0d97588e02a7846ef9da57679a9ca4525daee17
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
@@ -0,0 +1,47 @@
+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();
+
+}
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..f85820b959213c9bb566897c173f644fd430d01a
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
@@ -0,0 +1,810 @@
+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;
+
+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);
+ 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);
+ }
+ }
+ }
+ }
+
+ 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]);
+ }
+ // 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..3a8c192d1aed186ff506d69e3960e3b2792ddbd1
--- /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(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() + ",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..dba09cb32844533c383635e7623f5180a468f636
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
@@ -0,0 +1,1059 @@
+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 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 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 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 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..bc07e710a5854fd526e3bb56d1565602ec728ce1
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
@@ -0,0 +1,140 @@
+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 chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ UnloadSection section = this.unloadSections.get(chunkKey);
+ if (section == null) {
+ section = new UnloadSection(this.orderGenerator.getAndIncrement());
+ this.unloadSections.put(chunkKey, 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 chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+ final UnloadSection section = this.unloadSections.get(chunkKey);
+
+ if (section == null) {
+ return false;
+ }
+
+ if (!section.chunks.remove(chunkKey)) {
+ return false;
+ }
+
+ if (section.chunks.isEmpty()) {
+ this.unloadSections.remove(chunkKey);
+ }
+
+ 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());
+ 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..a7e7569b9d4160e7d92422ca5c1cce7f46b78f2e
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
@@ -0,0 +1,1430 @@
+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 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 org.slf4j.LoggerFactory;
+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 = LoggerFactory.getLogger(ChunkHolderManager.class);
+
+ 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) {
+ io.papermc.paper.util.TickThread.ensureTickThread("Cannot process ticket levels off-main");
+ 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.addProperty("lock_shift", Integer.valueOf(this.taskScheduler.getChunkSystemLockShift()));
+ ret.addProperty("ticket_shift", Integer.valueOf(ThreadedTicketLevelPropagator.SECTION_SHIFT));
+ ret.addProperty("region_shift", Integer.valueOf(((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift()));
+
+ 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..f52e104b3e07825caf0d6d1bda2e45c8437d6e20
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
@@ -0,0 +1,923 @@
+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.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.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 net.minecraft.CrashReport;
+import net.minecraft.CrashReportCategory;
+import net.minecraft.ReportedException;
+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.TicketType;
+import net.minecraft.util.StaticCache2D;
+import net.minecraft.world.level.ChunkPos;
+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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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 = LoggerFactory.getLogger(ChunkTaskScheduler.class);
+
+ 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 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) {
+ onComplete.accept(null);
+ } else {
+ if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
+ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ } else {
+ 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;
+ }
+
+ @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]);
+ }
+ }
+
+ // Paper start
+ public static void dumpAllChunkLoadInfo(final boolean longPrint) {
+ final ChunkInfo[] chunkInfos = getChunkInfos();
+ if (chunkInfos.length > 0) {
+ LOGGER.error("Chunk wait task info below: ");
+ for (final ChunkInfo chunkInfo : chunkInfos) {
+ final NewChunkHolder holder = chunkInfo.world.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkInfo.chunkX, chunkInfo.chunkZ);
+ LOGGER.error("Chunk wait: " + chunkInfo);
+ LOGGER.error("Chunk holder: " + holder);
+ }
+
+ // TODO
+ /*
+ if (longPrint) {
+ final File file = new File(new File(new File("."), "debug"), "chunks-watchdog.txt");
+ LOGGER.error("Writing chunk information dump to " + file);
+ try {
+ MCUtil.dumpChunks(file, true);
+ LOGGER.error("Successfully written chunk information!");
+ } catch (final Throwable thr) {
+ LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
+ }
+ }
+ */
+ }
+ }
+ // Paper end
+}
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..cbc48ffafd3afd8d9f71790fafd15fcecfcedd78
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
@@ -0,0 +1,2014 @@
+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.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.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 unsynchronised access to the chunk and last gen status
+ private volatile ChunkCompletion lastChunkCompletion;
+
+ public ChunkCompletion getLastChunkCompletion() {
+ return this.lastChunkCompletion;
+ }
+
+ public static final record ChunkCompletion(ChunkAccess chunk, ChunkStatus genStatus) {};
+
+ /**
+ * 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);
+ }
+
+ private ImposterProtoChunk wrappedChunkForNeighbour;
+
+ // holds scheduling lock
+ public ChunkAccess getChunkForNeighbourAccess() {
+ // Vanilla overrides the status futures with an imposter chunk to prevent writes to full chunks
+ // But we don't store per-status futures, so we need this hack
+ if (this.wrappedChunkForNeighbour != null) {
+ return this.wrappedChunkForNeighbour;
+ }
+ final ChunkAccess ret = this.currentChunk;
+ return ret instanceof LevelChunk fullChunk ? this.wrappedChunkForNeighbour = new ImposterProtoChunk(fullChunk, false) : ret;
+ }
+
+ 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.wrappedChunkForNeighbour = null;
+ this.lastChunkCompletion = 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.generationStatusOrEmpty(oldLevel);
+ final ChunkStatus maxGenerationStatusNew = ChunkLevel.generationStatusOrEmpty(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);
+ 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);
+ 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;
+ this.lastChunkCompletion = new ChunkCompletion(newChunk, newStatus);
+
+ 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..1cf055447bfe235b806bfef6c95aa025f28cb239
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
@@ -0,0 +1,138 @@
+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
+ });
+ }
+
+ 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..21c9562781b05adf3871e522fddb654d75f605ba
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java
@@ -0,0 +1,7 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.server;
+
+public interface ChunkSystemMinecraftServer {
+
+ public void moonrise$setChunkSystemCrash(final Throwable throwable);
+
+}
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..51c9ed3dbb1787afc8e123b4f8104d54ed7ace89
--- /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 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/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..39ec319d0899a6e26f29fa7a6ead4bda78ecbaeb
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java
@@ -0,0 +1,20 @@
+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;
+
+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);
+
+}
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 0d0cb3e63acd5156b6f9d6d78cc949b0af36a77b..dd1649abe57d7191c15a9b2862d5fd11ff0706b1 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,31 +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) {
- onComplete.accept(null);
- } else {
- if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
- } else {
- 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);
@@ -67,164 +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 ThreadDeath death) {
- throw death;
- } 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 ThreadDeath death) {
- throw death;
- } 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) {
@@ -274,27 +129,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/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..9d04285165241baec1005cb3ae81a623bcd3945a
--- /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 2;
+ }
+
+}
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/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 6915522f669631779c1fb8a8e2db330f4b9fb921..cd69971065b13353353eca55f6e145949390de11 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,15 @@ 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;
+ }
+ // 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 +666,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 +879,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 +893,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 +994,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 +1011,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 +1031,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 +1206,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);
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
index c643bb0daa5cd264fd6ebab7acf0a2bdd7fe7029..0048077dedd19adc6c5a88e7d916d88a71662115 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 (!ChunkLevel.fullStatus(this.newChunkHolder.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) 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..e56b9333a4d99fa6a94e923cb5e8c7544c6309bf 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;
@@ -19,6 +19,12 @@ public class ChunkLevel {
return getStatusAroundFullChunk(level - 33, null);
}
+ // Paper start - rewrite chunk system
+ public static ChunkStatus generationStatusOrEmpty(int level) {
+ return getStatusAroundFullChunk(level - 33, ChunkStatus.EMPTY);
+ }
+ // Paper end - rewrite chunk system
+
@Nullable
@Contract("_,!null->!null;_,_->_")
public static ChunkStatus getStatusAroundFullChunk(int additionalLevel, @Nullable ChunkStatus emptyStatus) {
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index 2ce7da9707d7c1a48b5609ae51a516d599d7aee8..b849e0cf15f894aa87b1bb397d85b887b8fb816e 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();
@@ -223,23 +218,21 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
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();
@@ -270,15 +263,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.dataRegionManager = new io.papermc.paper.chunk.SingleThreadChunkRegionManager(this.level, 2, (1.0 / 3.0), 1, 6, "Data", DataRegionData::new, DataRegionSectionData::new);
this.regionManagers.add(this.dataRegionManager);
@@ -319,23 +310,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() {
@@ -344,20 +323,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) {
@@ -386,55 +367,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) {
@@ -464,93 +397,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) {
@@ -567,134 +430,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) {
@@ -754,137 +508,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() {
@@ -892,135 +553,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);
@@ -1086,7 +696,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
}
@@ -1180,7 +790,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();
@@ -1191,7 +801,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
}
}
@@ -1239,71 +849,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) {
@@ -1374,13 +944,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();
@@ -1487,27 +1051,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..864f521e1d683b106ec109d5927679467cd6bb38 100644
--- a/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java
@@ -27,249 +27,113 @@ 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
+ 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 || !lastCompletion.genStatus().isOrAfter(requestedStatus) ? null : lastCompletion.chunk();
+ // Paper end - rewrite chunk system
}
@Nullable
public ChunkAccess getChunkIfPresent(ChunkStatus requestedStatus) {
- return this.isStatusDisallowed(requestedStatus) ? null : this.getChunkIfPresentUnchecked(requestedStatus);
+ // Paper start - rewrite chunk system
+ final ChunkStatus maxStatus = ChunkLevel.generationStatus(this.getTicketLevel());
+
+ if (maxStatus == null || requestedStatus.isOrAfter(maxStatus)) {
+ return null;
+ }
+
+ 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 || !lastCompletion.genStatus().isOrAfter(requestedStatus) ? null : lastCompletion.chunk();
+ // 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 +141,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 +150,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 a94833f58eb823332890d07c147161e9e0a938e9..b2d444f5b1a89e1f03bf4422474accde184240b0 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -75,6 +75,29 @@ public class ServerChunkCache extends ChunkSource {
long chunkFutureAwaitCounter;
private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4];
// Paper end
+ // Paper start - rewrite chunk system
+ 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;
+ }
+ // 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;
@@ -248,63 +271,36 @@ 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
+ 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;
- gameprofilerfiller.incrementCounter("getChunk");
- long k = ChunkPos.asLong(x, z);
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder currentChunk = chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(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 (leastStatus == ChunkStatus.FULL) {
+ if (currentChunk != null && currentChunk.isFullChunkReady() && (currentChunk.getCurrentChunk() instanceof LevelChunk fullChunk)) {
+ return fullChunk;
+ } else if (!create) {
+ return null;
}
-
- 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 this.syncLoad(x, z, leastStatus);
+ } else {
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion;
+ if (currentChunk != null && (lastCompletion = currentChunk.getLastChunkCompletion()) != null &&
+ lastCompletion.genStatus().isOrAfter(leastStatus)) {
+ return lastCompletion.chunk();
+ } else if (!create) {
+ return null;
}
+ return this.syncLoad(x, z, leastStatus);
}
+ // 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() {
@@ -335,56 +331,63 @@ 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 ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion chunkCompletion = chunkHolder == null ? null : chunkHolder.getLastChunkCompletion();
+ if (needsFullScheduling || chunkCompletion == null || !chunkCompletion.genStatus().isOrAfter(leastStatus)) {
+ // schedule
+ CompletableFuture<ChunkResult<ChunkAccess>> ret = new CompletableFuture<>();
+ 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(chunkCompletion.chunk()));
+ }
+ // 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;
+ }
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion();
+ if (lastCompletion == null || !lastCompletion.genStatus().isOrAfter(ChunkStatus.INITIALIZE_LIGHT)) {
+ return null;
+ }
+ return lastCompletion.chunk();
+ // Paper end - rewrite chunk system
}
@Override
@@ -397,16 +400,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
@@ -416,13 +410,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
@@ -435,12 +430,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
@@ -468,6 +458,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();
@@ -567,11 +558,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
}
@@ -665,6 +657,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);
}
@@ -743,16 +741,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..37971d9fc59ecf3736fccf7a27f17e37a56efeb9 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,163 @@ 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;
+
+ @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.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;
+ }
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion();
+ return lastCompletion == null || !lastCompletion.genStatus().isOrAfter(leastStatus) ? null : lastCompletion.chunk();
+ }
+
+ @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;
+ }
+ // 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 +542,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 +576,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 +722,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 +747,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() {
@@ -1061,6 +1233,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 +1253,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 +1400,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 +1431,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 +1442,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 +2028,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 +2077,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 +2098,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 +2240,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 +2270,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 +2308,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 +2374,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 3cbb59df34156479d24a8251f2b3acbb5e60dc2c..6b9354e3ac064daa3101e71d8e54e883f628f70c 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..4700c97487e176c670a3930564b621b4a6bf52bb 100644
--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
@@ -23,15 +23,49 @@ 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);
+ });
+ }
+ // Paper end - rewrite chunk system
public ThreadedLevelLightEngine(
LightChunkGetter chunkProvider,
@@ -42,8 +76,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 +90,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 8779d54c816bb97ccdeb268d1929f693d322ee14..bf60b1aba3019996f53a3cf051d2a603cb7b8404 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();
@@ -4408,6 +4479,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;
}
@@ -4539,6 +4619,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
@@ -4550,7 +4636,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) {
@@ -4581,7 +4667,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 6f822e9487bef5b9766d5ae86ebbd687e4eadc42..77c6613c39e3b266944e28cf2627483d9f32c511 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,58 @@ 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);
+ }
+ // 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 +333,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
@@ -551,7 +604,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);
}
@@ -951,7 +1004,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) {
@@ -1041,28 +1094,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();
-
- this.getEntities().get(box, (entity1) -> {
- if (entity1 != except && predicate.test(entity1)) {
- list.add(entity1);
- }
+ // Paper start - rewrite chunk system
+ final List<Entity> ret = new java.util.ArrayList<>();
- if (entity1 instanceof EnderDragon) {
- EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity1).getSubEntities();
- int i = aentitycomplexpart.length;
-
- for (int j = 0; j < i; ++j) {
- EnderDragonPart entitycomplexpart = aentitycomplexpart[j];
-
- if (entity1 != except && predicate.test(entitycomplexpart)) {
- list.add(entitycomplexpart);
- }
- }
- }
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(except, box, ret, predicate);
- });
- return list;
+ return ret;
+ // Paper end - rewrite chunk system
}
@Override
@@ -1077,36 +1115,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 6c0e12c9c9c0fb8377cd1f48a43ca75c9fc3e58e..3be8f35ece18d4cffe8b23ecfeeff359e0b36e3e 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() {
@@ -218,6 +218,14 @@ public class LevelChunk extends ChunkAccess {
}
}
// 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());
@@ -247,13 +255,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
@@ -436,7 +450,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();
@@ -777,8 +791,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() {
@@ -884,6 +917,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..cce2fed2d4e9d6147ea1854321012c6950eb05cc 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
@@ -116,60 +116,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 e41b7191d0cebeb77b8d07a1b5bddc1b695cb922..ecd975855d92ae5948ee926f42396cb004843aed 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -3491,12 +3491,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
@@ -3506,7 +3508,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
@@ -3516,6 +3520,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 cd7f1309cf01a5f01a28aded03a36fe15adb1756..43cb7b91945a72ab4bd998a3a3eca3cdcf0432a9 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
@@ -815,19 +815,26 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel {
@Nullable
@Override
public BlockState getBlockStateIfLoaded(final BlockPos blockposition) {
- return null;
+ return this.handle.getBlockStateIfLoaded(blockposition); // Paper - rewrite chunk system
}
@Nullable
@Override
public FluidState getFluidIfLoaded(final BlockPos blockposition) {
- return null;
+ return this.handle.getFluidIfLoaded(blockposition); // Paper - rewrite chunk system
}
@Nullable
@Override
public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) {
- return null;
+ return this.handle.getChunkIfLoadedImmediately(x, z); // Paper - rewrite chunk system
}
+
+ // 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..c68256c0c8e131497108f677c6b254c589ce67e2 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(isLongTimeout); // Paper - rewrite chunk system
WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
log.log( Level.SEVERE, "------------------------------" );
//