diff options
Diffstat (limited to 'patches/server/0989-Moonrise-optimisation-patches.patch')
-rw-r--r-- | patches/server/0989-Moonrise-optimisation-patches.patch | 36282 |
1 files changed, 36282 insertions, 0 deletions
diff --git a/patches/server/0989-Moonrise-optimisation-patches.patch b/patches/server/0989-Moonrise-optimisation-patches.patch new file mode 100644 index 0000000000..ae4d0b2e53 --- /dev/null +++ b/patches/server/0989-Moonrise-optimisation-patches.patch @@ -0,0 +1,36282 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf <[email protected]> +Date: Fri, 14 Jun 2024 11:57:26 -0700 +Subject: [PATCH] Moonrise optimisation patches + +Currently includes: + - Starlight + Chunk System + - Entity tracker optimisations + - Collision optimisations + - Random block ticking optimisations + +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/NearbyPlayers.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fdde1f5f988d581db41945916635cdd826a10680 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java +@@ -0,0 +1,209 @@ ++package ca.spottedleaf.moonrise.common.misc; ++ ++import ca.spottedleaf.moonrise.common.list.ReferenceList; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.MoonriseConstants; ++import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.level.ChunkPos; ++ ++public final class NearbyPlayers { ++ ++ public static enum NearbyMapType { ++ GENERAL, ++ GENERAL_SMALL, ++ GENERAL_REALLY_SMALL, ++ TICK_VIEW_DISTANCE, ++ VIEW_DISTANCE, ++ SPAWN_RANGE, ++ } ++ ++ private static final NearbyMapType[] MAP_TYPES = NearbyMapType.values(); ++ public static final int TOTAL_MAP_TYPES = MAP_TYPES.length; ++ ++ private static final int GENERAL_AREA_VIEW_DISTANCE = MoonriseConstants.MAX_VIEW_DISTANCE + 1; ++ private static final int GENERAL_SMALL_VIEW_DISTANCE = 10; ++ private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3; ++ ++ public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4); ++ public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4); ++ public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4); ++ ++ private final ServerLevel world; ++ private final Reference2ReferenceOpenHashMap<ServerPlayer, TrackedPlayer[]> players = new Reference2ReferenceOpenHashMap<>(); ++ private final Long2ReferenceOpenHashMap<TrackedChunk> byChunk = new Long2ReferenceOpenHashMap<>(); ++ ++ public NearbyPlayers(final ServerLevel world) { ++ this.world = world; ++ } ++ ++ public void addPlayer(final ServerPlayer player) { ++ final TrackedPlayer[] newTrackers = new TrackedPlayer[TOTAL_MAP_TYPES]; ++ if (this.players.putIfAbsent(player, newTrackers) != null) { ++ throw new IllegalStateException("Already have player " + player); ++ } ++ ++ final ChunkPos chunk = player.chunkPosition(); ++ ++ for (int i = 0; i < TOTAL_MAP_TYPES; ++i) { ++ // use 0 for default, will be updated by tickPlayer ++ (newTrackers[i] = new TrackedPlayer(player, MAP_TYPES[i])).add(chunk.x, chunk.z, 0); ++ } ++ ++ // update view distances ++ this.tickPlayer(player); ++ } ++ ++ public void removePlayer(final ServerPlayer player) { ++ final TrackedPlayer[] players = this.players.remove(player); ++ if (players == null) { ++ return; // May be called during teleportation before the player is actually placed ++ } ++ ++ for (final TrackedPlayer tracker : players) { ++ tracker.remove(); ++ } ++ } ++ ++ public void tickPlayer(final ServerPlayer player) { ++ final TrackedPlayer[] players = this.players.get(player); ++ if (players == null) { ++ throw new IllegalStateException("Don't have player " + player); ++ } ++ ++ final ChunkPos chunk = player.chunkPosition(); ++ ++ players[NearbyMapType.GENERAL.ordinal()].update(chunk.x, chunk.z, GENERAL_AREA_VIEW_DISTANCE); ++ players[NearbyMapType.GENERAL_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_SMALL_VIEW_DISTANCE); ++ players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE); ++ players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player)); ++ players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getLoadViewDistance(player)); ++ } ++ ++ public TrackedChunk getChunk(final ChunkPos pos) { ++ return this.byChunk.get(CoordinateUtils.getChunkKey(pos)); ++ } ++ ++ public TrackedChunk getChunk(final BlockPos pos) { ++ return this.byChunk.get(CoordinateUtils.getChunkKey(pos)); ++ } ++ ++ public ReferenceList<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) { ++ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos)); ++ ++ return chunk == null ? null : chunk.players[type.ordinal()]; ++ } ++ ++ public ReferenceList<ServerPlayer> getPlayers(final ChunkPos pos, final NearbyMapType type) { ++ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos)); ++ ++ return chunk == null ? null : chunk.players[type.ordinal()]; ++ } ++ ++ public ReferenceList<ServerPlayer> getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) { ++ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ return chunk == null ? null : chunk.players[type.ordinal()]; ++ } ++ ++ public ReferenceList<ServerPlayer> getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) { ++ final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4)); ++ ++ return chunk == null ? null : chunk.players[type.ordinal()]; ++ } ++ ++ public static final class TrackedChunk { ++ ++ private static final ServerPlayer[] EMPTY_PLAYERS_ARRAY = new ServerPlayer[0]; ++ ++ private final ReferenceList<ServerPlayer>[] players = new ReferenceList[TOTAL_MAP_TYPES]; ++ private int nonEmptyLists; ++ private long updateCount; ++ ++ public boolean isEmpty() { ++ return this.nonEmptyLists == 0; ++ } ++ ++ public long getUpdateCount() { ++ return this.updateCount; ++ } ++ ++ public ReferenceList<ServerPlayer> getPlayers(final NearbyMapType type) { ++ return this.players[type.ordinal()]; ++ } ++ ++ public void addPlayer(final ServerPlayer player, final NearbyMapType type) { ++ ++this.updateCount; ++ ++ final int idx = type.ordinal(); ++ final ReferenceList<ServerPlayer> list = this.players[idx]; ++ if (list == null) { ++ ++this.nonEmptyLists; ++ (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY, 0)).add(player); ++ return; ++ } ++ ++ if (!list.add(player)) { ++ throw new IllegalStateException("Already contains player " + player); ++ } ++ } ++ ++ public void removePlayer(final ServerPlayer player, final NearbyMapType type) { ++ ++this.updateCount; ++ ++ final int idx = type.ordinal(); ++ final ReferenceList<ServerPlayer> list = this.players[idx]; ++ if (list == null) { ++ throw new IllegalStateException("Does not contain player " + player); ++ } ++ ++ if (!list.remove(player)) { ++ throw new IllegalStateException("Does not contain player " + player); ++ } ++ ++ if (list.size() == 0) { ++ this.players[idx] = null; ++ --this.nonEmptyLists; ++ } ++ } ++ } ++ ++ private final class TrackedPlayer extends SingleUserAreaMap<ServerPlayer> { ++ ++ private final NearbyMapType type; ++ ++ public TrackedPlayer(final ServerPlayer player, final NearbyMapType type) { ++ super(player); ++ this.type = type; ++ } ++ ++ @Override ++ protected void addCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) { ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ NearbyPlayers.this.byChunk.computeIfAbsent(chunkKey, (final long keyInMap) -> { ++ return new TrackedChunk(); ++ }).addPlayer(parameter, this.type); ++ } ++ ++ @Override ++ protected void removeCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) { ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey); ++ if (chunk == null) { ++ throw new IllegalStateException("Chunk should exist at " + new ChunkPos(chunkKey)); ++ } ++ ++ chunk.removePlayer(parameter, this.type); ++ ++ if (chunk.isEmpty()) { ++ NearbyPlayers.this.byChunk.remove(chunkKey); ++ } ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..61f70247486fd15ed3ffc5b606582dc6a2dd81d3 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java +@@ -0,0 +1,232 @@ ++package ca.spottedleaf.moonrise.common.misc; ++ ++import ca.spottedleaf.concurrentutil.util.IntegerUtil; ++ ++public abstract class SingleUserAreaMap<T> { ++ ++ private static final int NOT_SET = Integer.MIN_VALUE; ++ ++ private final T parameter; ++ private int lastChunkX = NOT_SET; ++ private int lastChunkZ = NOT_SET; ++ private int distance = NOT_SET; ++ ++ public SingleUserAreaMap(final T parameter) { ++ this.parameter = parameter; ++ } ++ ++ /* math sign function except 0 returns 1 */ ++ protected static int sign(int val) { ++ return 1 | (val >> (Integer.SIZE - 1)); ++ } ++ ++ protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ); ++ ++ protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ); ++ ++ private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) { ++ final int maxX = chunkX + distance; ++ final int maxZ = chunkZ + distance; ++ ++ for (int cx = chunkX - distance; cx <= maxX; ++cx) { ++ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) { ++ this.addCallback(parameter, cx, cz); ++ } ++ } ++ } ++ ++ private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) { ++ final int maxX = chunkX + distance; ++ final int maxZ = chunkZ + distance; ++ ++ for (int cx = chunkX - distance; cx <= maxX; ++cx) { ++ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) { ++ this.removeCallback(parameter, cx, cz); ++ } ++ } ++ } ++ ++ public final boolean add(final int chunkX, final int chunkZ, final int distance) { ++ if (distance < 0) { ++ throw new IllegalArgumentException(Integer.toString(distance)); ++ } ++ if (this.lastChunkX != NOT_SET) { ++ return false; ++ } ++ this.lastChunkX = chunkX; ++ this.lastChunkZ = chunkZ; ++ this.distance = distance; ++ ++ this.addToNew(this.parameter, chunkX, chunkZ, distance); ++ ++ return true; ++ } ++ ++ public final boolean update(final int toX, final int toZ, final int newViewDistance) { ++ if (newViewDistance < 0) { ++ throw new IllegalArgumentException(Integer.toString(newViewDistance)); ++ } ++ final int fromX = this.lastChunkX; ++ final int fromZ = this.lastChunkZ; ++ final int oldViewDistance = this.distance; ++ if (fromX == NOT_SET) { ++ return false; ++ } ++ ++ this.lastChunkX = toX; ++ this.lastChunkZ = toZ; ++ this.distance = newViewDistance; ++ ++ final T parameter = this.parameter; ++ ++ ++ final int dx = toX - fromX; ++ final int dz = toZ - fromZ; ++ ++ final int totalX = IntegerUtil.branchlessAbs(fromX - toX); ++ final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ); ++ ++ if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) { ++ // teleported ++ this.removeFromOld(parameter, fromX, fromZ, oldViewDistance); ++ this.addToNew(parameter, toX, toZ, newViewDistance); ++ return true; ++ } ++ ++ if (oldViewDistance != newViewDistance) { ++ // remove loop ++ ++ final int oldMinX = fromX - oldViewDistance; ++ final int oldMinZ = fromZ - oldViewDistance; ++ final int oldMaxX = fromX + oldViewDistance; ++ final int oldMaxZ = fromZ + oldViewDistance; ++ for (int currX = oldMinX; currX <= oldMaxX; ++currX) { ++ for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) { ++ ++ // only remove if we're outside the new view distance... ++ if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) { ++ this.removeCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ // add loop ++ ++ final int newMinX = toX - newViewDistance; ++ final int newMinZ = toZ - newViewDistance; ++ final int newMaxX = toX + newViewDistance; ++ final int newMaxZ = toZ + newViewDistance; ++ for (int currX = newMinX; currX <= newMaxX; ++currX) { ++ for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) { ++ ++ // only add if we're outside the old view distance... ++ if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) { ++ this.addCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ // x axis is width ++ // z axis is height ++ // right refers to the x axis of where we moved ++ // top refers to the z axis of where we moved ++ ++ // same view distance ++ ++ // used for relative positioning ++ final int up = sign(dz); // 1 if dz >= 0, -1 otherwise ++ final int right = sign(dx); // 1 if dx >= 0, -1 otherwise ++ ++ // The area excluded by overlapping the two view distance squares creates four rectangles: ++ // Two on the left, and two on the right. The ones on the left we consider the "removed" section ++ // and on the right the "added" section. ++ // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually ++ // exclusive to the regions they surround. ++ ++ // 4 points of the rectangle ++ int maxX; // exclusive ++ int minX; // inclusive ++ int maxZ; // exclusive ++ int minZ; // inclusive ++ ++ if (dx != 0) { ++ // handle right addition ++ ++ maxX = toX + (oldViewDistance * right) + right; // exclusive ++ minX = fromX + (oldViewDistance * right) + right; // inclusive ++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive ++ minZ = toZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.addCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ if (dz != 0) { ++ // handle up addition ++ ++ maxX = toX + (oldViewDistance * right) + right; // exclusive ++ minX = toX - (oldViewDistance * right); // inclusive ++ maxZ = toZ + (oldViewDistance * up) + up; // exclusive ++ minZ = fromZ + (oldViewDistance * up) + up; // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.addCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ if (dx != 0) { ++ // handle left removal ++ ++ maxX = toX - (oldViewDistance * right); // exclusive ++ minX = fromX - (oldViewDistance * right); // inclusive ++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive ++ minZ = toZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.removeCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ if (dz != 0) { ++ // handle down removal ++ ++ maxX = fromX + (oldViewDistance * right) + right; // exclusive ++ minX = fromX - (oldViewDistance * right); // inclusive ++ maxZ = toZ - (oldViewDistance * up); // exclusive ++ minZ = fromZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.removeCallback(parameter, currX, currZ); ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ public final boolean remove() { ++ final int chunkX = this.lastChunkX; ++ final int chunkZ = this.lastChunkZ; ++ final int distance = this.distance; ++ if (chunkX == NOT_SET) { ++ return false; ++ } ++ ++ this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET; ++ ++ this.removeFromOld(this.parameter, chunkX, chunkZ, distance); ++ ++ return true; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4123edddc556c47f3f8d83523c125fd2e46b30e2 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java +@@ -0,0 +1,68 @@ ++package ca.spottedleaf.moonrise.common.set; ++ ++import java.util.Collection; ++ ++public final class OptimizedSmallEnumSet<E extends Enum<E>> { ++ ++ private final Class<E> enumClass; ++ private long backingSet; ++ ++ public OptimizedSmallEnumSet(final Class<E> clazz) { ++ if (clazz == null) { ++ throw new IllegalArgumentException("Null class"); ++ } ++ if (!clazz.isEnum()) { ++ throw new IllegalArgumentException("Class must be enum, not " + clazz.getCanonicalName()); ++ } ++ this.enumClass = clazz; ++ } ++ ++ public boolean addUnchecked(final E element) { ++ final int ordinal = element.ordinal(); ++ final long key = 1L << ordinal; ++ ++ final long prev = this.backingSet; ++ this.backingSet = prev | key; ++ ++ return (prev & key) == 0; ++ } ++ ++ public boolean removeUnchecked(final E element) { ++ final int ordinal = element.ordinal(); ++ final long key = 1L << ordinal; ++ ++ final long prev = this.backingSet; ++ this.backingSet = prev & ~key; ++ ++ return (prev & key) != 0; ++ } ++ ++ public void clear() { ++ this.backingSet = 0L; ++ } ++ ++ public int size() { ++ return Long.bitCount(this.backingSet); ++ } ++ ++ public void addAllUnchecked(final Collection<E> enums) { ++ for (final E element : enums) { ++ if (element == null) { ++ throw new NullPointerException("Null element"); ++ } ++ this.backingSet |= (1L << element.ordinal()); ++ } ++ } ++ ++ public long getBackingSet() { ++ return this.backingSet; ++ } ++ ++ public boolean hasCommonElements(final OptimizedSmallEnumSet<E> other) { ++ return (other.backingSet & this.backingSet) != 0; ++ } ++ ++ public boolean hasElement(final E element) { ++ return (this.backingSet & (1L << element.ordinal())) != 0; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java +new file mode 100644 +index 0000000000000000000000000000000000000000..31b92bd48828cbea25b44a9f0f96886347aa1ae6 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java +@@ -0,0 +1,129 @@ ++package ca.spottedleaf.moonrise.common.util; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.SectionPos; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.phys.Vec3; ++ ++public final class CoordinateUtils { ++ ++ // the chunk keys are compatible with vanilla ++ ++ public static long getChunkKey(final BlockPos pos) { ++ return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL); ++ } ++ ++ public static long getChunkKey(final Entity entity) { ++ return ((Mth.lfloor(entity.getZ()) >> 4) << 32) | ((Mth.lfloor(entity.getX()) >> 4) & 0xFFFFFFFFL); ++ } ++ ++ public static long getChunkKey(final ChunkPos pos) { ++ return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL); ++ } ++ ++ public static long getChunkKey(final SectionPos pos) { ++ return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL); ++ } ++ ++ public static long getChunkKey(final int x, final int z) { ++ return ((long)z << 32) | (x & 0xFFFFFFFFL); ++ } ++ ++ public static int getChunkX(final long chunkKey) { ++ return (int)chunkKey; ++ } ++ ++ public static int getChunkZ(final long chunkKey) { ++ return (int)(chunkKey >>> 32); ++ } ++ ++ public static int getChunkCoordinate(final double blockCoordinate) { ++ return Mth.floor(blockCoordinate) >> 4; ++ } ++ ++ // the section keys are compatible with vanilla's ++ ++ static final int SECTION_X_BITS = 22; ++ static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1; ++ static final int SECTION_Y_BITS = 20; ++ static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1; ++ static final int SECTION_Z_BITS = 22; ++ static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1; ++ // format is y,z,x (in order of LSB to MSB) ++ static final int SECTION_Y_SHIFT = 0; ++ static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS; ++ static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS; ++ static final int SECTION_TO_BLOCK_SHIFT = 4; ++ ++ public static long getChunkSectionKey(final int x, final int y, final int z) { ++ return ((x & SECTION_X_MASK) << SECTION_X_SHIFT) ++ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) ++ | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT); ++ } ++ ++ public static long getChunkSectionKey(final SectionPos pos) { ++ return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT) ++ | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT) ++ | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT); ++ } ++ ++ public static long getChunkSectionKey(final ChunkPos pos, final int y) { ++ return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT) ++ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) ++ | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT); ++ } ++ ++ public static long getChunkSectionKey(final BlockPos pos) { ++ return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | ++ ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | ++ (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); ++ } ++ ++ public static long getChunkSectionKey(final Entity entity) { ++ return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | ++ ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | ++ ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); ++ } ++ ++ public static int getChunkSectionX(final long key) { ++ return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS)); ++ } ++ ++ public static int getChunkSectionY(final long key) { ++ return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS)); ++ } ++ ++ public static int getChunkSectionZ(final long key) { ++ return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS)); ++ } ++ ++ public static int getBlockX(final Vec3 pos) { ++ return Mth.floor(pos.x); ++ } ++ ++ public static int getBlockY(final Vec3 pos) { ++ return Mth.floor(pos.y); ++ } ++ ++ public static int getBlockZ(final Vec3 pos) { ++ return Mth.floor(pos.z); ++ } ++ ++ public static int getChunkX(final Vec3 pos) { ++ return Mth.floor(pos.x) >> 4; ++ } ++ ++ public static int getChunkY(final Vec3 pos) { ++ return Mth.floor(pos.y) >> 4; ++ } ++ ++ public static int getChunkZ(final Vec3 pos) { ++ return Mth.floor(pos.z) >> 4; ++ } ++ ++ private CoordinateUtils() { ++ throw new RuntimeException(); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0531f25aaad162386a029d33e68d7c8336b9d5d1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java +@@ -0,0 +1,109 @@ ++package ca.spottedleaf.moonrise.common.util; ++ ++import java.util.Objects; ++ ++public final class FlatBitsetUtil { ++ ++ private static final int LOG2_LONG = 6; ++ private static final long ALL_SET = -1L; ++ private static final int BITS_PER_LONG = Long.SIZE; ++ ++ // from inclusive ++ // to exclusive ++ public static int firstSet(final long[] bitset, final int from, final int to) { ++ if ((from | to | (to - from)) < 0) { ++ throw new IndexOutOfBoundsException(); ++ } ++ ++ int bitsetIdx = from >>> LOG2_LONG; ++ int bitIdx = from & ~(BITS_PER_LONG - 1); ++ ++ long tmp = bitset[bitsetIdx] & (ALL_SET << from); ++ for (;;) { ++ if (tmp != 0L) { ++ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp); ++ return ret >= to ? -1 : ret; ++ } ++ ++ bitIdx += BITS_PER_LONG; ++ ++ if (bitIdx >= to) { ++ return -1; ++ } ++ ++ tmp = bitset[++bitsetIdx]; ++ } ++ } ++ ++ // from inclusive ++ // to exclusive ++ public static int firstClear(final long[] bitset, final int from, final int to) { ++ if ((from | to | (to - from)) < 0) { ++ throw new IndexOutOfBoundsException(); ++ } ++ // like firstSet, but invert the bitset ++ ++ int bitsetIdx = from >>> LOG2_LONG; ++ int bitIdx = from & ~(BITS_PER_LONG - 1); ++ ++ long tmp = (~bitset[bitsetIdx]) & (ALL_SET << from); ++ for (;;) { ++ if (tmp != 0L) { ++ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp); ++ return ret >= to ? -1 : ret; ++ } ++ ++ bitIdx += BITS_PER_LONG; ++ ++ if (bitIdx >= to) { ++ return -1; ++ } ++ ++ tmp = ~bitset[++bitsetIdx]; ++ } ++ } ++ ++ // from inclusive ++ // to exclusive ++ public static void clearRange(final long[] bitset, final int from, int to) { ++ if ((from | to | (to - from)) < 0) { ++ throw new IndexOutOfBoundsException(); ++ } ++ ++ if (from == to) { ++ return; ++ } ++ ++ --to; ++ ++ final int fromBitsetIdx = from >>> LOG2_LONG; ++ final int toBitsetIdx = to >>> LOG2_LONG; ++ ++ final long keepFirst = ~(ALL_SET << from); ++ final long keepLast = ~(ALL_SET >>> ((BITS_PER_LONG - 1) ^ to)); ++ ++ Objects.checkFromToIndex(fromBitsetIdx, toBitsetIdx, bitset.length); ++ ++ if (fromBitsetIdx == toBitsetIdx) { ++ // special case: need to keep both first and last ++ bitset[fromBitsetIdx] &= (keepFirst | keepLast); ++ } else { ++ bitset[fromBitsetIdx] &= keepFirst; ++ ++ for (int i = fromBitsetIdx + 1; i < toBitsetIdx; ++i) { ++ bitset[i] = 0L; ++ } ++ ++ bitset[toBitsetIdx] &= keepLast; ++ } ++ } ++ ++ // from inclusive ++ // to exclusive ++ public static boolean isRangeSet(final long[] bitset, final int from, final int to) { ++ return firstClear(bitset, from, to) == -1; ++ } ++ ++ ++ private FlatBitsetUtil() {} ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..91efda726b87a8a8f28dee84e31b6a7063752ebd +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java +@@ -0,0 +1,34 @@ ++package ca.spottedleaf.moonrise.common.util; ++ ++import com.google.gson.JsonElement; ++import com.google.gson.internal.Streams; ++import com.google.gson.stream.JsonWriter; ++import java.io.File; ++import java.io.FileOutputStream; ++import java.io.IOException; ++import java.io.PrintStream; ++import java.io.StringWriter; ++import java.nio.charset.StandardCharsets; ++ ++public final class JsonUtil { ++ ++ public static void writeJson(final JsonElement element, final File file) throws IOException { ++ final StringWriter stringWriter = new StringWriter(); ++ final JsonWriter jsonWriter = new JsonWriter(stringWriter); ++ jsonWriter.setIndent(" "); ++ jsonWriter.setLenient(false); ++ Streams.write(element, jsonWriter); ++ ++ final String jsonString = stringWriter.toString(); ++ ++ final File parent = file.getParentFile(); ++ if (parent != null) { ++ parent.mkdirs(); ++ } ++ file.createNewFile(); ++ try (final PrintStream out = new PrintStream(new FileOutputStream(file), false, StandardCharsets.UTF_8)) { ++ out.print(jsonString); ++ } ++ } ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ac6f284ee4469d16c5655328b2488d7612832353 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java +@@ -0,0 +1,10 @@ ++package ca.spottedleaf.moonrise.common.util; ++ ++public final class MixinWorkarounds { ++ ++ // mixins tries to find the owner of the clone() method, which doesn't exist and NPEs ++ public static long[] clone(final long[] values) { ++ return values.clone(); ++ } ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ef1c9e1e8636a14b5215c6c55d3032bacfd94cac +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java +@@ -0,0 +1,45 @@ ++package ca.spottedleaf.moonrise.common.util; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; ++ ++public final class MoonriseCommon { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(MoonriseCommon.class); ++ ++ // Paper start ++ public static PrioritisedThreadPool WORKER_POOL; ++ public static int WORKER_THREADS; ++ public static void init(io.papermc.paper.configuration.GlobalConfiguration.ChunkSystem chunkSystem) { ++ // Paper end ++ int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2; ++ if (defaultWorkerThreads <= 4) { ++ defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2; ++ } else { ++ defaultWorkerThreads = defaultWorkerThreads / 2; ++ } ++ defaultWorkerThreads = Integer.getInteger("Paper.WorkerThreadCount", Integer.valueOf(defaultWorkerThreads)); ++ ++ int workerThreads = chunkSystem.workerThreads; ++ ++ if (workerThreads <= 0) { ++ workerThreads = defaultWorkerThreads; ++ } ++ ++ WORKER_POOL = new PrioritisedThreadPool( ++ "Paper Worker Pool", workerThreads, ++ (final Thread thread, final Integer id) -> { ++ thread.setName("Paper Common Worker #" + id.intValue()); ++ thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { ++ @Override ++ public void uncaughtException(final Thread thread, final Throwable throwable) { ++ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable); ++ } ++ }); ++ }, (long)(20.0e6)); // 20ms ++ WORKER_THREADS = workerThreads; ++ } ++ ++ private MoonriseCommon() {} ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1cf32d7d1bbc8a0a3f7cb9024c793f6744199f64 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.common.util; ++ ++public final class MoonriseConstants { ++ ++ public static final int MAX_VIEW_DISTANCE = 32; ++ ++ private MoonriseConstants() {} ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e95cc73ddf20050aa4a241b0a309240e2bf46abd +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java +@@ -0,0 +1,54 @@ ++package ca.spottedleaf.moonrise.common.util; ++ ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.LevelHeightAccessor; ++ ++public final class WorldUtil { ++ ++ // min, max are inclusive ++ ++ public static int getMaxSection(final LevelHeightAccessor world) { ++ return world.getMaxSection() - 1; // getMaxSection() is exclusive ++ } ++ ++ public static int getMinSection(final LevelHeightAccessor world) { ++ return world.getMinSection(); ++ } ++ ++ public static int getMaxLightSection(final LevelHeightAccessor world) { ++ return getMaxSection(world) + 1; ++ } ++ ++ public static int getMinLightSection(final LevelHeightAccessor world) { ++ return getMinSection(world) - 1; ++ } ++ ++ ++ ++ public static int getTotalSections(final LevelHeightAccessor world) { ++ return getMaxSection(world) - getMinSection(world) + 1; ++ } ++ ++ public static int getTotalLightSections(final LevelHeightAccessor world) { ++ return getMaxLightSection(world) - getMinLightSection(world) + 1; ++ } ++ ++ public static int getMinBlockY(final LevelHeightAccessor world) { ++ return getMinSection(world) << 4; ++ } ++ ++ public static int getMaxBlockY(final LevelHeightAccessor world) { ++ return (getMaxSection(world) << 4) | 15; ++ } ++ ++ public static String getWorldName(final Level world) { ++ if (world == null) { ++ return "null world"; ++ } ++ return world.getWorld().getName(); ++ } ++ ++ private WorldUtil() { ++ throw new RuntimeException(); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..aef4fc0d3c272febe675d1ac846b88e58b4e7533 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java +@@ -0,0 +1,10 @@ ++package ca.spottedleaf.moonrise.patches.block_counting; ++ ++import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.ints.IntArrayList; ++ ++public interface BlockCountingBitStorage { ++ ++ public Int2ObjectOpenHashMap<IntArrayList> moonrise$countEntries(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a08ddb0598d44368af5b6bace971ee31edf9919e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java +@@ -0,0 +1,11 @@ ++package ca.spottedleaf.moonrise.patches.block_counting; ++ ++import ca.spottedleaf.moonrise.common.list.IBlockDataList; ++ ++public interface BlockCountingChunkSection { ++ ++ public int moonrise$getSpecialCollidingBlocks(); ++ ++ public IBlockDataList moonrise$getTickingBlockList(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java +new file mode 100644 +index 0000000000000000000000000000000000000000..08338917dc61c856eaba0b76e05c1497c458399d +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.chunk_getblock; ++ ++import net.minecraft.world.level.block.state.BlockState; ++ ++public interface GetBlockChunk { ++ ++ public BlockState moonrise$getBlock(final int x, final int y, final int z); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c2ff037e180393de6576f12c32c665ef640d6f50 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystem.java +@@ -0,0 +1,162 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk; ++import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; ++import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache; ++import com.mojang.logging.LogUtils; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import org.slf4j.Logger; ++import java.util.List; ++import java.util.function.Consumer; ++ ++public final class ChunkSystem { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) { ++ scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) { ++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkTask(chunkX, chunkZ, run, priority); ++ } ++ ++ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen, ++ final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority, ++ final Consumer<ChunkAccess> onComplete) { ++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete); ++ } ++ ++ // Paper - rewrite chunk system ++ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, ++ final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) { ++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ } ++ ++ public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, ++ final FullChunkStatus toStatus, final boolean addTicket, ++ final PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) { ++ ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ } ++ ++ public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) { ++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders(); ++ } ++ ++ public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) { ++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.getOldChunkHolders(); ++ } ++ ++ public static int getVisibleChunkHolderCount(final ServerLevel level) { ++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size(); ++ } ++ ++ public static int getUpdatingChunkHolderCount(final ServerLevel level) { ++ return ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().chunkHolderManager.size(); ++ } ++ ++ public static boolean hasAnyChunkHolders(final ServerLevel level) { ++ return getUpdatingChunkHolderCount(level) != 0; ++ } ++ ++ public static void onEntityPreAdd(final ServerLevel level, final Entity entity) { ++ // TODO move hook ++ io.papermc.paper.chunk.system.ChunkSystem.onEntityPreAdd(level, entity); ++ } ++ ++ public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) { ++ // TODO move hook ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderCreate(level, holder); ++ } ++ ++ public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { ++ // TODO move hook ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(level, holder); ++ } ++ ++ public static void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource()) ++ .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, chunk); ++ } ++ ++ public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ // TODO move hook ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(chunk, holder); ++ chunk.loadCallback(); // Paper ++ } ++ ++ public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ // TODO move hook ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(chunk, holder); ++ chunk.unloadCallback(); // Paper ++ } ++ ++ public static void onChunkPostNotBorder(final LevelChunk chunk, final ChunkHolder holder) { ++ ((ChunkSystemServerChunkCache)((ServerLevel)chunk.getLevel()).getChunkSource()) ++ .moonrise$setFullChunk(chunk.getPos().x, chunk.getPos().z, null); ++ } ++ ++ public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ // TODO move hook ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(chunk, holder); ++ if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) { ++ chunk.postProcessGeneration(); ++ } ++ ((ServerLevel)chunk.getLevel()).startTickingChunk(chunk); ++ ((ServerLevel)chunk.getLevel()).getChunkSource().chunkMap.tickingGenerated.incrementAndGet(); ++ } ++ ++ public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ // TODO move hook ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(chunk, holder); ++ } ++ ++ public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ // TODO move hook ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(chunk, holder); ++ } ++ ++ public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ // TODO move hook ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(chunk, holder); ++ } ++ ++ public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { ++ return null; ++ } ++ ++ public static int getSendViewDistance(final ServerPlayer player) { ++ return RegionizedPlayerChunkLoader.getAPISendViewDistance(player); ++ } ++ ++ public static int getLoadViewDistance(final ServerPlayer player) { ++ return RegionizedPlayerChunkLoader.getLoadViewDistance(player); ++ } ++ ++ public static int getTickViewDistance(final ServerPlayer player) { ++ return RegionizedPlayerChunkLoader.getAPITickViewDistance(player); ++ } ++ ++ public static void addPlayerToDistanceMaps(final ServerLevel world, final ServerPlayer player) { ++ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().addPlayer(player); ++ } ++ ++ public static void removePlayerFromDistanceMaps(final ServerLevel world, final ServerPlayer player) { ++ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().removePlayer(player); ++ } ++ ++ public static void updateMaps(final ServerLevel world, final ServerPlayer player) { ++ ((ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player); ++ } ++ ++ private ChunkSystem() {} ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java +new file mode 100644 +index 0000000000000000000000000000000000000000..49160a30b8e19e5c5ada811fbcae2a05959524f3 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java +@@ -0,0 +1,38 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system; ++ ++import net.minecraft.SharedConstants; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.Tag; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.datafix.DataFixTypes; ++ ++public final class ChunkSystemConverters { ++ ++ // See SectionStorage#getVersion ++ private static final int DEFAULT_POI_DATA_VERSION = 1945; ++ ++ private static final int DEFAULT_ENTITY_CHUNK_DATA_VERSION = -1; ++ ++ private static int getCurrentVersion() { ++ return SharedConstants.getCurrentVersion().getDataVersion().getVersion(); ++ } ++ ++ private static int getDataVersion(final CompoundTag data, final int dfl) { ++ return !data.contains(SharedConstants.DATA_VERSION_TAG, Tag.TAG_ANY_NUMERIC) ++ ? dfl : data.getInt(SharedConstants.DATA_VERSION_TAG); ++ } ++ ++ public static CompoundTag convertPoiCompoundTag(final CompoundTag data, final ServerLevel world) { ++ final int dataVersion = getDataVersion(data, DEFAULT_POI_DATA_VERSION); ++ ++ return DataFixTypes.POI_CHUNK.update(world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion()); ++ } ++ ++ public static CompoundTag convertEntityChunkCompoundTag(final CompoundTag data, final ServerLevel world) { ++ final int dataVersion = getDataVersion(data, DEFAULT_ENTITY_CHUNK_DATA_VERSION); ++ ++ return DataFixTypes.ENTITY_CHUNK.update(world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion()); ++ } ++ ++ private ChunkSystemConverters() {} ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java +new file mode 100644 +index 0000000000000000000000000000000000000000..67f6dd9a4855611cfe242c2e37e90f6d27d4c823 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java +@@ -0,0 +1,36 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.ChunkAccess; ++ ++public final class ChunkSystemFeatures { ++ ++ public static boolean supportsAsyncChunkSave() { ++ // uncertain how to properly pass AsyncSaveData to ChunkSerializer#write ++ // additionally, there may be mods hooking into the write() call which may not be thread-safe to call ++ return true; ++ } ++ ++ public static AsyncChunkSaveData getAsyncSaveData(final ServerLevel world, final ChunkAccess chunk) { ++ return net.minecraft.world.level.chunk.storage.ChunkSerializer.getAsyncSaveData(world, chunk); ++ } ++ ++ public static CompoundTag saveChunkAsync(final ServerLevel world, final ChunkAccess chunk, final AsyncChunkSaveData asyncSaveData) { ++ return net.minecraft.world.level.chunk.storage.ChunkSerializer.saveChunk(world, chunk, asyncSaveData); ++ } ++ ++ public static boolean forceNoSave(final ChunkAccess chunk) { ++ // support for CB chunk mustNotSave ++ return chunk instanceof net.minecraft.world.level.chunk.LevelChunk levelChunk && levelChunk.mustNotSave; ++ } ++ ++ public static boolean supportsAsyncChunkDeserialization() { ++ // as it stands, the current problem with supporting this in Moonrise is that we are unsure that any mods ++ // hooking into ChunkSerializer#read() are thread-safe to call ++ return true; ++ } ++ ++ private ChunkSystemFeatures() {} ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..becd1c6d54ed6c912aee3a9178a970e2751d3694 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java +@@ -0,0 +1,11 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.async_save; ++ ++import net.minecraft.nbt.ListTag; ++import net.minecraft.nbt.Tag; ++ ++public record AsyncChunkSaveData( ++ Tag blockTickList, // non-null if we had to go to the server's tick list ++ Tag fluidTickList, // non-null if we had to go to the server's tick list ++ ListTag blockEntities, ++ long worldTime ++) {} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2c279854bdf214538380fa354e4298ec4bd9ac4e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java +@@ -0,0 +1,39 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.entity; ++ ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.monster.Shulker; ++import net.minecraft.world.entity.vehicle.AbstractMinecart; ++import net.minecraft.world.entity.vehicle.Boat; ++ ++public interface ChunkSystemEntity { ++ ++ public boolean moonrise$isHardColliding(); ++ ++ // for mods to override ++ public default boolean moonrise$isHardCollidingUncached() { ++ return this instanceof Boat || this instanceof AbstractMinecart || this instanceof Shulker || ((Entity)this).canBeCollidedWith(); ++ } ++ ++ public FullChunkStatus moonrise$getChunkStatus(); ++ ++ public void moonrise$setChunkStatus(final FullChunkStatus status); ++ ++ public int moonrise$getSectionX(); ++ ++ public void moonrise$setSectionX(final int x); ++ ++ public int moonrise$getSectionY(); ++ ++ public void moonrise$setSectionY(final int y); ++ ++ public int moonrise$getSectionZ(); ++ ++ public void moonrise$setSectionZ(final int z); ++ ++ public boolean moonrise$isUpdatingSectionStatus(); ++ ++ public void moonrise$setUpdatingSectionStatus(final boolean to); ++ ++ public boolean moonrise$hasAnyPlayerPassengers(); ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..73df26b27146bbad2106d57b22dd3c792ed3dd1d +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java +@@ -0,0 +1,14 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.io; ++ ++import net.minecraft.world.level.chunk.storage.RegionFile; ++import java.io.IOException; ++ ++public interface ChunkSystemRegionFileStorage { ++ ++ public boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ); ++ ++ public RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ); ++ ++ public RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c833f78d083b8f661087471c35bc90f65af1b525 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java +@@ -0,0 +1,1239 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.io; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedQueueExecutorThread; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue; ++import ca.spottedleaf.concurrentutil.function.BiLong1Function; ++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.storage.RegionFile; ++import net.minecraft.world.level.chunk.storage.RegionFileStorage; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; ++import java.io.IOException; ++import java.lang.invoke.VarHandle; ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.CompletionException; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.function.BiConsumer; ++import java.util.function.Consumer; ++import java.util.function.Function; ++ ++/** ++ * Prioritised RegionFile I/O executor, responsible for all RegionFile access. ++ * <p> ++ * All functions provided are MT-Safe, however certain ordering constraints are recommended: ++ * <li> ++ * Chunk saves may not occur for unloaded chunks. ++ * </li> ++ * <li> ++ * Tasks must be scheduled on the chunk scheduler thread. ++ * </li> ++ * By following these constraints, no chunk data loss should occur with the exception of underlying I/O problems. ++ * </p> ++ */ ++public final class RegionFileIOThread extends PrioritisedQueueExecutorThread { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(RegionFileIOThread.class); ++ ++ /** ++ * The kinds of region files controlled by the region file thread. Add more when needed, and ensure ++ * getControllerFor is updated. ++ */ ++ public static enum RegionFileType { ++ CHUNK_DATA, ++ POI_DATA, ++ ENTITY_DATA; ++ } ++ ++ private static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values(); ++ ++ public static ChunkDataController getControllerFor(final ServerLevel world, final RegionFileType type) { ++ switch (type) { ++ case CHUNK_DATA: ++ return ((ChunkSystemServerLevel)world).moonrise$getChunkDataController(); ++ case POI_DATA: ++ return ((ChunkSystemServerLevel)world).moonrise$getPoiChunkDataController(); ++ case ENTITY_DATA: ++ return ((ChunkSystemServerLevel)world).moonrise$getEntityChunkDataController(); ++ default: ++ throw new IllegalStateException("Unknown controller type " + type); ++ } ++ } ++ ++ /** ++ * Collects regionfile data for a certain chunk. ++ */ ++ public static final class RegionFileData { ++ ++ private final boolean[] hasResult = new boolean[CACHED_REGIONFILE_TYPES.length]; ++ private final CompoundTag[] data = new CompoundTag[CACHED_REGIONFILE_TYPES.length]; ++ private final Throwable[] throwables = new Throwable[CACHED_REGIONFILE_TYPES.length]; ++ ++ /** ++ * Sets the result associated with the specified regionfile type. Note that ++ * results can only be set once per regionfile type. ++ * ++ * @param type The regionfile type. ++ * @param data The result to set. ++ */ ++ public void setData(final RegionFileType type, final CompoundTag data) { ++ final int index = type.ordinal(); ++ ++ if (this.hasResult[index]) { ++ throw new IllegalArgumentException("Result already exists for type " + type); ++ } ++ this.hasResult[index] = true; ++ this.data[index] = data; ++ } ++ ++ /** ++ * Sets the result associated with the specified regionfile type. Note that ++ * results can only be set once per regionfile type. ++ * ++ * @param type The regionfile type. ++ * @param throwable The result to set. ++ */ ++ public void setThrowable(final RegionFileType type, final Throwable throwable) { ++ final int index = type.ordinal(); ++ ++ if (this.hasResult[index]) { ++ throw new IllegalArgumentException("Result already exists for type " + type); ++ } ++ this.hasResult[index] = true; ++ this.throwables[index] = throwable; ++ } ++ ++ /** ++ * Returns whether there is a result for the specified regionfile type. ++ * ++ * @param type Specified regionfile type. ++ * ++ * @return Whether a result exists for {@code type}. ++ */ ++ public boolean hasResult(final RegionFileType type) { ++ return this.hasResult[type.ordinal()]; ++ } ++ ++ /** ++ * Returns the data result for the regionfile type. ++ * ++ * @param type Specified regionfile type. ++ * ++ * @throws IllegalArgumentException If the result has not been set for {@code type}. ++ * @return The data result for the specified type. If the result is a {@code Throwable}, ++ * then returns {@code null}. ++ */ ++ public CompoundTag getData(final RegionFileType type) { ++ final int index = type.ordinal(); ++ ++ if (!this.hasResult[index]) { ++ throw new IllegalArgumentException("Result does not exist for type " + type); ++ } ++ ++ return this.data[index]; ++ } ++ ++ /** ++ * Returns the throwable result for the regionfile type. ++ * ++ * @param type Specified regionfile type. ++ * ++ * @throws IllegalArgumentException If the result has not been set for {@code type}. ++ * @return The throwable result for the specified type. If the result is an {@code CompoundTag}, ++ * then returns {@code null}. ++ */ ++ public Throwable getThrowable(final RegionFileType type) { ++ final int index = type.ordinal(); ++ ++ if (!this.hasResult[index]) { ++ throw new IllegalArgumentException("Result does not exist for type " + type); ++ } ++ ++ return this.throwables[index]; ++ } ++ } ++ ++ private static final Object INIT_LOCK = new Object(); ++ ++ static RegionFileIOThread[] threads; ++ ++ /* needs to be consistent given a set of parameters */ ++ static RegionFileIOThread selectThread(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ if (threads == null) { ++ throw new IllegalStateException("Threads not initialised"); ++ } ++ ++ final int regionX = chunkX >> 5; ++ final int regionZ = chunkZ >> 5; ++ final int typeOffset = type.ordinal(); ++ ++ return threads[(System.identityHashCode(world) + regionX + regionZ + typeOffset) % threads.length]; ++ } ++ ++ /** ++ * Shuts down the I/O executor(s). Watis for all tasks to complete if specified. ++ * Tasks queued during this call might not be accepted, and tasks queued after will not be accepted. ++ * ++ * @param wait Whether to wait until all tasks have completed. ++ */ ++ public static void close(final boolean wait) { ++ for (int i = 0, len = threads.length; i < len; ++i) { ++ threads[i].close(false, true); ++ } ++ if (wait) { ++ RegionFileIOThread.flush(); ++ } ++ } ++ ++ public static long[] getExecutedTasks() { ++ final long[] ret = new long[threads.length]; ++ for (int i = 0, len = threads.length; i < len; ++i) { ++ ret[i] = threads[i].getTotalTasksExecuted(); ++ } ++ ++ return ret; ++ } ++ ++ public static long[] getTasksScheduled() { ++ final long[] ret = new long[threads.length]; ++ for (int i = 0, len = threads.length; i < len; ++i) { ++ ret[i] = threads[i].getTotalTasksScheduled(); ++ } ++ return ret; ++ } ++ ++ public static void flush() { ++ for (int i = 0, len = threads.length; i < len; ++i) { ++ threads[i].waitUntilAllExecuted(); ++ } ++ } ++ ++ public static void flushRegionStorages(final ServerLevel world) throws IOException { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ getControllerFor(world, type).getCache().flush(); ++ } ++ } ++ ++ public static void partialFlush(final int totalTasksRemaining) { ++ long failures = 1L; // start out at 0.25ms ++ ++ for (;;) { ++ final long[] executed = getExecutedTasks(); ++ final long[] scheduled = getTasksScheduled(); ++ ++ long sum = 0; ++ for (int i = 0; i < executed.length; ++i) { ++ sum += scheduled[i] - executed[i]; ++ } ++ ++ if (sum <= totalTasksRemaining) { ++ break; ++ } ++ ++ failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms ++ } ++ } ++ ++ /** ++ * Inits the executor with the specified number of threads. ++ * ++ * @param threads Specified number of threads. ++ */ ++ public static void init(final int threads) { ++ synchronized (INIT_LOCK) { ++ if (RegionFileIOThread.threads != null) { ++ throw new IllegalStateException("Already initialised threads"); ++ } ++ ++ RegionFileIOThread.threads = new RegionFileIOThread[threads]; ++ ++ for (int i = 0; i < threads; ++i) { ++ RegionFileIOThread.threads[i] = new RegionFileIOThread(i); ++ RegionFileIOThread.threads[i].start(); ++ } ++ } ++ } ++ ++ public static void deinit() { ++ if (true) { // Paper ++ // TODO does this cause issues with mods? how to implement ++ close(true); ++ synchronized (INIT_LOCK) { ++ RegionFileIOThread.threads = null; ++ } ++ } else { RegionFileIOThread.flush(); } ++ } ++ ++ private RegionFileIOThread(final int threadNumber) { ++ super(new PrioritisedThreadedTaskQueue(), (int)(1.0e6)); // 1.0ms spinwait time ++ this.setName("RegionFile I/O Thread #" + threadNumber); ++ this.setPriority(Thread.NORM_PRIORITY - 2); // we keep priority close to normal because threads can wait on us ++ this.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> { ++ LOGGER.error("Uncaught exception thrown from I/O thread, report this! Thread: " + thread.getName(), thr); ++ }); ++ } ++ ++ /** ++ * Returns whether the current thread is a regionfile I/O executor. ++ * @return Whether the current thread is a regionfile I/O executor. ++ */ ++ public static boolean isRegionFileThread() { ++ return Thread.currentThread() instanceof RegionFileIOThread; ++ } ++ ++ /** ++ * Returns the priority associated with blocking I/O based on the current thread. The goal is to avoid ++ * dumb plugins from taking away priority from threads we consider crucial. ++ * @return The priroity to use with blocking I/O on the current thread. ++ */ ++ public static Priority getIOBlockingPriorityForCurrentThread() { ++ if (io.papermc.paper.util.TickThread.isTickThread()) { ++ return Priority.BLOCKING; ++ } ++ return Priority.HIGHEST; ++ } ++ ++ /** ++ * Returns the current {@code CompoundTag} pending for write for the specified chunk & regionfile type. ++ * Note that this does not copy the result, so do not modify the result returned. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * ++ * @return The compound tag associated for the specified chunk. {@code null} if no write was pending, or if {@code null} is the write pending. ++ */ ++ public static CompoundTag getPendingWrite(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ return thread.getPendingWriteInternal(world, chunkX, chunkZ, type); ++ } ++ ++ CompoundTag getPendingWriteInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ final ChunkDataController taskController = getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (task == null) { ++ return null; ++ } ++ ++ final CompoundTag ret = task.inProgressWrite; ++ ++ return ret == ChunkDataTask.NOTHING_TO_WRITE ? null : ret; ++ } ++ ++ /** ++ * Returns the priority for the specified regionfile type for the specified chunk. ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @return The priority for the chunk ++ */ ++ public static Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ return thread.getPriorityInternal(world, chunkX, chunkZ, type); ++ } ++ ++ Priority getPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ final ChunkDataController taskController = getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (task == null) { ++ return Priority.COMPLETING; ++ } ++ ++ return task.prioritisedTask.getPriority(); ++ } ++ ++ /** ++ * Sets the priority for all regionfile types for the specified chunk. Note that great care should ++ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different ++ * priorities. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ RegionFileIOThread.setPriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Sets the priority for the specified regionfile type for the specified chunk. Note that great care should ++ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different ++ * priorities. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ thread.setPriorityInternal(world, chunkX, chunkZ, type, priority); ++ } ++ ++ void setPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) { ++ final ChunkDataController taskController = getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (task != null) { ++ task.prioritisedTask.setPriority(priority); ++ } ++ } ++ ++ /** ++ * Raises the priority for all regionfile types for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ RegionFileIOThread.raisePriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Raises the priority for the specified regionfile type for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ thread.raisePriorityInternal(world, chunkX, chunkZ, type, priority); ++ } ++ ++ void raisePriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) { ++ final ChunkDataController taskController = getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (task != null) { ++ task.prioritisedTask.raisePriority(priority); ++ } ++ } ++ ++ /** ++ * Lowers the priority for all regionfile types for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ RegionFileIOThread.lowerPriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Lowers the priority for the specified regionfile type for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ thread.lowerPriorityInternal(world, chunkX, chunkZ, type, priority); ++ } ++ ++ void lowerPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) { ++ final ChunkDataController taskController = getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ if (task != null) { ++ task.prioritisedTask.lowerPriority(priority); ++ } ++ } ++ ++ /** ++ * Schedules the chunk data to be written asynchronously. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means ++ * saves must be scheduled before a chunk is unloaded. ++ * </li> ++ * <li> ++ * Writes may be called concurrently, although only the "later" write will go through. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param data Chunk's data ++ * @param type The regionfile type to write to. ++ * ++ * @throws IllegalStateException If the file io thread has shutdown. ++ */ ++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, ++ final RegionFileType type) { ++ RegionFileIOThread.scheduleSave(world, chunkX, chunkZ, data, type, Priority.NORMAL); ++ } ++ ++ /** ++ * Schedules the chunk data to be written asynchronously. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means ++ * saves must be scheduled before a chunk is unloaded. ++ * </li> ++ * <li> ++ * Writes may be called concurrently, although only the "later" write will go through. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param data Chunk's data ++ * @param type The regionfile type to write to. ++ * @param priority The minimum priority to schedule at. ++ * ++ * @throws IllegalStateException If the file io thread has shutdown. ++ */ ++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, ++ final RegionFileType type, final Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ thread.scheduleSaveInternal(world, chunkX, chunkZ, data, type, priority); ++ } ++ ++ void scheduleSaveInternal(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, ++ final RegionFileType type, final Priority priority) { ++ final ChunkDataController taskController = getControllerFor(world, type); ++ ++ final boolean[] created = new boolean[1]; ++ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final ChunkDataTask task = taskController.tasks.compute(key, (final long keyInMap, final ChunkDataTask taskRunning) -> { ++ if (taskRunning == null || taskRunning.failedWrite) { ++ // no task is scheduled or the previous write failed - meaning we need to overwrite it ++ ++ // create task ++ final ChunkDataTask newTask = new ChunkDataTask(world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority); ++ newTask.inProgressWrite = data; ++ created[0] = true; ++ ++ return newTask; ++ } ++ ++ taskRunning.inProgressWrite = data; ++ ++ return taskRunning; ++ }); ++ ++ if (created[0]) { ++ task.prioritisedTask.queue(); ++ } else { ++ task.prioritisedTask.raisePriority(priority); ++ } ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call ++ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} ++ * for single load. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ */ ++ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock) { ++ return RegionFileIOThread.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call ++ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} ++ * for single load. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param priority The minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ */ ++ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, ++ final Priority priority) { ++ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and ++ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} ++ * for single load. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param types The regionfile type(s) to load. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, ++ final RegionFileType... types) { ++ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL, types); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and ++ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} ++ * for single load. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param types The regionfile type(s) to load. ++ * @param priority The minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock, ++ final Priority priority, final RegionFileType... types) { ++ if (types == null) { ++ throw new NullPointerException("Types cannot be null"); ++ } ++ if (types.length == 0) { ++ throw new IllegalArgumentException("Types cannot be empty"); ++ } ++ ++ final RegionFileData ret = new RegionFileData(); ++ ++ final Cancellable[] reads = new CancellableRead[types.length]; ++ final AtomicInteger completions = new AtomicInteger(); ++ final int expectedCompletions = types.length; ++ ++ for (int i = 0; i < expectedCompletions; ++i) { ++ final RegionFileType type = types[i]; ++ reads[i] = RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, ++ (final CompoundTag data, final Throwable throwable) -> { ++ if (throwable != null) { ++ ret.setThrowable(type, throwable); ++ } else { ++ ret.setData(type, data); ++ } ++ ++ if (completions.incrementAndGet() == expectedCompletions) { ++ onComplete.accept(ret); ++ } ++ }, intendingToBlock, priority); ++ } ++ ++ return new CancellableReads(reads); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call ++ * {@code onComplete}. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete, ++ final boolean intendingToBlock) { ++ return RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, Priority.NORMAL); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call ++ * {@code onComplete}. ++ * <p> ++ * Impl notes: ++ * </p> ++ * <li> ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ * </li> ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param priority Minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete, ++ final boolean intendingToBlock, final Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ return thread.loadDataAsyncInternal(world, chunkX, chunkZ, type, onComplete, intendingToBlock, priority); ++ } ++ ++ Cancellable loadDataAsyncInternal(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete, ++ final boolean intendingToBlock, final Priority priority) { ++ final ChunkDataController taskController = getControllerFor(world, type); ++ ++ final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion(); ++ ++ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final BiLong1Function<ChunkDataTask, ChunkDataTask> compute = (final long keyInMap, final ChunkDataTask running) -> { ++ if (running == null) { ++ // not scheduled ++ ++ // set up task ++ final ChunkDataTask newTask = new ChunkDataTask( ++ world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority ++ ); ++ newTask.inProgressRead = new InProgressRead(); ++ newTask.inProgressRead.addToAsyncWaiters(onComplete); ++ ++ callbackInfo.tasksNeedsScheduling = true; ++ return newTask; ++ } ++ ++ final CompoundTag pendingWrite = running.inProgressWrite; ++ ++ if (pendingWrite == ChunkDataTask.NOTHING_TO_WRITE) { ++ // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations ++ if (!running.inProgressRead.addToAsyncWaiters(onComplete)) { ++ callbackInfo.data = running.inProgressRead.value; ++ callbackInfo.throwable = running.inProgressRead.throwable; ++ callbackInfo.completeNow = true; ++ } ++ return running; ++ } ++ ++ // at this stage we have to use the in progress write's data to avoid an order issue ++ callbackInfo.data = pendingWrite; ++ callbackInfo.throwable = null; ++ callbackInfo.completeNow = true; ++ return running; ++ }; ++ ++ final ChunkDataTask ret = taskController.tasks.compute(key, compute); ++ ++ // needs to be scheduled ++ if (callbackInfo.tasksNeedsScheduling) { ++ ret.prioritisedTask.queue(); ++ } else if (callbackInfo.completeNow) { ++ try { ++ onComplete.accept(callbackInfo.data == null ? null : callbackInfo.data.copy(), callbackInfo.throwable); ++ } catch (final Throwable thr) { ++ LOGGER.error("Callback " + ConcurrentUtil.genericToString(onComplete) + " synchronously failed to handle chunk data for task " + ret.toString(), thr); ++ } ++ } else { ++ // we're waiting on a task we didn't schedule, so raise its priority to what we want ++ ret.prioritisedTask.raisePriority(priority); ++ } ++ ++ return new CancellableRead(onComplete, ret); ++ } ++ ++ /** ++ * Schedules a load task to be executed asynchronously, and blocks on that task. ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param type Regionfile type ++ * @param priority Minimum priority to load the data at. ++ * ++ * @return The chunk data for the chunk. Note that a {@code null} result means the chunk or regionfile does not exist on disk. ++ * ++ * @throws IOException If the load fails for any reason ++ */ ++ public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final Priority priority) throws IOException { ++ final CompletableFuture<CompoundTag> ret = new CompletableFuture<>(); ++ ++ RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> { ++ if (thr != null) { ++ ret.completeExceptionally(thr); ++ } else { ++ ret.complete(compound); ++ } ++ }, true, priority); ++ ++ try { ++ return ret.join(); ++ } catch (final CompletionException ex) { ++ throw new IOException(ex); ++ } ++ } ++ ++ private static final class ImmediateCallbackCompletion { ++ ++ public CompoundTag data; ++ public Throwable throwable; ++ public boolean completeNow; ++ public boolean tasksNeedsScheduling; ++ ++ } ++ ++ private static final class CancellableRead implements Cancellable { ++ ++ private BiConsumer<CompoundTag, Throwable> callback; ++ private ChunkDataTask task; ++ ++ CancellableRead(final BiConsumer<CompoundTag, Throwable> callback, final ChunkDataTask task) { ++ this.callback = callback; ++ this.task = task; ++ } ++ ++ @Override ++ public boolean cancel() { ++ final BiConsumer<CompoundTag, Throwable> callback = this.callback; ++ final ChunkDataTask task = this.task; ++ ++ if (callback == null || task == null) { ++ return false; ++ } ++ ++ this.callback = null; ++ this.task = null; ++ ++ final InProgressRead read = task.inProgressRead; ++ ++ // read can be null if no read was scheduled (i.e no regionfile existed or chunk in regionfile didn't) ++ return read != null && read.cancel(callback); ++ } ++ } ++ ++ private static final class CancellableReads implements Cancellable { ++ ++ private Cancellable[] reads; ++ ++ private static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class); ++ ++ CancellableReads(final Cancellable[] reads) { ++ this.reads = reads; ++ } ++ ++ @Override ++ public boolean cancel() { ++ final Cancellable[] reads = (Cancellable[])READS_HANDLE.getAndSet((CancellableReads)this, (Cancellable[])null); ++ ++ if (reads == null) { ++ return false; ++ } ++ ++ boolean ret = false; ++ ++ for (final Cancellable read : reads) { ++ ret |= read.cancel(); ++ } ++ ++ return ret; ++ } ++ } ++ ++ private static final class InProgressRead { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(InProgressRead.class); ++ ++ private CompoundTag value; ++ private Throwable throwable; ++ private final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> callbacks = new MultiThreadedQueue<>(); ++ ++ public boolean hasNoWaiters() { ++ return this.callbacks.isEmpty(); ++ } ++ ++ public boolean addToAsyncWaiters(final BiConsumer<CompoundTag, Throwable> callback) { ++ return this.callbacks.add(callback); ++ } ++ ++ public boolean cancel(final BiConsumer<CompoundTag, Throwable> callback) { ++ return this.callbacks.remove(callback); ++ } ++ ++ public void complete(final ChunkDataTask task, final CompoundTag value, final Throwable throwable) { ++ this.value = value; ++ this.throwable = throwable; ++ ++ BiConsumer<CompoundTag, Throwable> consumer; ++ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) { ++ try { ++ consumer.accept(value == null ? null : value.copy(), throwable); ++ } catch (final Throwable thr) { ++ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data for task " + task.toString(), thr); ++ } ++ } ++ } ++ } ++ ++ public static abstract class ChunkDataController { ++ ++ // ConcurrentHashMap synchronizes per chain, so reduce the chance of task's hashes colliding. ++ private final ConcurrentLong2ReferenceChainedHashTable<ChunkDataTask> tasks = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(8192, 0.5f); ++ ++ public final RegionFileType type; ++ ++ public ChunkDataController(final RegionFileType type) { ++ this.type = type; ++ } ++ ++ public abstract RegionFileStorage getCache(); ++ ++ public abstract void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException; ++ ++ public abstract CompoundTag readData(final int chunkX, final int chunkZ) throws IOException; ++ ++ public boolean hasTasks() { ++ return !this.tasks.isEmpty(); ++ } ++ ++ public boolean doesRegionFileNotExist(final int chunkX, final int chunkZ) { ++ return ((ChunkSystemRegionFileStorage)(Object)this.getCache()).moonrise$doesRegionFileNotExistNoIO(chunkX, chunkZ); ++ } ++ ++ public <T> T computeForRegionFile(final int chunkX, final int chunkZ, final boolean existingOnly, final Function<RegionFile, T> function) { ++ final RegionFileStorage cache = this.getCache(); ++ final RegionFile regionFile; ++ synchronized (cache) { ++ try { ++ if (existingOnly) { ++ regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfExists(chunkX, chunkZ); ++ } else { ++ regionFile = cache.getRegionFile(new ChunkPos(chunkX, chunkZ), existingOnly); ++ } ++ } catch (final IOException ex) { ++ throw new RuntimeException(ex); ++ } ++ ++ return function.apply(regionFile); ++ } ++ } ++ ++ public <T> T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function<RegionFile, T> function) { ++ final RegionFileStorage cache = this.getCache(); ++ final RegionFile regionFile; ++ ++ synchronized (cache) { ++ regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfLoaded(chunkX, chunkZ); ++ ++ return function.apply(regionFile); ++ } ++ } ++ } ++ ++ private static final class ChunkDataTask implements Runnable { ++ ++ private static final CompoundTag NOTHING_TO_WRITE = new CompoundTag(); ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkDataTask.class); ++ ++ private InProgressRead inProgressRead; ++ private volatile CompoundTag inProgressWrite = NOTHING_TO_WRITE; // only needs to be acquire/release ++ ++ private boolean failedWrite; ++ ++ private final ServerLevel world; ++ private final int chunkX; ++ private final int chunkZ; ++ private final ChunkDataController taskController; ++ ++ private final PrioritisedTask prioritisedTask; ++ ++ /* ++ * IO thread will perform reads before writes for a given chunk x and z ++ * ++ * How reads/writes are scheduled: ++ * ++ * If read is scheduled while scheduling write, take no special action and just schedule write ++ * If read is scheduled while scheduling read and no write is scheduled, chain the read task ++ * ++ * ++ * If write is scheduled while scheduling read, use the pending write data and ret immediately (so no read is scheduled) ++ * If write is scheduled while scheduling write (ignore read in progress), overwrite the write in progress data ++ * ++ * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however ++ * it fails to properly propagate write failures thanks to writes overwriting each other ++ */ ++ ++ public ChunkDataTask(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkDataController taskController, ++ final PrioritisedExecutor executor, final Priority priority) { ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.taskController = taskController; ++ this.prioritisedTask = executor.createTask(this, priority); ++ } ++ ++ @Override ++ public String toString() { ++ return "Task for world: '" + WorldUtil.getWorldName(this.world) + "' at (" + this.chunkX + "," + this.chunkZ + ++ ") type: " + this.taskController.type.name() + ", hash: " + this.hashCode(); ++ } ++ ++ @Override ++ public void run() { ++ final InProgressRead read = this.inProgressRead; ++ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ); ++ ++ if (read != null) { ++ final boolean[] canRead = new boolean[] { true }; ++ ++ if (read.hasNoWaiters()) { ++ // cancelled read? go to task controller to confirm ++ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> { ++ if (valueInMap == null) { ++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); ++ } ++ if (valueInMap != ChunkDataTask.this) { ++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); ++ } ++ ++ if (!read.hasNoWaiters()) { ++ return valueInMap; ++ } else { ++ canRead[0] = false; ++ } ++ ++ return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap; ++ }); ++ ++ if (inMap == null) { ++ // read is cancelled - and no write pending, so we're done ++ return; ++ } ++ // if there is a write in progress, we don't actually have to worry about waiters gaining new entries - ++ // the readers will just use the in progress write, so the value in canRead is good to use without ++ // further synchronisation. ++ } ++ ++ if (canRead[0]) { ++ CompoundTag compound = null; ++ Throwable throwable = null; ++ ++ try { ++ compound = this.taskController.readData(this.chunkX, this.chunkZ); ++ } catch (final Throwable thr) { ++ throwable = thr; ++ LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr); ++ } ++ read.complete(this, compound, throwable); ++ } ++ } ++ ++ CompoundTag write = this.inProgressWrite; ++ ++ if (write == NOTHING_TO_WRITE) { ++ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> { ++ if (valueInMap == null) { ++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); ++ } ++ if (valueInMap != ChunkDataTask.this) { ++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); ++ } ++ return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap; ++ }); ++ ++ if (inMap == null) { ++ return; // set the task value to null, indicating we're done ++ } // else: inProgressWrite changed, so now we have something to write ++ } ++ ++ for (;;) { ++ write = this.inProgressWrite; ++ final CompoundTag dataWritten = write; ++ ++ boolean failedWrite = false; ++ ++ try { ++ this.taskController.writeData(this.chunkX, this.chunkZ, write); ++ } catch (final Throwable thr) { ++ if (thr instanceof RegionFileStorage.RegionFileSizeException) { ++ final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024); ++ LOGGER.error("Chunk at (" + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk."); ++ } else { ++ failedWrite = thr instanceof IOException; ++ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr); ++ } ++ } ++ ++ final boolean finalFailWrite = failedWrite; ++ final boolean[] done = new boolean[] { false }; ++ ++ this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> { ++ if (valueInMap == null) { ++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); ++ } ++ if (valueInMap != ChunkDataTask.this) { ++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); ++ } ++ if (valueInMap.inProgressWrite == dataWritten) { ++ valueInMap.failedWrite = finalFailWrite; ++ done[0] = true; ++ // keep the data in map if we failed the write so we can try to prevent data loss ++ return finalFailWrite ? valueInMap : null; ++ } ++ // different data than expected, means we need to retry write ++ return valueInMap; ++ }); ++ ++ if (done[0]) { ++ return; ++ } ++ ++ // fetch & write new data ++ continue; ++ } ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c35e0c29700be48dda3e53e7d2db224766ef17b7 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java +@@ -0,0 +1,56 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkStorage; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.storage.RegionFileStorage; ++import java.io.IOException; ++import java.util.Optional; ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.CompletionException; ++ ++public final class ChunkDataController extends RegionFileIOThread.ChunkDataController { ++ ++ private final ServerLevel world; ++ ++ public ChunkDataController(final ServerLevel world) { ++ super(RegionFileIOThread.RegionFileType.CHUNK_DATA); ++ this.world = world; ++ } ++ ++ @Override ++ public RegionFileStorage getCache() { ++ return ((ChunkSystemChunkStorage)this.world.getChunkSource().chunkMap).moonrise$getRegionStorage(); ++ } ++ ++ @Override ++ public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { ++ final CompletableFuture<Void> future = this.world.getChunkSource().chunkMap.write(new ChunkPos(chunkX, chunkZ), compound); ++ ++ try { ++ if (future != null) { ++ // rets non-null when sync writing (i.e. future should be completed here) ++ future.join(); ++ } ++ } catch (final CompletionException ex) { ++ if (ex.getCause() instanceof IOException ioException) { ++ throw ioException; ++ } ++ throw ex; ++ } ++ } ++ ++ @Override ++ public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException { ++ try { ++ return this.world.getChunkSource().chunkMap.read(new ChunkPos(chunkX, chunkZ)).join().orElse(null); ++ } catch (final CompletionException ex) { ++ if (ex.getCause() instanceof IOException ioException) { ++ throw ioException; ++ } ++ throw ex; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fdd189ef056187941d43809c5d61cab717aecf60 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java +@@ -0,0 +1,55 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.storage.EntityStorage; ++import net.minecraft.world.level.chunk.storage.RegionFileStorage; ++import net.minecraft.world.level.chunk.storage.RegionStorageInfo; ++import java.io.IOException; ++import java.nio.file.Path; ++ ++public final class EntityDataController extends RegionFileIOThread.ChunkDataController { ++ ++ private final EntityRegionFileStorage storage; ++ ++ public EntityDataController(final EntityRegionFileStorage storage) { ++ super(RegionFileIOThread.RegionFileType.ENTITY_DATA); ++ this.storage = storage; ++ } ++ ++ @Override ++ public RegionFileStorage getCache() { ++ return this.storage; ++ } ++ ++ @Override ++ public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { ++ this.storage.write(new ChunkPos(chunkX, chunkZ), compound); ++ } ++ ++ @Override ++ public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException { ++ return this.storage.read(new ChunkPos(chunkX, chunkZ)); ++ } ++ ++ public static final class EntityRegionFileStorage extends RegionFileStorage { ++ ++ public EntityRegionFileStorage(final RegionStorageInfo regionStorageInfo, final Path directory, ++ final boolean dsync) { ++ super(regionStorageInfo, directory, dsync); ++ } ++ ++ @Override ++ public void write(final ChunkPos pos, final CompoundTag nbt) throws IOException { ++ final ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt); ++ if (nbtPos != null && !pos.equals(nbtPos)) { ++ throw new IllegalArgumentException( ++ "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString() ++ + " but compound says coordinate is " + nbtPos + " for world: " + this ++ ); ++ } ++ super.write(pos, nbt); ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java +new file mode 100644 +index 0000000000000000000000000000000000000000..af867f8fedd0bb8f675e94243aa1a3f17363483b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java +@@ -0,0 +1,33 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.storage.ChunkSystemSectionStorage; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.storage.RegionFileStorage; ++import java.io.IOException; ++ ++public final class PoiDataController extends RegionFileIOThread.ChunkDataController { ++ ++ private final ServerLevel world; ++ ++ public PoiDataController(final ServerLevel world) { ++ super(RegionFileIOThread.RegionFileType.POI_DATA); ++ this.world = world; ++ } ++ ++ @Override ++ public RegionFileStorage getCache() { ++ return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$getRegionStorage(); ++ } ++ ++ @Override ++ public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException { ++ ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$write(chunkX, chunkZ, compound); ++ } ++ ++ @Override ++ public CompoundTag readData(final int chunkX, final int chunkZ) throws IOException { ++ return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$read(chunkX, chunkZ); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java +new file mode 100644 +index 0000000000000000000000000000000000000000..efcd9057f008f0b9cf0d22b2b21d1851205841e5 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java +@@ -0,0 +1,22 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++ ++public interface ChunkSystemLevel { ++ ++ public EntityLookup moonrise$getEntityLookup(); ++ ++ public void moonrise$setEntityLookup(final EntityLookup entityLookup); ++ ++ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ); ++ ++ public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ); ++ ++ public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus); ++ ++ public void moonrise$midTickTasks(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0b58701342d573fa43cdd06681534854a0e51d77 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java +@@ -0,0 +1,10 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level; ++ ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++ ++public interface ChunkSystemLevelReader { ++ ++ public ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1a06c2c139e930d2081e605b460ae5403de8c3ea +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java +@@ -0,0 +1,53 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import net.minecraft.core.BlockPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import java.util.List; ++import java.util.function.Consumer; ++ ++public interface ChunkSystemServerLevel extends ChunkSystemLevel { ++ ++ public ChunkTaskScheduler moonrise$getChunkTaskScheduler(); ++ ++ public RegionFileIOThread.ChunkDataController moonrise$getChunkDataController(); ++ ++ public RegionFileIOThread.ChunkDataController moonrise$getPoiChunkDataController(); ++ ++ public RegionFileIOThread.ChunkDataController moonrise$getEntityChunkDataController(); ++ ++ public int moonrise$getRegionChunkShift(); ++ ++ // Paper - marked closing not needed on CB ++ ++ public RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader(); ++ ++ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, ++ final PrioritisedExecutor.Priority priority, ++ final Consumer<List<ChunkAccess>> onLoad); ++ ++ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, ++ final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority, ++ final Consumer<List<ChunkAccess>> onLoad); ++ ++ public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, ++ final PrioritisedExecutor.Priority priority, ++ final Consumer<List<ChunkAccess>> onLoad); ++ ++ public void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, ++ final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority, ++ final Consumer<List<ChunkAccess>> onLoad); ++ ++ public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder(); ++ ++ public long moonrise$getLastMidTickFailure(); ++ ++ public void moonrise$setLastMidTickFailure(final long time); ++ ++ public NearbyPlayers moonrise$getNearbyPlayers(); ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7d049d750df88762566f13a9c4fc7574a2df4825 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java +@@ -0,0 +1,26 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; ++ ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.level.chunk.LevelChunk; ++import java.util.List; ++ ++public interface ChunkSystemChunkHolder { ++ ++ public NewChunkHolder moonrise$getRealChunkHolder(); ++ ++ public void moonrise$setRealChunkHolder(final NewChunkHolder newChunkHolder); ++ ++ public void moonrise$addReceivedChunk(final ServerPlayer player); ++ ++ public void moonrise$removeReceivedChunk(final ServerPlayer player); ++ ++ public boolean moonrise$hasChunkBeenSent(); ++ ++ public boolean moonrise$hasChunkBeenSent(final ServerPlayer to); ++ ++ public List<ServerPlayer> moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge); ++ ++ public LevelChunk moonrise$getFullChunk(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f4bc44bb266763345c4e6f859c89352c769a104d +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkStatus.java +@@ -0,0 +1,26 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; ++ ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import java.util.concurrent.atomic.AtomicBoolean; ++ ++public interface ChunkSystemChunkStatus { ++ ++ public boolean moonrise$isParallelCapable(); ++ ++ public void moonrise$setParallelCapable(final boolean value); ++ ++ public int moonrise$getWriteRadius(); ++ ++ public void moonrise$setWriteRadius(final int value); ++ ++ public ChunkStatus moonrise$getNextStatus(); ++ ++ public boolean moonrise$isEmptyLoadStatus(); ++ ++ public void moonrise$setEmptyLoadStatus(final boolean value); ++ ++ public boolean moonrise$isEmptyGenStatus(); ++ ++ public AtomicBoolean moonrise$getWarnedAboutNoImmediateComplete(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..883fe6401f1b9711fa544d18a815b4d638f580df +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; ++ ++import net.minecraft.server.level.ChunkMap; ++ ++public interface ChunkSystemDistanceManager { ++ ++ public ChunkMap moonrise$getChunkMap(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java +new file mode 100644 +index 0000000000000000000000000000000000000000..755b08dd32e568d341ceef8a8aef841831a0781d +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java +@@ -0,0 +1,7 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level.chunk; ++ ++public interface ChunkSystemLevelChunk { ++ ++ public boolean moonrise$isPostProcessingDone(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java +new file mode 100644 +index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548cc65efa8e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java +@@ -0,0 +1,819 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level.entity; ++ ++import ca.spottedleaf.moonrise.common.list.EntityList; ++import ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity; ++import com.google.common.collect.ImmutableList; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.ListTag; ++import net.minecraft.nbt.NbtUtils; ++import net.minecraft.nbt.Tag; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.boss.EnderDragonPart; ++import net.minecraft.world.entity.boss.enderdragon.EnderDragon; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.chunk.storage.EntityStorage; ++import net.minecraft.world.level.entity.Visibility; ++import net.minecraft.world.phys.AABB; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.List; ++import java.util.function.Predicate; ++import org.bukkit.event.entity.EntityRemoveEvent; ++ ++public final class ChunkEntitySlices { ++ ++ public final int minSection; ++ public final int maxSection; ++ public final int chunkX; ++ public final int chunkZ; ++ public final Level world; ++ ++ private final EntityCollectionBySection allEntities; ++ private final EntityCollectionBySection hardCollidingEntities; ++ private final Reference2ObjectOpenHashMap<Class<? extends Entity>, EntityCollectionBySection> entitiesByClass; ++ private final Reference2ObjectOpenHashMap<EntityType<?>, EntityCollectionBySection> entitiesByType; ++ private final EntityList entities = new EntityList(); ++ ++ public FullChunkStatus status; ++ ++ private boolean isTransient; ++ ++ public boolean isTransient() { ++ return this.isTransient; ++ } ++ ++ public void setTransient(final boolean value) { ++ this.isTransient = value; ++ } ++ ++ public ChunkEntitySlices(final Level world, final int chunkX, final int chunkZ, final FullChunkStatus status, ++ final int minSection, final int maxSection) { // inclusive, inclusive ++ this.minSection = minSection; ++ this.maxSection = maxSection; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.world = world; ++ ++ this.allEntities = new EntityCollectionBySection(this); ++ this.hardCollidingEntities = new EntityCollectionBySection(this); ++ this.entitiesByClass = new Reference2ObjectOpenHashMap<>(); ++ this.entitiesByType = new Reference2ObjectOpenHashMap<>(); ++ ++ this.status = status; ++ } ++ ++ public static List<Entity> readEntities(final ServerLevel world, final CompoundTag compoundTag) { ++ // TODO check this and below on update for format changes ++ return EntityType.loadEntitiesRecursive(compoundTag.getList("Entities", 10), world).collect(ImmutableList.toImmutableList()); ++ } ++ ++ // Paper start - rewrite chunk system ++ public static void copyEntities(final CompoundTag from, final CompoundTag into) { ++ if (from == null) { ++ return; ++ } ++ final ListTag entitiesFrom = from.getList("Entities", Tag.TAG_COMPOUND); ++ if (entitiesFrom == null || entitiesFrom.isEmpty()) { ++ return; ++ } ++ ++ final ListTag entitiesInto = into.getList("Entities", Tag.TAG_COMPOUND); ++ into.put("Entities", entitiesInto); // this is in case into doesn't have any entities ++ entitiesInto.addAll(0, entitiesFrom); ++ } ++ ++ public static CompoundTag saveEntityChunk(final List<Entity> entities, final ChunkPos chunkPos, final ServerLevel world) { ++ return saveEntityChunk0(entities, chunkPos, world, false); ++ } ++ ++ public static CompoundTag saveEntityChunk0(final List<Entity> entities, final ChunkPos chunkPos, final ServerLevel world, final boolean force) { ++ if (!force && entities.isEmpty()) { ++ return null; ++ } ++ ++ final ListTag entitiesTag = new ListTag(); ++ for (final Entity entity : entities) { ++ CompoundTag compoundTag = new CompoundTag(); ++ if (entity.save(compoundTag)) { ++ entitiesTag.add(compoundTag); ++ } ++ } ++ final CompoundTag ret = NbtUtils.addCurrentDataVersion(new CompoundTag()); ++ ret.put("Entities", entitiesTag); ++ EntityStorage.writeChunkPos(ret, chunkPos); ++ ++ return !force && entitiesTag.isEmpty() ? null : ret; ++ } ++ ++ public CompoundTag save() { ++ final int len = this.entities.size(); ++ if (len == 0) { ++ return null; ++ } ++ ++ final Entity[] rawData = this.entities.getRawData(); ++ final List<Entity> collectedEntities = new ArrayList<>(len); ++ for (int i = 0; i < len; ++i) { ++ final Entity entity = rawData[i]; ++ if (entity.shouldBeSaved()) { ++ collectedEntities.add(entity); ++ } ++ } ++ ++ if (collectedEntities.isEmpty()) { ++ return null; ++ } ++ ++ return saveEntityChunk(collectedEntities, new ChunkPos(this.chunkX, this.chunkZ), (ServerLevel)this.world); ++ } ++ ++ // returns true if this chunk has transient entities remaining ++ public boolean unload() { ++ final int len = this.entities.size(); ++ final Entity[] collectedEntities = Arrays.copyOf(this.entities.getRawData(), len); ++ ++ for (int i = 0; i < len; ++i) { ++ final Entity entity = collectedEntities[i]; ++ if (entity.isRemoved()) { ++ // removed by us below ++ continue; ++ } ++ if (entity.shouldBeSaved()) { ++ entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD); ++ if (entity.isVehicle()) { ++ // we cannot assume that these entities are contained within this chunk, because entities can ++ // desync - so we need to remove them all ++ for (final Entity passenger : entity.getIndirectPassengers()) { ++ passenger.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD); ++ } ++ } ++ } ++ } ++ ++ return this.entities.size() != 0; ++ } ++ ++ // Paper start ++ public org.bukkit.entity.Entity[] getChunkEntities() { ++ List<org.bukkit.entity.Entity> ret = new java.util.ArrayList<>(); ++ final Entity[] entities = this.entities.getRawData(); ++ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { ++ final Entity entity = entities[i]; ++ if (entity == null) { ++ continue; ++ } ++ final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity(); ++ if (bukkit != null && bukkit.isValid()) { ++ ret.add(bukkit); ++ } ++ } ++ ++ return ret.toArray(new org.bukkit.entity.Entity[0]); ++ } ++ ++ public void callEntitiesLoadEvent() { ++ org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesLoadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities()); ++ } ++ ++ public void callEntitiesUnloadEvent() { ++ org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesUnloadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities()); ++ } ++ // Paper end ++ ++ private List<Entity> getAllEntities() { ++ final int len = this.entities.size(); ++ if (len == 0) { ++ return new ArrayList<>(); ++ } ++ ++ final Entity[] rawData = this.entities.getRawData(); ++ final List<Entity> collectedEntities = new ArrayList<>(len); ++ for (int i = 0; i < len; ++i) { ++ collectedEntities.add(rawData[i]); ++ } ++ ++ return collectedEntities; ++ } ++ ++ public boolean isEmpty() { ++ return this.entities.size() == 0; ++ } ++ ++ public void mergeInto(final ChunkEntitySlices slices) { ++ final Entity[] entities = this.entities.getRawData(); ++ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { ++ final Entity entity = entities[i]; ++ slices.addEntity(entity, ((ChunkSystemEntity)entity).moonrise$getSectionY()); ++ } ++ } ++ ++ private boolean preventStatusUpdates; ++ public boolean startPreventingStatusUpdates() { ++ final boolean ret = this.preventStatusUpdates; ++ this.preventStatusUpdates = true; ++ return ret; ++ } ++ ++ public boolean isPreventingStatusUpdates() { ++ return this.preventStatusUpdates; ++ } ++ ++ public void stopPreventingStatusUpdates(final boolean prev) { ++ this.preventStatusUpdates = prev; ++ } ++ ++ public void updateStatus(final FullChunkStatus status, final EntityLookup lookup) { ++ this.status = status; ++ ++ final Entity[] entities = this.entities.getRawData(); ++ ++ for (int i = 0, size = this.entities.size(); i < size; ++i) { ++ final Entity entity = entities[i]; ++ ++ final Visibility oldVisibility = EntityLookup.getEntityStatus(entity); ++ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(status); ++ final Visibility newVisibility = EntityLookup.getEntityStatus(entity); ++ ++ lookup.entityStatusChange(entity, this, oldVisibility, newVisibility, false, false, false); ++ } ++ } ++ ++ public boolean addEntity(final Entity entity, final int chunkSection) { ++ if (!this.entities.add(entity)) { ++ return false; ++ } ++ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(this.status); ++ final int sectionIndex = chunkSection - this.minSection; ++ ++ this.allEntities.addEntity(entity, sectionIndex); ++ ++ if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) { ++ this.hardCollidingEntities.addEntity(entity, sectionIndex); ++ } ++ ++ for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator = ++ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next(); ++ ++ if (entry.getKey().isInstance(entity)) { ++ entry.getValue().addEntity(entity, sectionIndex); ++ } ++ } ++ ++ EntityCollectionBySection byType = this.entitiesByType.get(entity.getType()); ++ if (byType != null) { ++ byType.addEntity(entity, sectionIndex); ++ } else { ++ this.entitiesByType.put(entity.getType(), byType = new EntityCollectionBySection(this)); ++ byType.addEntity(entity, sectionIndex); ++ } ++ ++ return true; ++ } ++ ++ public boolean removeEntity(final Entity entity, final int chunkSection) { ++ if (!this.entities.remove(entity)) { ++ return false; ++ } ++ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(null); ++ final int sectionIndex = chunkSection - this.minSection; ++ ++ this.allEntities.removeEntity(entity, sectionIndex); ++ ++ if (((ChunkSystemEntity)entity).moonrise$isHardColliding()) { ++ this.hardCollidingEntities.removeEntity(entity, sectionIndex); ++ } ++ ++ for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator = ++ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next(); ++ ++ if (entry.getKey().isInstance(entity)) { ++ entry.getValue().removeEntity(entity, sectionIndex); ++ } ++ } ++ ++ final EntityCollectionBySection byType = this.entitiesByType.get(entity.getType()); ++ byType.removeEntity(entity, sectionIndex); ++ ++ return true; ++ } ++ ++ public void getHardCollidingEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) { ++ this.hardCollidingEntities.getEntities(except, box, into, predicate); ++ } ++ ++ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) { ++ this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate); ++ } ++ ++ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) { ++ this.allEntities.getEntities(except, box, into, predicate); ++ } ++ ++ ++ public boolean getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate, ++ final int maxCount) { ++ return this.allEntities.getEntitiesWithEnderDragonPartsLimited(except, box, into, predicate, maxCount); ++ } ++ ++ public boolean getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate, ++ final int maxCount) { ++ return this.allEntities.getEntitiesLimited(except, box, into, predicate, maxCount); ++ } ++ ++ public <T extends Entity> void getEntities(final EntityType<?> type, final AABB box, final List<? super T> into, ++ final Predicate<? super T> predicate) { ++ final EntityCollectionBySection byType = this.entitiesByType.get(type); ++ ++ if (byType != null) { ++ byType.getEntities((Entity)null, box, (List)into, (Predicate) predicate); ++ } ++ } ++ ++ public <T extends Entity> boolean getEntities(final EntityType<?> type, final AABB box, final List<? super T> into, ++ final Predicate<? super T> predicate, final int maxCount) { ++ final EntityCollectionBySection byType = this.entitiesByType.get(type); ++ ++ if (byType != null) { ++ return byType.getEntitiesLimited((Entity)null, box, (List)into, (Predicate)predicate, maxCount); ++ } ++ ++ return false; ++ } ++ ++ protected EntityCollectionBySection initClass(final Class<? extends Entity> clazz) { ++ final EntityCollectionBySection ret = new EntityCollectionBySection(this); ++ ++ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) { ++ final BasicEntityList<Entity> sectionEntities = this.allEntities.entitiesBySection[sectionIndex]; ++ if (sectionEntities == null) { ++ continue; ++ } ++ ++ final Entity[] storage = sectionEntities.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (clazz.isInstance(entity)) { ++ ret.addEntity(entity, sectionIndex); ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into, ++ final Predicate<? super T> predicate) { ++ EntityCollectionBySection collection = this.entitiesByClass.get(clazz); ++ if (collection != null) { ++ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); ++ } else { ++ this.entitiesByClass.put(clazz, collection = this.initClass(clazz)); ++ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); ++ } ++ } ++ ++ public <T extends Entity> boolean getEntities(final Class<? extends T> clazz, final Entity except, final AABB box, final List<? super T> into, ++ final Predicate<? super T> predicate, final int maxCount) { ++ EntityCollectionBySection collection = this.entitiesByClass.get(clazz); ++ if (collection != null) { ++ return collection.getEntitiesWithEnderDragonPartsLimited(except, clazz, box, (List)into, (Predicate)predicate, maxCount); ++ } else { ++ this.entitiesByClass.put(clazz, collection = this.initClass(clazz)); ++ return collection.getEntitiesWithEnderDragonPartsLimited(except, clazz, box, (List)into, (Predicate)predicate, maxCount); ++ } ++ } ++ ++ private static final class BasicEntityList<E extends Entity> { ++ ++ private static final Entity[] EMPTY = new Entity[0]; ++ private static final int DEFAULT_CAPACITY = 4; ++ ++ private E[] storage; ++ private int size; ++ ++ public BasicEntityList() { ++ this(0); ++ } ++ ++ public BasicEntityList(final int cap) { ++ this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]); ++ } ++ ++ public boolean isEmpty() { ++ return this.size == 0; ++ } ++ ++ public int size() { ++ return this.size; ++ } ++ ++ private void resize() { ++ if (this.storage == EMPTY) { ++ this.storage = (E[])new Entity[DEFAULT_CAPACITY]; ++ } else { ++ this.storage = Arrays.copyOf(this.storage, this.storage.length * 2); ++ } ++ } ++ ++ public void add(final E entity) { ++ final int idx = this.size++; ++ if (idx >= this.storage.length) { ++ this.resize(); ++ this.storage[idx] = entity; ++ } else { ++ this.storage[idx] = entity; ++ } ++ } ++ ++ public int indexOf(final E entity) { ++ final E[] storage = this.storage; ++ ++ for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) { ++ if (storage[i] == entity) { ++ return i; ++ } ++ } ++ ++ return -1; ++ } ++ ++ public boolean remove(final E entity) { ++ final int idx = this.indexOf(entity); ++ if (idx == -1) { ++ return false; ++ } ++ ++ final int size = --this.size; ++ final E[] storage = this.storage; ++ if (idx != size) { ++ System.arraycopy(storage, idx + 1, storage, idx, size - idx); ++ } ++ ++ storage[size] = null; ++ ++ return true; ++ } ++ ++ public boolean has(final E entity) { ++ return this.indexOf(entity) != -1; ++ } ++ } ++ ++ private static final class EntityCollectionBySection { ++ ++ private final ChunkEntitySlices slices; ++ private final BasicEntityList<Entity>[] entitiesBySection; ++ private int count; ++ ++ public EntityCollectionBySection(final ChunkEntitySlices slices) { ++ this.slices = slices; ++ ++ final int sectionCount = slices.maxSection - slices.minSection + 1; ++ ++ this.entitiesBySection = new BasicEntityList[sectionCount]; ++ } ++ ++ public void addEntity(final Entity entity, final int sectionIndex) { ++ BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex]; ++ ++ if (list != null && list.has(entity)) { ++ return; ++ } ++ ++ if (list == null) { ++ this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>(); ++ } ++ ++ list.add(entity); ++ ++this.count; ++ } ++ ++ public void removeEntity(final Entity entity, final int sectionIndex) { ++ final BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex]; ++ ++ if (list == null || !list.remove(entity)) { ++ return; ++ } ++ ++ --this.count; ++ ++ if (list.isEmpty()) { ++ this.entitiesBySection[sectionIndex] = null; ++ } ++ } ++ ++ public void getEntities(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate) { ++ if (this.count == 0) { ++ return; ++ } ++ ++ final int minSection = this.slices.minSection; ++ final int maxSection = this.slices.maxSection; ++ ++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); ++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); ++ ++ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList<Entity> list = entitiesBySection[section - minSection]; ++ ++ if (list == null) { ++ continue; ++ } ++ ++ final Entity[] storage = list.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(entity)) { ++ continue; ++ } ++ ++ into.add(entity); ++ } ++ } ++ } ++ ++ public boolean getEntitiesLimited(final Entity except, final AABB box, final List<Entity> into, final Predicate<? super Entity> predicate, ++ final int maxCount) { ++ if (this.count == 0) { ++ return false; ++ } ++ ++ final int minSection = this.slices.minSection; ++ final int maxSection = this.slices.maxSection; ++ ++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); ++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); ++ ++ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList<Entity> list = entitiesBySection[section - minSection]; ++ ++ if (list == null) { ++ continue; ++ } ++ ++ final Entity[] storage = list.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(entity)) { ++ continue; ++ } ++ ++ into.add(entity); ++ if (into.size() >= maxCount) { ++ return true; ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ public void getEntitiesWithEnderDragonParts(final Entity except, final AABB box, final List<Entity> into, ++ final Predicate<? super Entity> predicate) { ++ if (this.count == 0) { ++ return; ++ } ++ ++ final int minSection = this.slices.minSection; ++ final int maxSection = this.slices.maxSection; ++ ++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); ++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); ++ ++ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList<Entity> list = entitiesBySection[section - minSection]; ++ ++ if (list == null) { ++ continue; ++ } ++ ++ final Entity[] storage = list.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate == null || predicate.test(entity)) { ++ into.add(entity); ++ } // else: continue to test the ender dragon parts ++ ++ if (entity instanceof EnderDragon) { ++ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) { ++ if (part == except || !part.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(part)) { ++ continue; ++ } ++ ++ into.add(part); ++ } ++ } ++ } ++ } ++ } ++ ++ public boolean getEntitiesWithEnderDragonPartsLimited(final Entity except, final AABB box, final List<Entity> into, ++ final Predicate<? super Entity> predicate, final int maxCount) { ++ if (this.count == 0) { ++ return false; ++ } ++ ++ final int minSection = this.slices.minSection; ++ final int maxSection = this.slices.maxSection; ++ ++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); ++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); ++ ++ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList<Entity> list = entitiesBySection[section - minSection]; ++ ++ if (list == null) { ++ continue; ++ } ++ ++ final Entity[] storage = list.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate == null || predicate.test(entity)) { ++ into.add(entity); ++ if (into.size() >= maxCount) { ++ return true; ++ } ++ } // else: continue to test the ender dragon parts ++ ++ if (entity instanceof EnderDragon) { ++ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) { ++ if (part == except || !part.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(part)) { ++ continue; ++ } ++ ++ into.add(part); ++ if (into.size() >= maxCount) { ++ return true; ++ } ++ } ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ public void getEntitiesWithEnderDragonParts(final Entity except, final Class<?> clazz, final AABB box, final List<Entity> into, ++ final Predicate<? super Entity> predicate) { ++ if (this.count == 0) { ++ return; ++ } ++ ++ final int minSection = this.slices.minSection; ++ final int maxSection = this.slices.maxSection; ++ ++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); ++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); ++ ++ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList<Entity> list = entitiesBySection[section - minSection]; ++ ++ if (list == null) { ++ continue; ++ } ++ ++ final Entity[] storage = list.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate == null || predicate.test(entity)) { ++ into.add(entity); ++ } // else: continue to test the ender dragon parts ++ ++ if (entity instanceof EnderDragon) { ++ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) { ++ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) { ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(part)) { ++ continue; ++ } ++ ++ into.add(part); ++ } ++ } ++ } ++ } ++ } ++ ++ public boolean getEntitiesWithEnderDragonPartsLimited(final Entity except, final Class<?> clazz, final AABB box, final List<Entity> into, ++ final Predicate<? super Entity> predicate, final int maxCount) { ++ if (this.count == 0) { ++ return false; ++ } ++ ++ final int minSection = this.slices.minSection; ++ final int maxSection = this.slices.maxSection; ++ ++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); ++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); ++ ++ final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList<Entity> list = entitiesBySection[section - minSection]; ++ ++ if (list == null) { ++ continue; ++ } ++ ++ final Entity[] storage = list.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate == null || predicate.test(entity)) { ++ into.add(entity); ++ if (into.size() >= maxCount) { ++ return true; ++ } ++ } // else: continue to test the ender dragon parts ++ ++ if (entity instanceof EnderDragon) { ++ for (final EnderDragonPart part : ((EnderDragon)entity).getSubEntities()) { ++ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) { ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(part)) { ++ continue; ++ } ++ ++ into.add(part); ++ if (into.size() >= maxCount) { ++ return true; ++ } ++ } ++ } ++ } ++ } ++ ++ return false; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a346435abac725b4e024acf4a1589a51caac8d69 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java +@@ -0,0 +1,1077 @@ ++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); ++ ++ protected abstract void entitySectionChangeCallback( ++ final Entity entity, ++ final int oldSectionX, final int oldSectionY, final int oldSectionZ, ++ final int newSectionX, final int newSectionY, final int newSectionZ ++ ); ++ ++ protected abstract void addEntityCallback(final Entity entity); ++ ++ protected abstract void removeEntityCallback(final Entity entity); ++ ++ protected abstract void entityStartLoaded(final Entity entity); ++ ++ protected abstract void entityEndLoaded(final Entity entity); ++ ++ protected abstract void entityStartTicking(final Entity entity); ++ ++ protected abstract void entityEndTicking(final Entity entity); ++ ++ private static Entity maskNonAccessible(final Entity entity) { ++ if (entity == null) { ++ return null; ++ } ++ final Visibility visibility = EntityLookup.getEntityStatus(entity); ++ return visibility.isAccessible() ? entity : null; ++ } ++ ++ @Override ++ public Entity get(final int id) { ++ return maskNonAccessible(this.entityById.get((long)id)); ++ } ++ ++ @Override ++ public Entity get(final UUID id) { ++ return maskNonAccessible(id == null ? null : this.entityByUUID.get(id)); ++ } ++ ++ public boolean hasEntity(final UUID uuid) { ++ return this.get(uuid) != null; ++ } ++ ++ public String getDebugInfo() { ++ return "count_id:" + this.entityById.size() + ",count_uuid:" + this.entityByUUID.size() + ",count_accessible:" + this.getEntityCount() + ",region_count:" + this.regions.size(); ++ } ++ ++ protected static final class ArrayIterable<T> implements Iterable<T> { ++ ++ private final T[] array; ++ private final int off; ++ private final int length; ++ ++ public ArrayIterable(final T[] array, final int off, final int length) { ++ this.array = array; ++ this.off = off; ++ this.length = length; ++ if (length > array.length) { ++ throw new IllegalArgumentException("Length must be no greater-than the array length"); ++ } ++ } ++ ++ @Override ++ public Iterator<T> iterator() { ++ return new ArrayIterator<>(this.array, this.off, this.length); ++ } ++ ++ protected static final class ArrayIterator<T> implements Iterator<T> { ++ ++ private final T[] array; ++ private int off; ++ private final int length; ++ ++ public ArrayIterator(final T[] array, final int off, final int length) { ++ this.array = array; ++ this.off = off; ++ this.length = length; ++ } ++ ++ @Override ++ public boolean hasNext() { ++ return this.off < this.length; ++ } ++ ++ @Override ++ public T next() { ++ if (this.off >= this.length) { ++ throw new NoSuchElementException(); ++ } ++ return this.array[this.off++]; ++ } ++ ++ @Override ++ public void remove() { ++ throw new UnsupportedOperationException(); ++ } ++ } ++ } ++ ++ @Override ++ public Iterable<Entity> getAll() { ++ synchronized (this.accessibleEntities) { ++ final int len = this.accessibleEntities.size(); ++ final Entity[] cpy = Arrays.copyOf(this.accessibleEntities.getRawData(), len, Entity[].class); ++ ++ Objects.checkFromToIndex(0, len, cpy.length); ++ ++ return new ArrayIterable<>(cpy, 0, len); ++ } ++ } ++ ++ public int getEntityCount() { ++ synchronized (this.accessibleEntities) { ++ return this.accessibleEntities.size(); ++ } ++ } ++ ++ public Entity[] getAllCopy() { ++ synchronized (this.accessibleEntities) { ++ return Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size(), Entity[].class); ++ } ++ } ++ ++ @Override ++ public <U extends Entity> void get(final EntityTypeTest<Entity, U> filter, final AbortableIterationConsumer<U> action) { ++ for (final Iterator<Entity> iterator = this.entityById.valueIterator(); iterator.hasNext();) { ++ final Entity entity = iterator.next(); ++ final Visibility visibility = EntityLookup.getEntityStatus(entity); ++ if (!visibility.isAccessible()) { ++ continue; ++ } ++ final U casted = filter.tryCast(entity); ++ if (casted != null && action.accept(casted).shouldAbort()) { ++ break; ++ } ++ } ++ } ++ ++ @Override ++ public void get(final AABB box, final Consumer<Entity> action) { ++ List<Entity> entities = new ArrayList<>(); ++ this.getEntitiesWithoutDragonParts(null, box, entities, null); ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ action.accept(entities.get(i)); ++ } ++ } ++ ++ @Override ++ public <U extends Entity> void get(final EntityTypeTest<Entity, U> filter, final AABB box, final AbortableIterationConsumer<U> action) { ++ List<Entity> entities = new ArrayList<>(); ++ this.getEntitiesWithoutDragonParts(null, box, entities, null); ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ final U casted = filter.tryCast(entities.get(i)); ++ if (casted != null && action.accept(casted).shouldAbort()) { ++ break; ++ } ++ } ++ } ++ ++ public void entityStatusChange(final Entity entity, final ChunkEntitySlices slices, final Visibility oldVisibility, final Visibility newVisibility, final boolean moved, ++ final boolean created, final boolean destroyed) { ++ this.checkThread(entity, "Entity status change must only happen on the main thread"); ++ ++ if (((ChunkSystemEntity)entity).moonrise$isUpdatingSectionStatus()) { ++ // recursive status update ++ LOGGER.error("Cannot recursively update entity chunk status for entity " + entity, new Throwable()); ++ return; ++ } ++ ++ final boolean entityStatusUpdateBefore = slices == null ? false : slices.startPreventingStatusUpdates(); ++ ++ if (entityStatusUpdateBefore) { ++ LOGGER.error("Cannot update chunk status for entity " + entity + " since entity chunk (" + slices.chunkX + "," + slices.chunkZ + ") is receiving update", new Throwable()); ++ return; ++ } ++ ++ try { ++ final Boolean ticketBlockBefore = this.blockTicketUpdates(); ++ try { ++ ((ChunkSystemEntity)entity).moonrise$setUpdatingSectionStatus(true); ++ try { ++ if (created) { ++ if (EntityLookup.this.worldCallback != null) { ++ EntityLookup.this.worldCallback.onCreated(entity); ++ } ++ } ++ ++ if (oldVisibility == newVisibility) { ++ if (moved && newVisibility.isAccessible()) { ++ if (EntityLookup.this.worldCallback != null) { ++ EntityLookup.this.worldCallback.onSectionChange(entity); ++ } ++ } ++ return; ++ } ++ ++ if (newVisibility.ordinal() > oldVisibility.ordinal()) { ++ // status upgrade ++ if (!oldVisibility.isAccessible() && newVisibility.isAccessible()) { ++ EntityLookup.this.entityStartLoaded(entity); ++ synchronized (this.accessibleEntities) { ++ this.accessibleEntities.add(entity); ++ } ++ if (EntityLookup.this.worldCallback != null) { ++ EntityLookup.this.worldCallback.onTrackingStart(entity); ++ } ++ } ++ ++ if (!oldVisibility.isTicking() && newVisibility.isTicking()) { ++ EntityLookup.this.entityStartTicking(entity); ++ if (EntityLookup.this.worldCallback != null) { ++ EntityLookup.this.worldCallback.onTickingStart(entity); ++ } ++ } ++ } else { ++ // status downgrade ++ if (oldVisibility.isTicking() && !newVisibility.isTicking()) { ++ EntityLookup.this.entityEndTicking(entity); ++ if (EntityLookup.this.worldCallback != null) { ++ EntityLookup.this.worldCallback.onTickingEnd(entity); ++ } ++ } ++ ++ if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) { ++ EntityLookup.this.entityEndLoaded(entity); ++ 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.addEntityCallback(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); ++ } ++ ++ this.entitySectionChangeCallback( ++ entity, ++ sectionX, sectionY, sectionZ, ++ newSectionX, newSectionY, newSectionZ ++ ); ++ ++ 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()); ++ ++ EntityLookup.this.removeEntityCallback(entity); ++ ++ 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..77e81414d43826955da8af85ec1bd0009af4e1b9 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java +@@ -0,0 +1,118 @@ ++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); ++ } ++ ++ @Override ++ protected void entitySectionChangeCallback(final Entity entity, ++ final int oldSectionX, final int oldSectionY, final int oldSectionZ, ++ final int newSectionX, final int newSectionY, final int newSectionZ) { ++ ++ } ++ ++ @Override ++ protected void addEntityCallback(final Entity entity) { ++ ++ } ++ ++ @Override ++ protected void removeEntityCallback(final Entity entity) { ++ ++ } ++ ++ @Override ++ protected void entityStartLoaded(final Entity entity) { ++ ++ } ++ ++ @Override ++ protected void entityEndLoaded(final Entity entity) { ++ ++ } ++ ++ @Override ++ protected void entityStartTicking(final Entity entity) { ++ ++ } ++ ++ @Override ++ protected void entityEndTicking(final Entity entity) { ++ ++ } ++ ++ 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..4747499e84b05d499364622b0f750fdd66b737e0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java +@@ -0,0 +1,109 @@ ++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); ++ } ++ ++ @Override ++ protected void entitySectionChangeCallback(final Entity entity, ++ final int oldSectionX, final int oldSectionY, final int oldSectionZ, ++ final int newSectionX, final int newSectionY, final int newSectionZ) { ++ ++ } ++ ++ @Override ++ protected void addEntityCallback(final Entity entity) { ++ ++ } ++ ++ @Override ++ protected void removeEntityCallback(final Entity entity) { ++ ++ } ++ ++ @Override ++ protected void entityStartLoaded(final Entity entity) { ++ ++ } ++ ++ @Override ++ protected void entityEndLoaded(final Entity entity) { ++ ++ } ++ ++ @Override ++ protected void entityStartTicking(final Entity entity) { ++ ++ } ++ ++ @Override ++ protected void entityEndTicking(final Entity entity) { ++ ++ } ++ ++ 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..eb1b0b3594ae6861a05010949ba94a60f73ecc8b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java +@@ -0,0 +1,106 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server; ++ ++import ca.spottedleaf.moonrise.common.list.ReferenceList; ++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.server.level.ServerPlayer; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.entity.LevelCallback; ++ ++public final class ServerEntityLookup extends EntityLookup { ++ ++ private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0]; ++ ++ private final ServerLevel serverWorld; ++ public final ReferenceList<Entity> trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY, 0); // Moonrise - entity tracker ++ public final ReferenceList<Entity> trackerUnloadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY, 0); // Moonrise - entity tracker ++ ++ 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 ++ } ++ ++ @Override ++ protected void entitySectionChangeCallback(final Entity entity, ++ final int oldSectionX, final int oldSectionY, final int oldSectionZ, ++ final int newSectionX, final int newSectionY, final int newSectionZ) { ++ if (entity instanceof ServerPlayer player) { ++ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().tickPlayer(player); ++ } ++ } ++ ++ @Override ++ protected void addEntityCallback(final Entity entity) { ++ if (entity instanceof ServerPlayer player) { ++ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().addPlayer(player); ++ } ++ } ++ ++ @Override ++ protected void removeEntityCallback(final Entity entity) { ++ if (entity instanceof ServerPlayer player) { ++ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().removePlayer(player); ++ } ++ this.trackerUnloadedEntities.remove(entity); // Moonrise - entity tracker ++ } ++ ++ @Override ++ protected void entityStartLoaded(final Entity entity) { ++ // Moonrise start - entity tracker ++ this.trackerEntities.add(entity); ++ this.trackerUnloadedEntities.remove(entity); ++ // Moonrise end - entity tracker ++ } ++ ++ @Override ++ protected void entityEndLoaded(final Entity entity) { ++ // Moonrise start - entity tracker ++ this.trackerEntities.remove(entity); ++ this.trackerUnloadedEntities.add(entity); ++ // Moonrise end - entity tracker ++ } ++ ++ @Override ++ protected void entityStartTicking(final Entity entity) { ++ ++ } ++ ++ @Override ++ protected void entityEndTicking(final Entity entity) { ++ ++ } ++} +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..f063ab3ff122b920bcc4aa4f5bf9a442a8eb32fd +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +@@ -0,0 +1,1076 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.player; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.moonrise.common.misc.AllocatingRateLimiter; ++import ca.spottedleaf.moonrise.common.misc.SingleUserAreaMap; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.MoonriseCommon; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import ca.spottedleaf.moonrise.patches.chunk_system.util.ParallelSearchRadiusIteration; ++import com.google.gson.JsonObject; ++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import it.unimi.dsi.fastutil.longs.LongComparator; ++import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; ++import it.unimi.dsi.fastutil.longs.LongOpenHashSet; ++import net.minecraft.network.protocol.Packet; ++import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket; ++import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket; ++import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket; ++import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket; ++import net.minecraft.server.level.ChunkTrackingView; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.server.network.PlayerChunkSender; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.GameRules; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.levelgen.BelowZeroRetrogen; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayDeque; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.function.Function; ++ ++public final class RegionizedPlayerChunkLoader { ++ ++ public static final TicketType<Long> PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo); ++ public static final TicketType<Long> PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 5 * 20); ++ ++ public static final int MIN_VIEW_DISTANCE = 2; ++ public static final int MAX_VIEW_DISTANCE = 32; ++ ++ public static final int GENERATED_TICKET_LEVEL = ChunkHolderManager.FULL_LOADED_TICKET_LEVEL; ++ public static final int LOADED_TICKET_LEVEL = ChunkTaskScheduler.getTicketLevel(ChunkStatus.EMPTY); ++ public static final int TICK_TICKET_LEVEL = ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL; ++ ++ public static final class ViewDistanceHolder { ++ ++ private volatile ViewDistances viewDistances; ++ private static final VarHandle VIEW_DISTANCES_HANDLE = ConcurrentUtil.getVarHandle(ViewDistanceHolder.class, "viewDistances", ViewDistances.class); ++ ++ public ViewDistanceHolder() { ++ VIEW_DISTANCES_HANDLE.setVolatile(this, new ViewDistances(-1, -1, -1)); ++ } ++ ++ public ViewDistances getViewDistances() { ++ return (ViewDistances)VIEW_DISTANCES_HANDLE.getVolatile(this); ++ } ++ ++ public ViewDistances compareAndExchangeViewDistance(final ViewDistances expect, final ViewDistances update) { ++ return (ViewDistances)VIEW_DISTANCES_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ public void updateViewDistance(final Function<ViewDistances, ViewDistances> update) { ++ int failures = 0; ++ for (ViewDistances curr = this.getViewDistances();;) { ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = this.compareAndExchangeViewDistance(curr, update.apply(curr)))) { ++ return; ++ } ++ ++failures; ++ } ++ } ++ ++ public void setTickViewDistance(final int distance) { ++ this.updateViewDistance((final ViewDistances param) -> { ++ return param.setTickViewDistance(distance); ++ }); ++ } ++ ++ public void setLoadViewDistance(final int distance) { ++ this.updateViewDistance((final ViewDistances param) -> { ++ return param.setLoadViewDistance(distance); ++ }); ++ } ++ ++ public void setSendViewDistance(final int distance) { ++ this.updateViewDistance((final ViewDistances param) -> { ++ return param.setTickViewDistance(distance); ++ }); ++ } ++ ++ public JsonObject toJson() { ++ return this.getViewDistances().toJson(); ++ } ++ } ++ ++ public static final record ViewDistances( ++ int tickViewDistance, ++ int loadViewDistance, ++ int sendViewDistance ++ ) { ++ public ViewDistances setTickViewDistance(final int distance) { ++ return new ViewDistances(distance, this.loadViewDistance, this.sendViewDistance); ++ } ++ ++ public ViewDistances setLoadViewDistance(final int distance) { ++ return new ViewDistances(this.tickViewDistance, distance, this.sendViewDistance); ++ } ++ ++ public ViewDistances setSendViewDistance(final int distance) { ++ return new ViewDistances(this.tickViewDistance, this.loadViewDistance, distance); ++ } ++ ++ public JsonObject toJson() { ++ final JsonObject ret = new JsonObject(); ++ ++ ret.addProperty("tick-view-distance", this.tickViewDistance); ++ ret.addProperty("load-view-distance", this.loadViewDistance); ++ ret.addProperty("send-view-distance", this.sendViewDistance); ++ ++ return ret; ++ } ++ } ++ ++ public static int getAPITickViewDistance(final ServerPlayer player) { ++ final ServerLevel level = player.serverLevel(); ++ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); ++ if (data == null) { ++ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPITickDistance(); ++ } ++ return data.lastTickDistance; ++ } ++ ++ public static int getAPIViewDistance(final ServerPlayer player) { ++ final ServerLevel level = player.serverLevel(); ++ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); ++ if (data == null) { ++ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPIViewDistance(); ++ } ++ // view distance = load distance + 1 ++ return data.lastLoadDistance - 1; ++ } ++ ++ public static int getLoadViewDistance(final ServerPlayer player) { ++ final ServerLevel level = player.serverLevel(); ++ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); ++ if (data == null) { ++ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPIViewDistance(); ++ } ++ // view distance = load distance + 1 ++ return data.lastLoadDistance - 1; ++ } ++ ++ public static int getAPISendViewDistance(final ServerPlayer player) { ++ final ServerLevel level = player.serverLevel(); ++ final PlayerChunkLoaderData data = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); ++ if (data == null) { ++ return ((ChunkSystemServerLevel)level).moonrise$getPlayerChunkLoader().getAPISendViewDistance(); ++ } ++ return data.lastSendDistance; ++ } ++ ++ private final ServerLevel world; ++ ++ public RegionizedPlayerChunkLoader(final ServerLevel world) { ++ this.world = world; ++ } ++ ++ public void addPlayer(final ServerPlayer player) { ++ io.papermc.paper.util.TickThread.ensureTickThread(player, "Cannot add player to player chunk loader async"); ++ if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) { ++ return; ++ } ++ ++ if (((ChunkSystemServerPlayer)player).moonrise$getChunkLoader() != null) { ++ throw new IllegalStateException("Player is already added to player chunk loader"); ++ } ++ ++ final PlayerChunkLoaderData loader = new PlayerChunkLoaderData(this.world, player); ++ ++ ((ChunkSystemServerPlayer)player).moonrise$setChunkLoader(loader); ++ loader.add(); ++ } ++ ++ public void updatePlayer(final ServerPlayer player) { ++ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); ++ if (loader != null) { ++ loader.update(); ++ // update view distances for nearby players ++ ((ChunkSystemServerLevel)loader.world).moonrise$getNearbyPlayers().tickPlayer(player); ++ } ++ } ++ ++ public void removePlayer(final ServerPlayer player) { ++ io.papermc.paper.util.TickThread.ensureTickThread(player, "Cannot remove player from player chunk loader async"); ++ if (!((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()) { ++ return; ++ } ++ ++ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); ++ ++ if (loader == null) { ++ return; ++ } ++ ++ loader.remove(); ++ ((ChunkSystemServerPlayer)player).moonrise$setChunkLoader(null); ++ } ++ ++ public void setSendDistance(final int distance) { ++ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setSendViewDistance(distance); ++ } ++ ++ public void setLoadDistance(final int distance) { ++ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setLoadViewDistance(distance); ++ } ++ ++ public void setTickDistance(final int distance) { ++ ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().setTickViewDistance(distance); ++ } ++ ++ // Note: follow the player chunk loader so everything stays consistent... ++ public int getAPITickDistance() { ++ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); ++ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance( ++ -1, distances.tickViewDistance, ++ -1, distances.loadViewDistance ++ ); ++ return tickViewDistance; ++ } ++ ++ public int getAPIViewDistance() { ++ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); ++ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance( ++ -1, distances.tickViewDistance, ++ -1, distances.loadViewDistance ++ ); ++ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance); ++ ++ // loadDistance = api view distance + 1 ++ return loadDistance - 1; ++ } ++ ++ public int getAPISendViewDistance() { ++ final ViewDistances distances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); ++ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance( ++ -1, distances.tickViewDistance, ++ -1, distances.loadViewDistance ++ ); ++ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance); ++ final int sendViewDistance = PlayerChunkLoaderData.getSendViewDistance( ++ loadDistance, -1, -1, distances.sendViewDistance ++ ); ++ ++ return sendViewDistance; ++ } ++ ++ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) { ++ return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ); ++ } ++ ++ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) { ++ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); ++ if (loader == null) { ++ return false; ++ } ++ ++ return loader.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) { ++ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); ++ if (loader == null) { ++ return false; ++ } ++ ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ if (!loader.sentChunks.contains(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ))) { ++ return true; ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ public void tick() { ++ io.papermc.paper.util.TickThread.ensureTickThread("Cannot tick player chunk loader async"); ++ long currTime = System.nanoTime(); ++ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) { ++ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); ++ if (loader == null || loader.removed || loader.world != this.world) { ++ // not our problem anymore ++ continue; ++ } ++ loader.update(); // can't invoke plugin logic ++ loader.updateQueues(currTime); ++ } ++ } ++ ++ public static final class PlayerChunkLoaderData { ++ ++ private static final AtomicLong ID_GENERATOR = new AtomicLong(); ++ private final long id = ID_GENERATOR.incrementAndGet(); ++ private final Long idBoxed = Long.valueOf(this.id); ++ ++ private static final long MAX_RATE = 10_000L; ++ ++ private final ServerPlayer player; ++ private final ServerLevel world; ++ ++ private int lastChunkX = Integer.MIN_VALUE; ++ private int lastChunkZ = Integer.MIN_VALUE; ++ ++ private int lastSendDistance = Integer.MIN_VALUE; ++ private int lastLoadDistance = Integer.MIN_VALUE; ++ private int lastTickDistance = Integer.MIN_VALUE; ++ ++ private int lastSentChunkCenterX = Integer.MIN_VALUE; ++ private int lastSentChunkCenterZ = Integer.MIN_VALUE; ++ ++ private int lastSentChunkRadius = Integer.MIN_VALUE; ++ private int lastSentSimulationDistance = Integer.MIN_VALUE; ++ ++ private boolean canGenerateChunks = true; ++ ++ private final ArrayDeque<ChunkHolderManager.TicketOperation<?, ?>> delayedTicketOps = new ArrayDeque<>(); ++ private final LongOpenHashSet sentChunks = new LongOpenHashSet(); ++ ++ private static final byte CHUNK_TICKET_STAGE_NONE = 0; ++ private static final byte CHUNK_TICKET_STAGE_LOADING = 1; ++ private static final byte CHUNK_TICKET_STAGE_LOADED = 2; ++ private static final byte CHUNK_TICKET_STAGE_GENERATING = 3; ++ private static final byte CHUNK_TICKET_STAGE_GENERATED = 4; ++ private static final byte CHUNK_TICKET_STAGE_TICK = 5; ++ private static final int[] TICKET_STAGE_TO_LEVEL = new int[] { ++ ChunkHolderManager.MAX_TICKET_LEVEL + 1, ++ LOADED_TICKET_LEVEL, ++ LOADED_TICKET_LEVEL, ++ GENERATED_TICKET_LEVEL, ++ GENERATED_TICKET_LEVEL, ++ TICK_TICKET_LEVEL ++ }; ++ private final Long2ByteOpenHashMap chunkTicketStage = new Long2ByteOpenHashMap(); ++ { ++ this.chunkTicketStage.defaultReturnValue(CHUNK_TICKET_STAGE_NONE); ++ } ++ ++ // rate limiting ++ private static final long ALLOCATION_GRANULARITY = TimeUnit.SECONDS.toNanos(1L); ++ private final AllocatingRateLimiter chunkSendLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY); ++ private final AllocatingRateLimiter chunkLoadTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY); ++ private final AllocatingRateLimiter chunkGenerateTicketLimiter = new AllocatingRateLimiter(ALLOCATION_GRANULARITY); ++ ++ // queues ++ private final LongComparator CLOSEST_MANHATTAN_DIST = (final long c1, final long c2) -> { ++ final int c1x = CoordinateUtils.getChunkX(c1); ++ final int c1z = CoordinateUtils.getChunkZ(c1); ++ ++ final int c2x = CoordinateUtils.getChunkX(c2); ++ final int c2z = CoordinateUtils.getChunkZ(c2); ++ ++ final int centerX = PlayerChunkLoaderData.this.lastChunkX; ++ final int centerZ = PlayerChunkLoaderData.this.lastChunkZ; ++ ++ return Integer.compare( ++ Math.abs(c1x - centerX) + Math.abs(c1z - centerZ), ++ Math.abs(c2x - centerX) + Math.abs(c2z - centerZ) ++ ); ++ }; ++ private final LongHeapPriorityQueue sendQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue tickingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue generatingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue genQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue loadingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ ++ private volatile boolean removed; ++ ++ public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) { ++ this.world = world; ++ this.player = player; ++ } ++ ++ private void flushDelayedTicketOps() { ++ if (this.delayedTicketOps.isEmpty()) { ++ return; ++ } ++ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.performTicketUpdates(this.delayedTicketOps); ++ this.delayedTicketOps.clear(); ++ } ++ ++ private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation<?, ?> op) { ++ this.delayedTicketOps.addLast(op); ++ } ++ ++ private void sendChunk(final int chunkX, final int chunkZ) { ++ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { ++ ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager ++ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player); ++ PlayerChunkSender.sendChunk(this.player.connection, this.world, ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ)); ++ return; ++ } ++ throw new IllegalStateException(); ++ } ++ ++ private void sendUnloadChunk(final int chunkX, final int chunkZ) { ++ if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { ++ return; ++ } ++ this.sendUnloadChunkRaw(chunkX, chunkZ); ++ } ++ ++ private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) { ++ // Note: Check PlayerChunkSender#dropChunk for other logic ++ // Note: drop isAlive() check so that chunks properly unload client-side when the player dies ++ ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager ++ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$removeReceivedChunk(this.player); ++ this.player.connection.send(new ClientboundForgetLevelChunkPacket(new ChunkPos(chunkX, chunkZ))); ++ } ++ ++ private final SingleUserAreaMap<PlayerChunkLoaderData> broadcastMap = new SingleUserAreaMap<>(this) { ++ @Override ++ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ // do nothing, we only care about remove ++ } ++ ++ @Override ++ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ parameter.sendUnloadChunk(chunkX, chunkZ); ++ } ++ }; ++ private final SingleUserAreaMap<PlayerChunkLoaderData> loadTicketCleanup = new SingleUserAreaMap<>(this) { ++ @Override ++ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ // do nothing, we only care about remove ++ } ++ ++ @Override ++ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final byte ticketStage = parameter.chunkTicketStage.remove(chunk); ++ final int level = TICKET_STAGE_TO_LEVEL[ticketStage]; ++ if (level > ChunkHolderManager.MAX_TICKET_LEVEL) { ++ return; ++ } ++ ++ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( ++ chunk, ++ PLAYER_TICKET_DELAYED, level, parameter.idBoxed, ++ PLAYER_TICKET, level, parameter.idBoxed ++ )); ++ } ++ }; ++ private final SingleUserAreaMap<PlayerChunkLoaderData> tickMap = new SingleUserAreaMap<>(this) { ++ @Override ++ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ // do nothing, we will detect ticking chunks when we try to load them ++ } ++ ++ @Override ++ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ // note: by the time this is called, the tick cleanup should have ran - so, if the chunk is at ++ // the tick stage it was deemed in range for loading. Thus, we need to move it to generated ++ if (!parameter.chunkTicketStage.replace(chunk, CHUNK_TICKET_STAGE_TICK, CHUNK_TICKET_STAGE_GENERATED)) { ++ return; ++ } ++ ++ // Since we are possibly downgrading the ticket level, we add the delayed unload ticket so that ++ // the level is kept for a short period of time ++ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( ++ chunk, ++ PLAYER_TICKET_DELAYED, TICK_TICKET_LEVEL, parameter.idBoxed, ++ PLAYER_TICKET, TICK_TICKET_LEVEL, parameter.idBoxed ++ )); ++ // keep chunk at new generated level ++ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addOp( ++ chunk, PLAYER_TICKET, GENERATED_TICKET_LEVEL, parameter.idBoxed ++ )); ++ } ++ }; ++ ++ private static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ, ++ final int sendRadius) { ++ // expect sendRadius to be = 1 + target viewable radius ++ return ChunkTrackingView.isWithinDistance(centerX, centerZ, sendRadius, chunkX, chunkZ, true); ++ } ++ ++ private static int getClientViewDistance(final ServerPlayer player) { ++ final Integer vd = player.requestedViewDistance(); ++ return vd == null ? -1 : Math.max(0, vd.intValue()); ++ } ++ ++ private static int getTickDistance(final int playerTickViewDistance, final int worldTickViewDistance, ++ final int playerLoadViewDistance, final int worldLoadViewDistance) { ++ return Math.min( ++ playerTickViewDistance < 0 ? worldTickViewDistance : playerTickViewDistance, ++ playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance ++ ); ++ } ++ ++ private static int getLoadViewDistance(final int tickViewDistance, final int playerLoadViewDistance, ++ final int worldLoadViewDistance) { ++ return Math.max(tickViewDistance + 1, playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance); ++ } ++ ++ private static int getSendViewDistance(final int loadViewDistance, final int clientViewDistance, ++ final int playerSendViewDistance, final int worldSendViewDistance) { ++ return Math.min( ++ loadViewDistance - 1, ++ playerSendViewDistance < 0 ? (!io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance ++ ); ++ } ++ ++ private Packet<?> updateClientChunkRadius(final int radius) { ++ this.lastSentChunkRadius = radius; ++ return new ClientboundSetChunkCacheRadiusPacket(radius); ++ } ++ ++ private Packet<?> updateClientSimulationDistance(final int distance) { ++ this.lastSentSimulationDistance = distance; ++ return new ClientboundSetSimulationDistancePacket(distance); ++ } ++ ++ private Packet<?> updateClientChunkCenter(final int chunkX, final int chunkZ) { ++ this.lastSentChunkCenterX = chunkX; ++ this.lastSentChunkCenterZ = chunkZ; ++ return new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ); ++ } ++ ++ private boolean canPlayerGenerateChunks() { ++ return !this.player.isSpectator() || this.world.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS); ++ } ++ ++ private double getMaxChunkLoadRate() { ++ final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate; ++ ++ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); ++ } ++ ++ private double getMaxChunkGenRate() { ++ final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate; ++ ++ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); ++ } ++ ++ private double getMaxChunkSendRate() { ++ final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate; ++ ++ return configRate <= 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); ++ } ++ ++ private long getMaxChunkLoads() { ++ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); ++ long configLimit = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads; ++ if (configLimit == 0L) { ++ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active ++ configLimit = Math.max(5L, radiusChunks / 5L); ++ } else if (configLimit < 0L) { ++ configLimit = Integer.MAX_VALUE; ++ } // else: use the value configured ++ configLimit = configLimit - this.loadingQueue.size(); ++ ++ return configLimit; ++ } ++ ++ private long getMaxChunkGenerates() { ++ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); ++ long configLimit = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates; ++ if (configLimit == 0L) { ++ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active ++ configLimit = Math.max(5L, radiusChunks / 5L); ++ } else if (configLimit < 0L) { ++ configLimit = Integer.MAX_VALUE; ++ } // else: use the value configured ++ configLimit = configLimit - this.generatingQueue.size(); ++ ++ return configLimit; ++ } ++ ++ private boolean wantChunkSent(final int chunkX, final int chunkZ) { ++ final int dx = this.lastChunkX - chunkX; ++ final int dz = this.lastChunkZ - chunkZ; ++ return (Math.max(Math.abs(dx), Math.abs(dz)) <= (this.lastSendDistance + 1)) && wantChunkLoaded( ++ this.lastChunkX, this.lastChunkZ, chunkX, chunkZ, this.lastSendDistance ++ ); ++ } ++ ++ private boolean wantChunkTicked(final int chunkX, final int chunkZ) { ++ final int dx = this.lastChunkX - chunkX; ++ final int dz = this.lastChunkZ - chunkZ; ++ return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance; ++ } ++ ++ private boolean areNeighboursGenerated(final int chunkX, final int chunkZ, final int radius) { ++ for (int dz = -radius; dz <= radius; ++dz) { ++ for (int dx = -radius; dx <= radius; ++dx) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ ++ final long neighbour = CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ); ++ final byte stage = this.chunkTicketStage.get(neighbour); ++ ++ if (stage != CHUNK_TICKET_STAGE_GENERATED && stage != CHUNK_TICKET_STAGE_TICK) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ void updateQueues(final long time) { ++ io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async"); ++ if (this.removed) { ++ throw new IllegalStateException("Ticking removed player chunk loader"); ++ } ++ // update rate limits ++ final double loadRate = this.getMaxChunkLoadRate(); ++ final double genRate = this.getMaxChunkGenRate(); ++ final double sendRate = this.getMaxChunkSendRate(); ++ ++ this.chunkLoadTicketLimiter.tickAllocation(time, loadRate, loadRate); ++ this.chunkGenerateTicketLimiter.tickAllocation(time, genRate, genRate); ++ this.chunkSendLimiter.tickAllocation(time, sendRate, sendRate); ++ ++ // try to progress chunk loads ++ while (!this.loadingQueue.isEmpty()) { ++ final long pendingLoadChunk = this.loadingQueue.firstLong(); ++ final int pendingChunkX = CoordinateUtils.getChunkX(pendingLoadChunk); ++ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingLoadChunk); ++ final ChunkAccess pending = ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(pendingChunkX, pendingChunkZ); ++ if (pending == null) { ++ // nothing to do here ++ break; ++ } ++ // chunk has loaded, so we can take it out of the queue ++ this.loadingQueue.dequeueLong(); ++ ++ // try to move to generate queue ++ final byte prev = this.chunkTicketStage.put(pendingLoadChunk, CHUNK_TICKET_STAGE_LOADED); ++ if (prev != CHUNK_TICKET_STAGE_LOADING) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADING + ", not " + prev); ++ } ++ ++ if (this.canGenerateChunks || this.isLoadedChunkGeneratable(pending)) { ++ this.genQueue.enqueue(pendingLoadChunk); ++ } // else: don't want to generate, so just leave it loaded ++ } ++ ++ // try to push more chunk loads ++ final long maxLoads = Math.max(0L, Math.min(MAX_RATE, Math.min(this.loadQueue.size(), this.getMaxChunkLoads()))); ++ final int maxLoadsThisTick = (int)this.chunkLoadTicketLimiter.takeAllocation(time, loadRate, maxLoads); ++ if (maxLoadsThisTick > 0) { ++ final LongArrayList chunks = new LongArrayList(maxLoadsThisTick); ++ for (int i = 0; i < maxLoadsThisTick; ++i) { ++ final long chunk = this.loadQueue.dequeueLong(); ++ final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_LOADING); ++ if (prev != CHUNK_TICKET_STAGE_NONE) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_NONE + ", not " + prev); ++ } ++ this.pushDelayedTicketOp( ++ ChunkHolderManager.TicketOperation.addOp( ++ chunk, ++ PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed ++ ) ++ ); ++ chunks.add(chunk); ++ this.loadingQueue.enqueue(chunk); ++ } ++ ++ // here we need to flush tickets, as scheduleChunkLoad requires tickets to be propagated with addTicket = false ++ this.flushDelayedTicketOps(); ++ // we only need to call scheduleChunkLoad because the loaded ticket level is not enough to start the chunk ++ // load - only generate ticket levels start anything, but they start generation... ++ // propagate levels ++ // Note: this CAN call plugin logic, so it is VITAL that our bookkeeping logic is completely done by the time this is invoked ++ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); ++ ++ if (this.removed) { ++ // process ticket updates may invoke plugin logic, which may remove this player ++ return; ++ } ++ ++ for (int i = 0; i < maxLoadsThisTick; ++i) { ++ final long queuedLoadChunk = chunks.getLong(i); ++ final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk); ++ final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk); ++ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkLoad( ++ queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, PrioritisedExecutor.Priority.NORMAL, null ++ ); ++ if (this.removed) { ++ return; ++ } ++ } ++ } ++ ++ // try to progress chunk generations ++ while (!this.generatingQueue.isEmpty()) { ++ final long pendingGenChunk = this.generatingQueue.firstLong(); ++ final int pendingChunkX = CoordinateUtils.getChunkX(pendingGenChunk); ++ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingGenChunk); ++ final LevelChunk pending = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingChunkX, pendingChunkZ); ++ if (pending == null) { ++ // nothing to do here ++ break; ++ } ++ ++ // chunk has generated, so we can take it out of queue ++ this.generatingQueue.dequeueLong(); ++ ++ final byte prev = this.chunkTicketStage.put(pendingGenChunk, CHUNK_TICKET_STAGE_GENERATED); ++ if (prev != CHUNK_TICKET_STAGE_GENERATING) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATING + ", not " + prev); ++ } ++ ++ // try to move to send queue ++ if (this.wantChunkSent(pendingChunkX, pendingChunkZ)) { ++ this.sendQueue.enqueue(pendingGenChunk); ++ } ++ // try to move to tick queue ++ if (this.wantChunkTicked(pendingChunkX, pendingChunkZ)) { ++ this.tickingQueue.enqueue(pendingGenChunk); ++ } ++ } ++ ++ // try to push more chunk generations ++ final long maxGens = Math.max(0L, Math.min(MAX_RATE, Math.min(this.genQueue.size(), this.getMaxChunkGenerates()))); ++ // preview the allocations, as we may not actually utilise all of them ++ final long maxGensThisTick = this.chunkGenerateTicketLimiter.previewAllocation(time, genRate, maxGens); ++ long ratedGensThisTick = 0L; ++ while (!this.genQueue.isEmpty()) { ++ final long chunkKey = this.genQueue.firstLong(); ++ final int chunkX = CoordinateUtils.getChunkX(chunkKey); ++ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey); ++ final ChunkAccess chunk = ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ); ++ if (chunk.getPersistedStatus() != ChunkStatus.FULL) { ++ // only rate limit actual generations ++ if ((ratedGensThisTick + 1L) > maxGensThisTick) { ++ break; ++ } ++ ++ratedGensThisTick; ++ } ++ ++ this.genQueue.dequeueLong(); ++ ++ final byte prev = this.chunkTicketStage.put(chunkKey, CHUNK_TICKET_STAGE_GENERATING); ++ if (prev != CHUNK_TICKET_STAGE_LOADED) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADED + ", not " + prev); ++ } ++ this.pushDelayedTicketOp( ++ ChunkHolderManager.TicketOperation.addAndRemove( ++ chunkKey, ++ PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed, ++ PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed ++ ) ++ ); ++ this.generatingQueue.enqueue(chunkKey); ++ } ++ // take the allocations we actually used ++ this.chunkGenerateTicketLimiter.takeAllocation(time, genRate, ratedGensThisTick); ++ ++ // try to pull ticking chunks ++ while (!this.tickingQueue.isEmpty()) { ++ final long pendingTicking = this.tickingQueue.firstLong(); ++ final int pendingChunkX = CoordinateUtils.getChunkX(pendingTicking); ++ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingTicking); ++ ++ if (!this.areNeighboursGenerated(pendingChunkX, pendingChunkZ, ++ ChunkHolderManager.FULL_LOADED_TICKET_LEVEL - ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL)) { ++ break; ++ } ++ ++ // only gets here if all neighbours were marked as generated or ticking themselves ++ this.tickingQueue.dequeueLong(); ++ this.pushDelayedTicketOp( ++ ChunkHolderManager.TicketOperation.addAndRemove( ++ pendingTicking, ++ PLAYER_TICKET, TICK_TICKET_LEVEL, this.idBoxed, ++ PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed ++ ) ++ ); ++ // note: there is no queue to add after ticking ++ final byte prev = this.chunkTicketStage.put(pendingTicking, CHUNK_TICKET_STAGE_TICK); ++ if (prev != CHUNK_TICKET_STAGE_GENERATED) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATED + ", not " + prev); ++ } ++ } ++ ++ // try to pull sending chunks ++ final long maxSends = Math.max(0L, Math.min(MAX_RATE, Integer.MAX_VALUE)); // note: no logic to track concurrent sends ++ final int maxSendsThisTick = Math.min((int)this.chunkSendLimiter.takeAllocation(time, sendRate, maxSends), this.sendQueue.size()); ++ // we do not return sends that we took from the allocation back because we want to limit the max send rate, not target it ++ for (int i = 0; i < maxSendsThisTick; ++i) { ++ final long pendingSend = this.sendQueue.firstLong(); ++ final int pendingSendX = CoordinateUtils.getChunkX(pendingSend); ++ final int pendingSendZ = CoordinateUtils.getChunkZ(pendingSend); ++ final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(pendingSendX, pendingSendZ); ++ if (!this.areNeighboursGenerated(pendingSendX, pendingSendZ, 1) || !io.papermc.paper.util.TickThread.isTickThreadFor(this.world, pendingSendX, pendingSendZ)) { ++ // nothing to do ++ // the target chunk may not be owned by this region, but this should be resolved in the future ++ break; ++ } ++ if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) { ++ // not yet post-processed, need to do this so that tile entities can properly be sent to clients ++ chunk.postProcessGeneration(); ++ // check if there was any recursive action ++ if (this.removed || this.sendQueue.isEmpty() || this.sendQueue.firstLong() != pendingSend) { ++ return; ++ } // else: good to dequeue and send, fall through ++ } ++ this.sendQueue.dequeueLong(); ++ ++ this.sendChunk(pendingSendX, pendingSendZ); ++ ++ if (this.removed) { ++ // sendChunk may invoke plugin logic ++ return; ++ } ++ } ++ ++ this.flushDelayedTicketOps(); ++ } ++ ++ void add() { ++ io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot add player asynchronously"); ++ if (this.removed) { ++ throw new IllegalStateException("Adding removed player chunk loader"); ++ } ++ final ViewDistances playerDistances = ((ChunkSystemServerPlayer)this.player).moonrise$getViewDistanceHolder().getViewDistances(); ++ final ViewDistances worldDistances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); ++ final int chunkX = this.player.chunkPosition().x; ++ final int chunkZ = this.player.chunkPosition().z; ++ ++ final int tickViewDistance = getTickDistance( ++ playerDistances.tickViewDistance, worldDistances.tickViewDistance, ++ playerDistances.loadViewDistance, worldDistances.loadViewDistance ++ ); ++ // load view cannot be less-than tick view + 1 ++ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance); ++ // send view cannot be greater-than load view ++ final int clientViewDistance = getClientViewDistance(this.player); ++ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); ++ ++ // TODO check PlayerList diff in paper chunk system patch ++ // send view distances ++ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); ++ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); ++ ++ // add to distance maps ++ this.broadcastMap.add(chunkX, chunkZ, sendViewDistance + 1); ++ this.loadTicketCleanup.add(chunkX, chunkZ, loadViewDistance + 1); ++ this.tickMap.add(chunkX, chunkZ, tickViewDistance); ++ ++ // update chunk center ++ this.player.connection.send(this.updateClientChunkCenter(chunkX, chunkZ)); ++ ++ // reset limiters, they will start at a zero allocation ++ final long time = System.nanoTime(); ++ this.chunkLoadTicketLimiter.reset(time); ++ this.chunkGenerateTicketLimiter.reset(time); ++ this.chunkSendLimiter.reset(time); ++ ++ // now we can update ++ this.update(); ++ } ++ ++ private boolean isLoadedChunkGeneratable(final int chunkX, final int chunkZ) { ++ return this.isLoadedChunkGeneratable(((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ)); ++ } ++ ++ private boolean isLoadedChunkGeneratable(final ChunkAccess chunkAccess) { ++ final BelowZeroRetrogen belowZeroRetrogen; ++ // see PortalForcer#findPortalAround ++ return chunkAccess != null && ( ++ chunkAccess.getPersistedStatus() == ChunkStatus.FULL || ++ ((belowZeroRetrogen = chunkAccess.getBelowZeroRetrogen()) != null && belowZeroRetrogen.targetStatus().isOrAfter(ChunkStatus.SPAWN)) ++ ); ++ } ++ ++ void update() { ++ io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot update player asynchronously"); ++ if (this.removed) { ++ throw new IllegalStateException("Updating removed player chunk loader"); ++ } ++ final ViewDistances playerDistances = ((ChunkSystemServerPlayer)this.player).moonrise$getViewDistanceHolder().getViewDistances(); ++ final ViewDistances worldDistances = ((ChunkSystemServerLevel)this.world).moonrise$getViewDistanceHolder().getViewDistances(); ++ ++ final int tickViewDistance = getTickDistance( ++ playerDistances.tickViewDistance, worldDistances.tickViewDistance, ++ playerDistances.loadViewDistance, worldDistances.loadViewDistance ++ ); ++ // load view cannot be less-than tick view + 1 ++ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance); ++ // send view cannot be greater-than load view ++ final int clientViewDistance = getClientViewDistance(this.player); ++ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); ++ ++ final ChunkPos playerPos = this.player.chunkPosition(); ++ final boolean canGenerateChunks = this.canPlayerGenerateChunks(); ++ final int currentChunkX = playerPos.x; ++ final int currentChunkZ = playerPos.z; ++ ++ final int prevChunkX = this.lastChunkX; ++ final int prevChunkZ = this.lastChunkZ; ++ ++ if ( ++ // has view distance stayed the same? ++ sendViewDistance == this.lastSendDistance ++ && loadViewDistance == this.lastLoadDistance ++ && tickViewDistance == this.lastTickDistance ++ ++ // has our chunk stayed the same? ++ && prevChunkX == currentChunkX ++ && prevChunkZ == currentChunkZ ++ ++ // can we still generate chunks? ++ && this.canGenerateChunks == canGenerateChunks ++ ) { ++ // nothing we care about changed, so we're not re-calculating ++ return; ++ } ++ ++ // update distance maps ++ this.broadcastMap.update(currentChunkX, currentChunkZ, sendViewDistance + 1); ++ this.loadTicketCleanup.update(currentChunkX, currentChunkZ, loadViewDistance + 1); ++ this.tickMap.update(currentChunkX, currentChunkZ, tickViewDistance); ++ if (sendViewDistance > loadViewDistance || tickViewDistance > loadViewDistance) { ++ throw new IllegalStateException(); ++ } ++ ++ // update VDs for client ++ // this should be after the distance map updates, as they will send unload packets ++ if (this.lastSentChunkRadius != sendViewDistance) { ++ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); ++ } ++ if (this.lastSentSimulationDistance != tickViewDistance) { ++ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); ++ } ++ ++ this.sendQueue.clear(); ++ this.tickingQueue.clear(); ++ this.generatingQueue.clear(); ++ this.genQueue.clear(); ++ this.loadingQueue.clear(); ++ this.loadQueue.clear(); ++ ++ this.lastChunkX = currentChunkX; ++ this.lastChunkZ = currentChunkZ; ++ this.lastSendDistance = sendViewDistance; ++ this.lastLoadDistance = loadViewDistance; ++ this.lastTickDistance = tickViewDistance; ++ this.canGenerateChunks = canGenerateChunks; ++ ++ // +1 since we need to load chunks +1 around the load view distance... ++ final long[] toIterate = ParallelSearchRadiusIteration.getSearchIteration(loadViewDistance + 1); ++ // the iteration order is by increasing manhattan distance - so, we do NOT need to ++ // sort anything in the queue! ++ for (final long deltaChunk : toIterate) { ++ final int dx = CoordinateUtils.getChunkX(deltaChunk); ++ final int dz = CoordinateUtils.getChunkZ(deltaChunk); ++ final int chunkX = dx + currentChunkX; ++ final int chunkZ = dz + currentChunkZ; ++ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); ++ final int manhattanDistance = Math.abs(dx) + Math.abs(dz); ++ ++ // since chunk sending is not by radius alone, we need an extra check here to account for ++ // everything <= sendDistance ++ // Note: Vanilla may want to send chunks outside the send view distance, so we do need ++ // the dist <= view check ++ final boolean sendChunk = (squareDistance <= (sendViewDistance + 1)) ++ && wantChunkLoaded(currentChunkX, currentChunkZ, chunkX, chunkZ, sendViewDistance); ++ final boolean sentChunk = sendChunk ? this.sentChunks.contains(chunk) : this.sentChunks.remove(chunk); ++ ++ if (!sendChunk && sentChunk) { ++ // have sent the chunk, but don't want it anymore ++ // unload it now ++ this.sendUnloadChunkRaw(chunkX, chunkZ); ++ } ++ ++ final byte stage = this.chunkTicketStage.get(chunk); ++ switch (stage) { ++ case CHUNK_TICKET_STAGE_NONE: { ++ // we want the chunk to be at least loaded ++ this.loadQueue.enqueue(chunk); ++ break; ++ } ++ case CHUNK_TICKET_STAGE_LOADING: { ++ this.loadingQueue.enqueue(chunk); ++ break; ++ } ++ case CHUNK_TICKET_STAGE_LOADED: { ++ if (canGenerateChunks || this.isLoadedChunkGeneratable(chunkX, chunkZ)) { ++ this.genQueue.enqueue(chunk); ++ } ++ break; ++ } ++ case CHUNK_TICKET_STAGE_GENERATING: { ++ this.generatingQueue.enqueue(chunk); ++ break; ++ } ++ case CHUNK_TICKET_STAGE_GENERATED: { ++ if (sendChunk && !sentChunk) { ++ this.sendQueue.enqueue(chunk); ++ } ++ if (squareDistance <= tickViewDistance) { ++ this.tickingQueue.enqueue(chunk); ++ } ++ break; ++ } ++ case CHUNK_TICKET_STAGE_TICK: { ++ if (sendChunk && !sentChunk) { ++ this.sendQueue.enqueue(chunk); ++ } ++ break; ++ } ++ default: { ++ throw new IllegalStateException("Unknown stage: " + stage); ++ } ++ } ++ } ++ ++ // update the chunk center ++ // this must be done last so that the client does not ignore any of our unload chunk packets above ++ if (this.lastSentChunkCenterX != currentChunkX || this.lastSentChunkCenterZ != currentChunkZ) { ++ this.player.connection.send(this.updateClientChunkCenter(currentChunkX, currentChunkZ)); ++ } ++ ++ this.flushDelayedTicketOps(); ++ } ++ ++ void remove() { ++ io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot add player asynchronously"); ++ if (this.removed) { ++ throw new IllegalStateException("Removing removed player chunk loader"); ++ } ++ this.removed = true; ++ // sends the chunk unload packets ++ this.broadcastMap.remove(); ++ // cleans up loading/generating tickets ++ this.loadTicketCleanup.remove(); ++ // cleans up ticking tickets ++ this.tickMap.remove(); ++ ++ // purge queues ++ this.sendQueue.clear(); ++ this.tickingQueue.clear(); ++ this.generatingQueue.clear(); ++ this.genQueue.clear(); ++ this.loadingQueue.clear(); ++ this.loadQueue.clear(); ++ ++ // flush ticket changes ++ this.flushDelayedTicketOps(); ++ ++ // now all tickets should be removed, which is all of our external state ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java +@@ -0,0 +1,144 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.queue; ++ ++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonElement; ++import com.google.gson.JsonObject; ++import it.unimi.dsi.fastutil.longs.LongIterator; ++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; ++import java.util.ArrayList; ++import java.util.Iterator; ++import java.util.List; ++import java.util.concurrent.atomic.AtomicLong; ++ ++public final class ChunkUnloadQueue { ++ ++ public final int coordinateShift; ++ private final AtomicLong orderGenerator = new AtomicLong(); ++ private final ConcurrentLong2ReferenceChainedHashTable<UnloadSection> unloadSections = new ConcurrentLong2ReferenceChainedHashTable<>(); ++ ++ /* ++ * Note: write operations do not occur in parallel for any given section. ++ * Note: coordinateShift <= region shift in order for retrieveForCurrentRegion() to function correctly ++ */ ++ ++ public ChunkUnloadQueue(final int coordinateShift) { ++ this.coordinateShift = coordinateShift; ++ } ++ ++ public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {} ++ ++ public List<SectionToUnload> retrieveForAllRegions() { ++ final List<SectionToUnload> ret = new ArrayList<>(); ++ ++ for (final Iterator<ConcurrentLong2ReferenceChainedHashTable.TableEntry<UnloadSection>> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) { ++ final ConcurrentLong2ReferenceChainedHashTable.TableEntry<UnloadSection> entry = iterator.next(); ++ final long key = entry.getKey(); ++ final UnloadSection section = entry.getValue(); ++ final int sectionX = CoordinateUtils.getChunkX(key); ++ final int sectionZ = CoordinateUtils.getChunkZ(key); ++ ++ ret.add(new SectionToUnload(sectionX, sectionZ, section.order, section.chunks.size())); ++ } ++ ++ ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> { ++ return Long.compare(s1.order, s2.order); ++ }); ++ ++ return ret; ++ } ++ ++ public UnloadSection getSectionUnsynchronized(final int sectionX, final int sectionZ) { ++ return this.unloadSections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ)); ++ } ++ ++ public UnloadSection removeSection(final int sectionX, final int sectionZ) { ++ return this.unloadSections.remove(CoordinateUtils.getChunkKey(sectionX, sectionZ)); ++ } ++ ++ // write operation ++ public boolean addChunk(final int chunkX, final int chunkZ) { ++ // write operations do not occur in parallel for a given section ++ final int shift = this.coordinateShift; ++ final int sectionX = chunkX >> shift; ++ final int sectionZ = chunkZ >> shift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ UnloadSection section = this.unloadSections.get(sectionKey); ++ if (section == null) { ++ section = new UnloadSection(this.orderGenerator.getAndIncrement()); ++ this.unloadSections.put(sectionKey, section); ++ } ++ ++ return section.chunks.add(chunkKey); ++ } ++ ++ // write operation ++ public boolean removeChunk(final int chunkX, final int chunkZ) { ++ // write operations do not occur in parallel for a given section ++ final int shift = this.coordinateShift; ++ final int sectionX = chunkX >> shift; ++ final int sectionZ = chunkZ >> shift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ final UnloadSection section = this.unloadSections.get(sectionKey); ++ ++ if (section == null) { ++ return false; ++ } ++ ++ if (!section.chunks.remove(chunkKey)) { ++ return false; ++ } ++ ++ if (section.chunks.isEmpty()) { ++ this.unloadSections.remove(sectionKey); ++ } ++ ++ return true; ++ } ++ ++ public JsonElement toDebugJson() { ++ final JsonArray ret = new JsonArray(); ++ ++ for (final SectionToUnload section : this.retrieveForAllRegions()) { ++ final JsonObject sectionJson = new JsonObject(); ++ ret.add(sectionJson); ++ ++ sectionJson.addProperty("sectionX", section.sectionX()); ++ sectionJson.addProperty("sectionZ", section.sectionX()); ++ sectionJson.addProperty("order", section.order()); ++ ++ final JsonArray coordinates = new JsonArray(); ++ sectionJson.add("coordinates", coordinates); ++ ++ final UnloadSection actualSection = this.getSectionUnsynchronized(section.sectionX(), section.sectionZ()); ++ if (actualSection != null) { ++ for (final LongIterator iterator = actualSection.chunks.clone().iterator(); iterator.hasNext(); ) { ++ final long coordinate = iterator.nextLong(); ++ ++ final JsonObject coordinateJson = new JsonObject(); ++ coordinates.add(coordinateJson); ++ ++ coordinateJson.addProperty("chunkX", Integer.valueOf(CoordinateUtils.getChunkX(coordinate))); ++ coordinateJson.addProperty("chunkZ", Integer.valueOf(CoordinateUtils.getChunkZ(coordinate))); ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public static final class UnloadSection { ++ ++ public final long order; ++ public final LongLinkedOpenHashSet chunks = new LongLinkedOpenHashSet(); ++ ++ public UnloadSection(final long order) { ++ this.order = order; ++ } ++ } ++} +\ No newline at end of file +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1030e1b059f7ccdedd9c00527c3dd20c9cb81f42 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +@@ -0,0 +1,1427 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; ++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.MoonriseCommon; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; ++import ca.spottedleaf.moonrise.patches.chunk_system.queue.ChunkUnloadQueue; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.GenericDataLoadTask; ++import ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket; ++import ca.spottedleaf.moonrise.patches.chunk_system.util.ChunkSystemSortedArraySet; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonObject; ++import com.mojang.logging.LogUtils; ++import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ByteMap; ++import it.unimi.dsi.fastutil.longs.Long2IntMap; ++import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import it.unimi.dsi.fastutil.longs.LongIterator; ++import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ChunkLevel; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.Ticket; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.util.SortedArraySet; ++import net.minecraft.util.Unit; ++import net.minecraft.world.level.ChunkPos; ++import org.slf4j.Logger; ++import java.io.IOException; ++import java.text.DecimalFormat; ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.Collection; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Objects; ++import java.util.PrimitiveIterator; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicReference; ++import java.util.concurrent.locks.LockSupport; ++import java.util.function.Predicate; ++ ++public final class ChunkHolderManager { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ public static final int FULL_LOADED_TICKET_LEVEL = ChunkLevel.FULL_CHUNK_LEVEL; ++ public static final int BLOCK_TICKING_TICKET_LEVEL = ChunkLevel.BLOCK_TICKING_LEVEL; ++ public static final int ENTITY_TICKING_TICKET_LEVEL = ChunkLevel.ENTITY_TICKING_LEVEL; ++ public static final int MAX_TICKET_LEVEL = ChunkLevel.MAX_LEVEL; // inclusive ++ ++ public static final TicketType<Unit> UNLOAD_COOLDOWN = TicketType.create("unload_cooldown", (u1, u2) -> 0, 5 * 20); ++ ++ private static final long NO_TIMEOUT_MARKER = Long.MIN_VALUE; ++ private static final long PROBE_MARKER = Long.MIN_VALUE + 1; ++ public final ReentrantAreaLock ticketLockArea; ++ ++ private final ConcurrentLong2ReferenceChainedHashTable<SortedArraySet<Ticket<?>>> tickets = new ConcurrentLong2ReferenceChainedHashTable<>(); ++ private final ConcurrentLong2ReferenceChainedHashTable<Long2IntOpenHashMap> sectionToChunkToExpireCount = new ConcurrentLong2ReferenceChainedHashTable<>(); ++ final ChunkUnloadQueue unloadQueue; ++ ++ private final ConcurrentLong2ReferenceChainedHashTable<NewChunkHolder> chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f); ++ private final ServerLevel world; ++ private final ChunkTaskScheduler taskScheduler; ++ private long currentTick; ++ ++ private final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>(); ++ private final ObjectRBTreeSet<NewChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { ++ if (c1 == c2) { ++ return 0; ++ } ++ ++ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); ++ ++ if (saveTickCompare != 0) { ++ return saveTickCompare; ++ } ++ ++ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); ++ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); ++ ++ if (coord1 == coord2) { ++ throw new IllegalStateException("Duplicate chunkholder in auto save queue"); ++ } ++ ++ return Long.compare(coord1, coord2); ++ }); ++ ++ public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { ++ this.world = world; ++ this.taskScheduler = taskScheduler; ++ this.ticketLockArea = new ReentrantAreaLock(taskScheduler.getChunkSystemLockShift()); ++ this.unloadQueue = new ChunkUnloadQueue(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift()); ++ } ++ ++ public boolean processTicketUpdates(final int posX, final int posZ) { ++ final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT; ++ final int ticketMask = (1 << ticketShift) - 1; ++ final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>(); ++ final List<NewChunkHolder> changedFullStatus = new ArrayList<>(); ++ final boolean ret; ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( ++ ((posX >> ticketShift) - 1) << ticketShift, ++ ((posZ >> ticketShift) - 1) << ticketShift, ++ (((posX >> ticketShift) + 1) << ticketShift) | ticketMask, ++ (((posZ >> ticketShift) + 1) << ticketShift) | ticketMask ++ ); ++ try { ++ ret = this.processTicketUpdatesNoLock(posX >> ticketShift, posZ >> ticketShift, scheduledTasks, changedFullStatus); ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ ++ this.addChangedStatuses(changedFullStatus); ++ ++ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) { ++ scheduledTasks.get(i).schedule(); ++ } ++ ++ return ret; ++ } ++ ++ private boolean processTicketUpdatesNoLock(final int sectionX, final int sectionZ, final List<ChunkProgressionTask> scheduledTasks, ++ final List<NewChunkHolder> changedFullStatus) { ++ return this.ticketLevelPropagator.performUpdate( ++ sectionX, sectionZ, this.taskScheduler.schedulingLockArea, scheduledTasks, changedFullStatus ++ ); ++ } ++ ++ public List<ChunkHolder> getOldChunkHolders() { ++ final List<ChunkHolder> ret = new ArrayList<>(this.chunkHolders.size() + 1); ++ for (final Iterator<NewChunkHolder> iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) { ++ ret.add(iterator.next().vanillaChunkHolder); ++ } ++ return ret; ++ } ++ ++ public List<NewChunkHolder> getChunkHolders() { ++ final List<NewChunkHolder> ret = new ArrayList<>(this.chunkHolders.size() + 1); ++ for (final Iterator<NewChunkHolder> iterator = this.chunkHolders.valueIterator(); iterator.hasNext();) { ++ ret.add(iterator.next()); ++ } ++ return ret; ++ } ++ ++ public int size() { ++ return this.chunkHolders.size(); ++ } ++ ++ // TODO replace the need for this, specifically: optimise ServerChunkCache#tickChunks ++ public Iterable<ChunkHolder> getOldChunkHoldersIterable() { ++ return new Iterable<ChunkHolder>() { ++ @Override ++ public Iterator<ChunkHolder> iterator() { ++ final Iterator<NewChunkHolder> iterator = ChunkHolderManager.this.chunkHolders.valueIterator(); ++ return new Iterator<ChunkHolder>() { ++ @Override ++ public boolean hasNext() { ++ return iterator.hasNext(); ++ } ++ ++ @Override ++ public ChunkHolder next() { ++ return iterator.next().vanillaChunkHolder; ++ } ++ }; ++ } ++ }; ++ } ++ ++ public void close(final boolean save, final boolean halt) { ++ io.papermc.paper.util.TickThread.ensureTickThread("Closing world off-main"); ++ if (halt) { ++ LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'"); ++ if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) { ++ LOGGER.warn("Failed to halt world generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'"); ++ } else { ++ LOGGER.info("Halted chunk system for world '" + WorldUtil.getWorldName(this.world) + "'"); ++ } ++ } ++ ++ if (save) { ++ this.saveAllChunks(true, true, true); ++ } ++ ++ boolean hasTasks = false; ++ for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) { ++ if (RegionFileIOThread.getControllerFor(this.world, type).hasTasks()) { ++ hasTasks = true; ++ break; ++ } ++ } ++ if (hasTasks) { ++ RegionFileIOThread.flush(); ++ } ++ ++ // kill regionfile cache ++ for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) { ++ try { ++ RegionFileIOThread.getControllerFor(this.world, type).getCache().close(); ++ } catch (final IOException ex) { ++ LOGGER.error("Failed to close '" + type.name() + "' regionfile cache for world '" + WorldUtil.getWorldName(this.world) + "'", ex); ++ } ++ } ++ } ++ ++ void ensureInAutosave(final NewChunkHolder holder) { ++ if (!this.autoSaveQueue.contains(holder)) { ++ holder.lastAutoSave = this.currentTick; ++ this.autoSaveQueue.add(holder); ++ } ++ } ++ ++ public void autoSave() { ++ final List<NewChunkHolder> reschedule = new ArrayList<>(); ++ final long currentTick = this.currentTick; ++ final long maxSaveTime = currentTick - Math.max(1L, this.world.paperConfig().chunks.autoSaveInterval.value()); ++ final int maxToSave = this.world.paperConfig().chunks.maxAutoSaveChunksPerTick; ++ for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) { ++ final NewChunkHolder holder = this.autoSaveQueue.first(); ++ ++ if (holder.lastAutoSave > maxSaveTime) { ++ break; ++ } ++ ++ this.autoSaveQueue.remove(holder); ++ ++ holder.lastAutoSave = currentTick; ++ if (holder.save(false) != null) { ++ ++autoSaved; ++ } ++ ++ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { ++ reschedule.add(holder); ++ } ++ } ++ ++ for (final NewChunkHolder holder : reschedule) { ++ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { ++ this.autoSaveQueue.add(holder); ++ } ++ } ++ } ++ ++ public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) { ++ final List<NewChunkHolder> holders = this.getChunkHolders(); ++ ++ if (logProgress) { ++ LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'"); ++ } ++ ++ final DecimalFormat format = new DecimalFormat("#0.00"); ++ ++ int saved = 0; ++ ++ long start = System.nanoTime(); ++ long lastLog = start; ++ boolean needsFlush = false; ++ final int flushInterval = 50; ++ ++ int savedChunk = 0; ++ int savedEntity = 0; ++ int savedPoi = 0; ++ ++ for (int i = 0, len = holders.size(); i < len; ++i) { ++ final NewChunkHolder holder = holders.get(i); ++ try { ++ final NewChunkHolder.SaveStat saveStat = holder.save(shutdown); ++ if (saveStat != null) { ++ ++saved; ++ needsFlush = flush; ++ if (saveStat.savedChunk()) { ++ ++savedChunk; ++ } ++ if (saveStat.savedEntityChunk()) { ++ ++savedEntity; ++ } ++ if (saveStat.savedPoiChunk()) { ++ ++savedPoi; ++ } ++ } ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save chunk (" + holder.chunkX + "," + holder.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'", thr); ++ } ++ if (needsFlush && (saved % flushInterval) == 0) { ++ needsFlush = false; ++ RegionFileIOThread.partialFlush(flushInterval / 2); ++ } ++ if (logProgress) { ++ final long currTime = System.nanoTime(); ++ if ((currTime - lastLog) > TimeUnit.SECONDS.toNanos(10L)) { ++ lastLog = currTime; ++ LOGGER.info("Saved " + saved + " chunks (" + format.format((double)(i+1)/(double)len * 100.0) + "%) in world '" + WorldUtil.getWorldName(this.world) + "'"); ++ } ++ } ++ } ++ if (flush) { ++ RegionFileIOThread.flush(); ++ try { ++ RegionFileIOThread.flushRegionStorages(this.world); ++ } catch (final IOException ex) { ++ LOGGER.error("Exception when flushing regions in world '" + WorldUtil.getWorldName(this.world) + "'", ex); ++ } ++ } ++ if (logProgress) { ++ LOGGER.info("Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi + " poi chunks in world '" + WorldUtil.getWorldName(this.world) + "' in " + format.format(1.0E-9 * (System.nanoTime() - start)) + "s"); ++ } ++ } ++ ++ private final ThreadedTicketLevelPropagator ticketLevelPropagator = new ThreadedTicketLevelPropagator() { ++ @Override ++ protected void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates) { ++ // first the necessary chunkholders must be created, so just update the ticket levels ++ for (final Iterator<Long2ByteMap.Entry> iterator = updates.long2ByteEntrySet().fastIterator(); iterator.hasNext();) { ++ final Long2ByteMap.Entry entry = iterator.next(); ++ final long key = entry.getLongKey(); ++ final int newLevel = convertBetweenTicketLevels((int)entry.getByteValue()); ++ ++ NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key); ++ if (current == null && newLevel > MAX_TICKET_LEVEL) { ++ // not loaded and it shouldn't be loaded! ++ iterator.remove(); ++ continue; ++ } ++ ++ final int currentLevel = current == null ? MAX_TICKET_LEVEL + 1 : current.getCurrentTicketLevel(); ++ if (currentLevel == newLevel) { ++ // nothing to do ++ iterator.remove(); ++ continue; ++ } ++ ++ if (current == null) { ++ // must create ++ current = ChunkHolderManager.this.createChunkHolder(key); ++ ChunkHolderManager.this.chunkHolders.put(key, current); ++ current.updateTicketLevel(newLevel); ++ } else { ++ current.updateTicketLevel(newLevel); ++ } ++ } ++ } ++ ++ @Override ++ protected void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List<ChunkProgressionTask> scheduledTasks, ++ final List<NewChunkHolder> changedFullStatus) { ++ final List<ChunkProgressionTask> prev = CURRENT_TICKET_UPDATE_SCHEDULING.get(); ++ CURRENT_TICKET_UPDATE_SCHEDULING.set(scheduledTasks); ++ try { ++ for (final LongIterator iterator = updates.keySet().iterator(); iterator.hasNext();) { ++ final long key = iterator.nextLong(); ++ final NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key); ++ ++ if (current == null) { ++ throw new IllegalStateException("Expected chunk holder to be created"); ++ } ++ ++ current.processTicketLevelUpdate(scheduledTasks, changedFullStatus); ++ } ++ } finally { ++ CURRENT_TICKET_UPDATE_SCHEDULING.set(prev); ++ } ++ } ++ }; ++ // function for converting between ticket levels and propagator levels and vice versa ++ // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects ++ // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator ++ // and the levels we get out of the propagator ++ ++ public static int convertBetweenTicketLevels(final int level) { ++ return ChunkLevel.MAX_LEVEL - level + 1; ++ } ++ ++ public String getTicketDebugString(final long coordinate) { ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate)); ++ try { ++ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coordinate); ++ ++ return tickets != null ? tickets.first().toString() : "no_ticket"; ++ } finally { ++ if (ticketLock != null) { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ } ++ ++ public Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> getTicketsCopy() { ++ final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> ret = new Long2ObjectOpenHashMap<>(); ++ final Long2ObjectOpenHashMap<LongArrayList> sections = new Long2ObjectOpenHashMap<>(); ++ final int sectionShift = this.taskScheduler.getChunkSystemLockShift(); ++ for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) { ++ final long coord = iterator.nextLong(); ++ sections.computeIfAbsent( ++ CoordinateUtils.getChunkKey( ++ CoordinateUtils.getChunkX(coord) >> sectionShift, ++ CoordinateUtils.getChunkZ(coord) >> sectionShift ++ ), ++ (final long keyInMap) -> { ++ return new LongArrayList(); ++ } ++ ).add(coord); ++ } ++ ++ for (final Iterator<Long2ObjectMap.Entry<LongArrayList>> iterator = sections.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry<LongArrayList> entry = iterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ final LongArrayList coordinates = entry.getValue(); ++ ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( ++ CoordinateUtils.getChunkX(sectionKey) << sectionShift, ++ CoordinateUtils.getChunkZ(sectionKey) << sectionShift ++ ); ++ try { ++ for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) { ++ final long coord = iterator2.nextLong(); ++ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coord); ++ if (tickets == null) { ++ // removed before we acquired lock ++ continue; ++ } ++ ret.put(coord, ((ChunkSystemSortedArraySet<Ticket<?>>)tickets).moonrise$copy()); ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ ++ return ret; ++ } ++ ++ // Paper start ++ public Collection<org.bukkit.plugin.Plugin> getPluginChunkTickets(int x, int z) { ++ com.google.common.collect.ImmutableList.Builder<org.bukkit.plugin.Plugin> ret; ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(x, z); ++ try { ++ final long coordinate = CoordinateUtils.getChunkKey(x, z); ++ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coordinate); ++ ++ if (tickets == null) { ++ return java.util.Collections.emptyList(); ++ } ++ ++ ret = com.google.common.collect.ImmutableList.builder(); ++ for (Ticket<?> ticket : tickets) { ++ if (ticket.getType() == TicketType.PLUGIN_TICKET) { ++ ret.add((org.bukkit.plugin.Plugin)ticket.key); ++ } ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ ++ return ret.build(); ++ } ++ // Paper end ++ ++ protected final void updateTicketLevel(final long coordinate, final int ticketLevel) { ++ if (ticketLevel > ChunkLevel.MAX_LEVEL) { ++ this.ticketLevelPropagator.removeSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate)); ++ } else { ++ this.ticketLevelPropagator.setSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate), convertBetweenTicketLevels(ticketLevel)); ++ } ++ } ++ ++ private static int getTicketLevelAt(SortedArraySet<Ticket<?>> tickets) { ++ return !tickets.isEmpty() ? tickets.first().getTicketLevel() : MAX_TICKET_LEVEL + 1; ++ } ++ ++ public <T> boolean addTicketAtLevel(final TicketType<T> type, final ChunkPos chunkPos, final int level, ++ final T identifier) { ++ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); ++ } ++ ++ public <T> boolean addTicketAtLevel(final TicketType<T> type, final int chunkX, final int chunkZ, final int level, ++ final T identifier) { ++ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); ++ } ++ ++ private void addExpireCount(final int chunkX, final int chunkZ) { ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); ++ final long sectionKey = CoordinateUtils.getChunkKey( ++ chunkX >> sectionShift, ++ chunkZ >> sectionShift ++ ); ++ ++ this.sectionToChunkToExpireCount.computeIfAbsent(sectionKey, (final long keyInMap) -> { ++ return new Long2IntOpenHashMap(); ++ }).addTo(chunkKey, 1); ++ } ++ ++ private void removeExpireCount(final int chunkX, final int chunkZ) { ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); ++ final long sectionKey = CoordinateUtils.getChunkKey( ++ chunkX >> sectionShift, ++ chunkZ >> sectionShift ++ ); ++ ++ final Long2IntOpenHashMap removeCounts = this.sectionToChunkToExpireCount.get(sectionKey); ++ final int prevCount = removeCounts.addTo(chunkKey, -1); ++ ++ if (prevCount == 1) { ++ removeCounts.remove(chunkKey); ++ if (removeCounts.isEmpty()) { ++ this.sectionToChunkToExpireCount.remove(sectionKey); ++ } ++ } ++ } ++ ++ // supposed to return true if the ticket was added and did not replace another ++ // but, we always return false if the ticket cannot be added ++ public <T> boolean addTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier) { ++ return this.addTicketAtLevel(type, chunk, level, identifier, true); ++ } ++ ++ <T> boolean addTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier, final boolean lock) { ++ final long removeDelay = type.timeout <= 0 ? NO_TIMEOUT_MARKER : type.timeout; ++ if (level > MAX_TICKET_LEVEL) { ++ return false; ++ } ++ ++ final int chunkX = CoordinateUtils.getChunkX(chunk); ++ final int chunkZ = CoordinateUtils.getChunkZ(chunk); ++ final Ticket<T> ticket = new Ticket<>(type, level, identifier); ++ ((ChunkSystemTicket<T>)(Object)ticket).moonrise$setRemoveDelay(removeDelay); ++ ++ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; ++ try { ++ final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> { ++ return SortedArraySet.create(4); ++ }); ++ ++ final int levelBefore = getTicketLevelAt(ticketsAtChunk); ++ final Ticket<T> current = (Ticket<T>)((ChunkSystemSortedArraySet<Ticket<?>>)ticketsAtChunk).moonrise$replace(ticket); ++ final int levelAfter = getTicketLevelAt(ticketsAtChunk); ++ ++ if (current != ticket) { ++ final long oldRemoveDelay = ((ChunkSystemTicket<T>)(Object)current).moonrise$getRemoveDelay(); ++ if (removeDelay != oldRemoveDelay) { ++ if (oldRemoveDelay != NO_TIMEOUT_MARKER && removeDelay == NO_TIMEOUT_MARKER) { ++ this.removeExpireCount(chunkX, chunkZ); ++ } else if (oldRemoveDelay == NO_TIMEOUT_MARKER) { ++ // since old != new, we have that NO_TIMEOUT_MARKER != new ++ this.addExpireCount(chunkX, chunkZ); ++ } ++ } ++ } else { ++ if (removeDelay != NO_TIMEOUT_MARKER) { ++ this.addExpireCount(chunkX, chunkZ); ++ } ++ } ++ ++ if (levelBefore != levelAfter) { ++ this.updateTicketLevel(chunk, levelAfter); ++ } ++ ++ return current == ticket; ++ } finally { ++ if (ticketLock != null) { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ } ++ ++ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final ChunkPos chunkPos, final int level, final T identifier) { ++ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); ++ } ++ ++ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final int chunkX, final int chunkZ, final int level, final T identifier) { ++ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); ++ } ++ ++ public <T> boolean removeTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier) { ++ return this.removeTicketAtLevel(type, chunk, level, identifier, true); ++ } ++ ++ <T> boolean removeTicketAtLevel(final TicketType<T> type, final long chunk, final int level, final T identifier, final boolean lock) { ++ if (level > MAX_TICKET_LEVEL) { ++ return false; ++ } ++ ++ final int chunkX = CoordinateUtils.getChunkX(chunk); ++ final int chunkZ = CoordinateUtils.getChunkZ(chunk); ++ final Ticket<T> probe = new Ticket<>(type, level, identifier); ++ ++ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; ++ try { ++ final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.get(chunk); ++ if (ticketsAtChunk == null) { ++ return false; ++ } ++ ++ final int oldLevel = getTicketLevelAt(ticketsAtChunk); ++ final Ticket<T> ticket = (Ticket<T>)((ChunkSystemSortedArraySet<Ticket<?>>)ticketsAtChunk).moonrise$removeAndGet(probe); ++ ++ if (ticket == null) { ++ return false; ++ } ++ ++ final int newLevel = getTicketLevelAt(ticketsAtChunk); ++ // we should not change the ticket levels while the target region may be ticking ++ if (oldLevel != newLevel) { ++ final Ticket<ChunkPos> unknownTicket = new Ticket<>(TicketType.UNKNOWN, level, new ChunkPos(chunk)); ++ ((ChunkSystemTicket<ChunkPos>)(Object)unknownTicket).moonrise$setRemoveDelay(Math.max(1, TicketType.UNKNOWN.timeout)); ++ if (ticketsAtChunk.add(unknownTicket)) { ++ this.addExpireCount(chunkX, chunkZ); ++ } else { ++ throw new IllegalStateException("Should have been able to add " + unknownTicket + " to " + ticketsAtChunk); ++ } ++ } ++ ++ final long removeDelay = ((ChunkSystemTicket<T>)(Object)ticket).moonrise$getRemoveDelay(); ++ if (removeDelay != NO_TIMEOUT_MARKER) { ++ this.removeExpireCount(chunkX, chunkZ); ++ } ++ ++ return true; ++ } finally { ++ if (ticketLock != null) { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ } ++ ++ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk ++ public <T, V> void addAndRemoveTickets(final long chunk, final TicketType<T> addType, final int addLevel, final T addIdentifier, ++ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) { ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)); ++ try { ++ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false); ++ this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false); ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ ++ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk ++ public <T, V> boolean addIfRemovedTicket(final long chunk, final TicketType<T> addType, final int addLevel, final T addIdentifier, ++ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) { ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)); ++ try { ++ if (this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false)) { ++ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false); ++ return true; ++ } ++ return false; ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ ++ public <T> void removeAllTicketsFor(final TicketType<T> ticketType, final int ticketLevel, final T ticketIdentifier) { ++ if (ticketLevel > MAX_TICKET_LEVEL) { ++ return; ++ } ++ ++ final Long2ObjectOpenHashMap<LongArrayList> sections = new Long2ObjectOpenHashMap<>(); ++ final int sectionShift = this.taskScheduler.getChunkSystemLockShift(); ++ for (final PrimitiveIterator.OfLong iterator = this.tickets.keyIterator(); iterator.hasNext();) { ++ final long coord = iterator.nextLong(); ++ sections.computeIfAbsent( ++ CoordinateUtils.getChunkKey( ++ CoordinateUtils.getChunkX(coord) >> sectionShift, ++ CoordinateUtils.getChunkZ(coord) >> sectionShift ++ ), ++ (final long keyInMap) -> { ++ return new LongArrayList(); ++ } ++ ).add(coord); ++ } ++ ++ for (final Iterator<Long2ObjectMap.Entry<LongArrayList>> iterator = sections.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry<LongArrayList> entry = iterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ final LongArrayList coordinates = entry.getValue(); ++ ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( ++ CoordinateUtils.getChunkX(sectionKey) << sectionShift, ++ CoordinateUtils.getChunkZ(sectionKey) << sectionShift ++ ); ++ try { ++ for (final LongIterator iterator2 = coordinates.iterator(); iterator2.hasNext();) { ++ final long coord = iterator2.nextLong(); ++ this.removeTicketAtLevel(ticketType, coord, ticketLevel, ticketIdentifier, false); ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ } ++ ++ public void tick() { ++ ++this.currentTick; ++ ++ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); ++ ++ final Predicate<Ticket<?>> expireNow = (final Ticket<?> ticket) -> { ++ long removeDelay = ((ChunkSystemTicket<?>)(Object)ticket).moonrise$getRemoveDelay(); ++ if (removeDelay == NO_TIMEOUT_MARKER) { ++ return false; ++ } ++ --removeDelay; ++ ((ChunkSystemTicket<?>)(Object)ticket).moonrise$setRemoveDelay(removeDelay); ++ return removeDelay <= 0L; ++ }; ++ ++ for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) { ++ final long sectionKey = iterator.nextLong(); ++ ++ if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) { ++ // removed concurrently ++ continue; ++ } ++ ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( ++ CoordinateUtils.getChunkX(sectionKey) << sectionShift, ++ CoordinateUtils.getChunkZ(sectionKey) << sectionShift ++ ); ++ ++ try { ++ final Long2IntOpenHashMap chunkToExpireCount = this.sectionToChunkToExpireCount.get(sectionKey); ++ if (chunkToExpireCount == null) { ++ // lost to some race ++ continue; ++ } ++ ++ for (final Iterator<Long2IntMap.Entry> iterator1 = chunkToExpireCount.long2IntEntrySet().fastIterator(); iterator1.hasNext();) { ++ final Long2IntMap.Entry entry = iterator1.next(); ++ ++ final long chunkKey = entry.getLongKey(); ++ final int expireCount = entry.getIntValue(); ++ ++ final SortedArraySet<Ticket<?>> tickets = this.tickets.get(chunkKey); ++ final int levelBefore = getTicketLevelAt(tickets); ++ ++ final int sizeBefore = tickets.size(); ++ tickets.removeIf(expireNow); ++ final int sizeAfter = tickets.size(); ++ final int levelAfter = getTicketLevelAt(tickets); ++ ++ if (tickets.isEmpty()) { ++ this.tickets.remove(chunkKey); ++ } ++ if (levelBefore != levelAfter) { ++ this.updateTicketLevel(chunkKey, levelAfter); ++ } ++ ++ final int newExpireCount = expireCount - (sizeBefore - sizeAfter); ++ ++ if (newExpireCount == expireCount) { ++ continue; ++ } ++ ++ if (newExpireCount != 0) { ++ entry.setValue(newExpireCount); ++ } else { ++ iterator1.remove(); ++ } ++ } ++ ++ if (chunkToExpireCount.isEmpty()) { ++ this.sectionToChunkToExpireCount.remove(sectionKey); ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ ++ this.processTicketUpdates(); ++ } ++ ++ public NewChunkHolder getChunkHolder(final int chunkX, final int chunkZ) { ++ return this.chunkHolders.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ public NewChunkHolder getChunkHolder(final long position) { ++ return this.chunkHolders.get(position); ++ } ++ ++ public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); ++ if (chunkHolder != null) { ++ chunkHolder.raisePriority(priority); ++ } ++ } ++ ++ public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); ++ if (chunkHolder != null) { ++ chunkHolder.setPriority(priority); ++ } ++ } ++ ++ public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); ++ if (chunkHolder != null) { ++ chunkHolder.lowerPriority(priority); ++ } ++ } ++ ++ private NewChunkHolder createChunkHolder(final long position) { ++ final NewChunkHolder ret = new NewChunkHolder(this.world, CoordinateUtils.getChunkX(position), CoordinateUtils.getChunkZ(position), this.taskScheduler); ++ ++ ChunkSystem.onChunkHolderCreate(this.world, ret.vanillaChunkHolder); ++ ++ return ret; ++ } ++ ++ // because this function creates the chunk holder without a ticket, it is the caller's responsibility to ensure ++ // the chunk holder eventually unloads. this should only be used to avoid using processTicketUpdates to create chunkholders, ++ // as processTicketUpdates may call plugin logic; in every other case a ticket is appropriate ++ private NewChunkHolder getOrCreateChunkHolder(final int chunkX, final int chunkZ) { ++ return this.getOrCreateChunkHolder(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ private NewChunkHolder getOrCreateChunkHolder(final long position) { ++ final int chunkX = CoordinateUtils.getChunkX(position); ++ final int chunkZ = CoordinateUtils.getChunkZ(position); ++ ++ if (!this.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ)) { ++ throw new IllegalStateException("Must hold ticket level update lock!"); ++ } ++ if (!this.taskScheduler.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ)) { ++ throw new IllegalStateException("Must hold scheduler lock!!"); ++ } ++ ++ // we could just acquire these locks, but... ++ // must own the locks because the caller needs to ensure that no unload can occur AFTER this function returns ++ ++ NewChunkHolder current = this.chunkHolders.get(position); ++ if (current != null) { ++ return current; ++ } ++ ++ current = this.createChunkHolder(position); ++ this.chunkHolders.put(position, current); ++ ++ ++ return current; ++ } ++ ++ public ChunkEntitySlices getOrCreateEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main"); ++ ChunkEntitySlices ret; ++ ++ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ); ++ if (current != null && (ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) { ++ return ret; ++ } ++ ++ final AtomicBoolean isCompleted = new AtomicBoolean(); ++ final Thread waiter = Thread.currentThread(); ++ final Long entityLoadId = ChunkTaskScheduler.getNextEntityLoadId(); ++ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null; ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ); ++ try { ++ this.addTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); ++ final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ); ++ try { ++ current = this.getOrCreateChunkHolder(chunkX, chunkZ); ++ if ((ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) { ++ this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); ++ return ret; ++ } ++ ++ if (!transientChunk) { ++ if (current.isEntityChunkNBTLoaded()) { ++ isCompleted.setPlain(true); ++ } else { ++ loadTask = current.getOrLoadEntityData((final GenericDataLoadTask.TaskResult<CompoundTag, Throwable> result) -> { ++ isCompleted.set(true); ++ LockSupport.unpark(waiter); ++ }); ++ final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask(); ++ ++ if (entityLoad != null) { ++ entityLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING); ++ } ++ } ++ } ++ } finally { ++ this.taskScheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ ++ if (loadTask != null) { ++ loadTask.schedule(); ++ } ++ ++ if (!transientChunk) { ++ // Note: no need to busy wait on the chunk queue, entity load will complete off-main ++ boolean interrupted = false; ++ while (!isCompleted.get()) { ++ interrupted |= Thread.interrupted(); ++ LockSupport.park(); ++ } ++ ++ if (interrupted) { ++ Thread.currentThread().interrupt(); ++ } ++ } ++ ++ // now that the entity data is loaded, we can load it into the world ++ ++ ret = current.loadInEntityChunk(transientChunk); ++ ++ this.removeTicketAtLevel(ChunkTaskScheduler.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); ++ ++ return ret; ++ } ++ ++ public PoiChunk getPoiChunkIfLoaded(final int chunkX, final int chunkZ, final boolean checkLoadInCallback) { ++ final NewChunkHolder holder = this.getChunkHolder(chunkX, chunkZ); ++ if (holder != null) { ++ final PoiChunk ret = holder.getPoiChunk(); ++ return ret == null || (checkLoadInCallback && !ret.isLoaded()) ? null : ret; ++ } ++ return null; ++ } ++ ++ public PoiChunk loadPoiChunk(final int chunkX, final int chunkZ) { ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create poi chunk off-main"); ++ PoiChunk ret; ++ ++ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ); ++ if (current != null && (ret = current.getPoiChunk()) != null) { ++ ret.load(); ++ return ret; ++ } ++ ++ final AtomicReference<PoiChunk> completed = new AtomicReference<>(); ++ final AtomicBoolean isCompleted = new AtomicBoolean(); ++ final Thread waiter = Thread.currentThread(); ++ final Long poiLoadId = ChunkTaskScheduler.getNextPoiLoadId(); ++ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null; ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ); ++ try { ++ this.addTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId); ++ final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ); ++ try { ++ current = this.getOrCreateChunkHolder(chunkX, chunkZ); ++ if (null == (ret = current.getPoiChunk())) { ++ loadTask = current.getOrLoadPoiData((final GenericDataLoadTask.TaskResult<PoiChunk, Throwable> result) -> { ++ completed.setPlain(result.left()); ++ isCompleted.set(true); ++ LockSupport.unpark(waiter); ++ }); ++ final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask(); ++ ++ if (poiLoad != null) { ++ poiLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING); ++ } ++ } ++ } finally { ++ this.taskScheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ ++ if (loadTask != null) { ++ loadTask.schedule(); ++ ++ // Note: no need to busy wait on the chunk queue, poi load will complete off-main ++ ++ boolean interrupted = false; ++ while (!isCompleted.get()) { ++ interrupted |= Thread.interrupted(); ++ LockSupport.park(); ++ } ++ ++ if (interrupted) { ++ Thread.currentThread().interrupt(); ++ } ++ ++ ret = completed.getPlain(); ++ } // else: became loaded during the scheduling attempt, need to ensure load() is invoked ++ ++ ret.load(); ++ ++ this.removeTicketAtLevel(ChunkTaskScheduler.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId); ++ ++ return ret; ++ } ++ ++ void addChangedStatuses(final List<NewChunkHolder> changedFullStatus) { ++ if (changedFullStatus.isEmpty()) { ++ return; ++ } ++ if (!io.papermc.paper.util.TickThread.isTickThread()) { ++ this.taskScheduler.scheduleChunkTask(() -> { ++ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate; ++ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { ++ pendingFullLoadUpdate.add(changedFullStatus.get(i)); ++ } ++ ++ ChunkHolderManager.this.processPendingFullUpdate(); ++ }, PrioritisedExecutor.Priority.HIGHEST); ++ } else { ++ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate; ++ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { ++ pendingFullLoadUpdate.add(changedFullStatus.get(i)); ++ } ++ } ++ } ++ ++ private void removeChunkHolder(final NewChunkHolder holder) { ++ holder.markUnloaded(); ++ this.autoSaveQueue.remove(holder); ++ ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder); ++ this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); ++ ++ } ++ ++ // note: never call while inside the chunk system, this will absolutely break everything ++ public void processUnloads() { ++ io.papermc.paper.util.TickThread.ensureTickThread("Cannot unload chunks off-main"); ++ ++ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { ++ throw new IllegalStateException("Cannot unload chunks recursively"); ++ } ++ final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift ++ final List<ChunkUnloadQueue.SectionToUnload> unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions(); ++ int unloadCountTentative = 0; ++ for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { ++ final ChunkUnloadQueue.UnloadSection section ++ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ()); ++ ++ if (section == null) { ++ // removed concurrently ++ continue; ++ } ++ ++ // technically reading the size field is unsafe, and it may be incorrect. ++ // We assume that the error here cumulatively goes away over many ticks. If it did not, then it is possible ++ // for chunks to never unload or not unload fast enough. ++ unloadCountTentative += section.chunks.size(); ++ } ++ ++ if (unloadCountTentative <= 0) { ++ // no work to do ++ return; ++ } ++ ++ // We do need to process updates here so that any addTicket that is synchronised before this call does not go missed. ++ this.processTicketUpdates(); ++ ++ final int toUnloadCount = Math.max(50, (int)(unloadCountTentative * 0.05)); ++ int processedCount = 0; ++ ++ for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { ++ final List<NewChunkHolder> stage1 = new ArrayList<>(); ++ final List<NewChunkHolder.UnloadState> stage2 = new ArrayList<>(); ++ ++ final int sectionLowerX = sectionRef.sectionX() << sectionShift; ++ final int sectionLowerZ = sectionRef.sectionZ() << sectionShift; ++ ++ // stage 1: set up for stage 2 while holding critical locks ++ ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ); ++ try { ++ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ); ++ try { ++ final ChunkUnloadQueue.UnloadSection section ++ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ()); ++ ++ if (section == null) { ++ // removed concurrently ++ continue; ++ } ++ ++ // collect the holders to run stage 1 on ++ final int sectionCount = section.chunks.size(); ++ ++ if ((sectionCount + processedCount) <= toUnloadCount) { ++ // we can just drain the entire section ++ ++ for (final LongIterator iterator = section.chunks.iterator(); iterator.hasNext();) { ++ final NewChunkHolder holder = this.chunkHolders.get(iterator.nextLong()); ++ if (holder == null) { ++ throw new IllegalStateException(); ++ } ++ stage1.add(holder); ++ } ++ ++ // remove section ++ this.unloadQueue.removeSection(sectionRef.sectionX(), sectionRef.sectionZ()); ++ } else { ++ // processedCount + len = toUnloadCount ++ // we cannot drain the entire section ++ for (int i = 0, len = toUnloadCount - processedCount; i < len; ++i) { ++ final NewChunkHolder holder = this.chunkHolders.get(section.chunks.removeFirstLong()); ++ if (holder == null) { ++ throw new IllegalStateException(); ++ } ++ stage1.add(holder); ++ } ++ } ++ ++ // run stage 1 ++ for (int i = 0, len = stage1.size(); i < len; ++i) { ++ final NewChunkHolder chunkHolder = stage1.get(i); ++ chunkHolder.removeFromUnloadQueue(); ++ if (chunkHolder.isSafeToUnload() != null) { ++ LOGGER.error("Chunkholder " + chunkHolder + " is not safe to unload but is inside the unload queue?"); ++ continue; ++ } ++ final NewChunkHolder.UnloadState state = chunkHolder.unloadStage1(); ++ if (state == null) { ++ // can unload immediately ++ this.removeChunkHolder(chunkHolder); ++ continue; ++ } ++ stage2.add(state); ++ } ++ } finally { ++ this.taskScheduler.schedulingLockArea.unlock(scheduleLock); ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ ++ // stage 2: invoke expensive unload logic, designed to run without locks thanks to stage 1 ++ final List<NewChunkHolder> stage3 = new ArrayList<>(stage2.size()); ++ ++ final Boolean before = this.blockTicketUpdates(); ++ try { ++ for (int i = 0, len = stage2.size(); i < len; ++i) { ++ final NewChunkHolder.UnloadState state = stage2.get(i); ++ final NewChunkHolder holder = state.holder(); ++ ++ holder.unloadStage2(state); ++ stage3.add(holder); ++ } ++ } finally { ++ this.unblockTicketUpdates(before); ++ } ++ ++ // stage 3: actually attempt to remove the chunk holders ++ ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ); ++ try { ++ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ); ++ try { ++ for (int i = 0, len = stage3.size(); i < len; ++i) { ++ final NewChunkHolder holder = stage3.get(i); ++ ++ if (holder.unloadStage3()) { ++ this.removeChunkHolder(holder); ++ } else { ++ // add cooldown so the next unload check is not immediately next tick ++ this.addTicketAtLevel(UNLOAD_COOLDOWN, CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ), MAX_TICKET_LEVEL, Unit.INSTANCE, false); ++ } ++ } ++ } finally { ++ this.taskScheduler.schedulingLockArea.unlock(scheduleLock); ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ ++ processedCount += stage1.size(); ++ ++ if (processedCount >= toUnloadCount) { ++ break; ++ } ++ } ++ } ++ ++ public enum TicketOperationType { ++ ADD, REMOVE, ADD_IF_REMOVED, ADD_AND_REMOVE ++ } ++ ++ public static record TicketOperation<T, V> ( ++ TicketOperationType op, long chunkCoord, ++ TicketType<T> ticketType, int ticketLevel, T identifier, ++ TicketType<V> ticketType2, int ticketLevel2, V identifier2 ++ ) { ++ ++ private TicketOperation(TicketOperationType op, long chunkCoord, ++ TicketType<T> ticketType, int ticketLevel, T identifier) { ++ this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null); ++ } ++ ++ public static <T> TicketOperation<T, T> addOp(final ChunkPos chunk, final TicketType<T> type, final int ticketLevel, final T identifier) { ++ return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); ++ } ++ ++ public static <T> TicketOperation<T, T> addOp(final int chunkX, final int chunkZ, final TicketType<T> type, final int ticketLevel, final T identifier) { ++ return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); ++ } ++ ++ public static <T> TicketOperation<T, T> addOp(final long chunk, final TicketType<T> type, final int ticketLevel, final T identifier) { ++ return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier); ++ } ++ ++ public static <T> TicketOperation<T, T> removeOp(final ChunkPos chunk, final TicketType<T> type, final int ticketLevel, final T identifier) { ++ return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); ++ } ++ ++ public static <T> TicketOperation<T, T> removeOp(final int chunkX, final int chunkZ, final TicketType<T> type, final int ticketLevel, final T identifier) { ++ return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); ++ } ++ ++ public static <T> TicketOperation<T, T> removeOp(final long chunk, final TicketType<T> type, final int ticketLevel, final T identifier) { ++ return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier); ++ } ++ ++ public static <T, V> TicketOperation<T, V> addIfRemovedOp(final long chunk, ++ final TicketType<T> addType, final int addLevel, final T addIdentifier, ++ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) { ++ return new TicketOperation<>( ++ TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier, ++ removeType, removeLevel, removeIdentifier ++ ); ++ } ++ ++ public static <T, V> TicketOperation<T, V> addAndRemove(final long chunk, ++ final TicketType<T> addType, final int addLevel, final T addIdentifier, ++ final TicketType<V> removeType, final int removeLevel, final V removeIdentifier) { ++ return new TicketOperation<>( ++ TicketOperationType.ADD_AND_REMOVE, chunk, addType, addLevel, addIdentifier, ++ removeType, removeLevel, removeIdentifier ++ ); ++ } ++ } ++ ++ private <T, V> boolean processTicketOp(TicketOperation<T, V> operation) { ++ boolean ret = false; ++ switch (operation.op) { ++ case ADD: { ++ ret |= this.addTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier); ++ break; ++ } ++ case REMOVE: { ++ ret |= this.removeTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier); ++ break; ++ } ++ case ADD_IF_REMOVED: { ++ ret |= this.addIfRemovedTicket( ++ operation.chunkCoord, ++ operation.ticketType, operation.ticketLevel, operation.identifier, ++ operation.ticketType2, operation.ticketLevel2, operation.identifier2 ++ ); ++ break; ++ } ++ case ADD_AND_REMOVE: { ++ ret = true; ++ this.addAndRemoveTickets( ++ operation.chunkCoord, ++ operation.ticketType, operation.ticketLevel, operation.identifier, ++ operation.ticketType2, operation.ticketLevel2, operation.identifier2 ++ ); ++ break; ++ } ++ } ++ ++ return ret; ++ } ++ ++ public void performTicketUpdates(final Collection<TicketOperation<?, ?>> operations) { ++ for (final TicketOperation<?, ?> operation : operations) { ++ this.processTicketOp(operation); ++ } ++ } ++ ++ private final ThreadLocal<Boolean> BLOCK_TICKET_UPDATES = ThreadLocal.withInitial(() -> { ++ return Boolean.FALSE; ++ }); ++ ++ public Boolean blockTicketUpdates() { ++ final Boolean ret = BLOCK_TICKET_UPDATES.get(); ++ BLOCK_TICKET_UPDATES.set(Boolean.TRUE); ++ return ret; ++ } ++ ++ public void unblockTicketUpdates(final Boolean before) { ++ BLOCK_TICKET_UPDATES.set(before); ++ } ++ ++ public boolean processTicketUpdates() { ++ return this.processTicketUpdates(true, null); ++ } ++ ++ private static final ThreadLocal<List<ChunkProgressionTask>> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>(); ++ ++ static List<ChunkProgressionTask> getCurrentTicketUpdateScheduling() { ++ return CURRENT_TICKET_UPDATE_SCHEDULING.get(); ++ } ++ ++ private boolean processTicketUpdates(final boolean processFullUpdates, List<ChunkProgressionTask> scheduledTasks) { ++ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { ++ throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager"); ++ } ++ ++ List<NewChunkHolder> changedFullStatus = null; ++ ++ final boolean isTickThread = io.papermc.paper.util.TickThread.isTickThread(); ++ ++ boolean ret = false; ++ final boolean canProcessFullUpdates = processFullUpdates & isTickThread; ++ final boolean canProcessScheduling = scheduledTasks == null; ++ ++ if (this.ticketLevelPropagator.hasPendingUpdates()) { ++ if (scheduledTasks == null) { ++ scheduledTasks = new ArrayList<>(); ++ } ++ changedFullStatus = new ArrayList<>(); ++ ++ ret |= this.ticketLevelPropagator.performUpdates( ++ this.ticketLockArea, this.taskScheduler.schedulingLockArea, ++ scheduledTasks, changedFullStatus ++ ); ++ } ++ ++ if (changedFullStatus != null) { ++ this.addChangedStatuses(changedFullStatus); ++ } ++ ++ if (canProcessScheduling && scheduledTasks != null) { ++ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) { ++ scheduledTasks.get(i).schedule(); ++ } ++ } ++ ++ if (canProcessFullUpdates) { ++ ret |= this.processPendingFullUpdate(); ++ } ++ ++ return ret; ++ } ++ ++ // only call on tick thread ++ private boolean processPendingFullUpdate() { ++ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate; ++ ++ boolean ret = false; ++ ++ final List<NewChunkHolder> changedFullStatus = new ArrayList<>(); ++ ++ NewChunkHolder holder; ++ while ((holder = pendingFullLoadUpdate.poll()) != null) { ++ ret |= holder.handleFullStatusChange(changedFullStatus); ++ ++ if (!changedFullStatus.isEmpty()) { ++ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { ++ pendingFullLoadUpdate.add(changedFullStatus.get(i)); ++ } ++ changedFullStatus.clear(); ++ } ++ } ++ ++ return ret; ++ } ++ ++ public JsonObject getDebugJson() { ++ final JsonObject ret = new JsonObject(); ++ ++ ret.add("unload_queue", this.unloadQueue.toDebugJson()); ++ ++ final JsonArray holders = new JsonArray(); ++ ret.add("chunkholders", holders); ++ ++ for (final NewChunkHolder holder : this.getChunkHolders()) { ++ holders.add(holder.getDebugJson()); ++ } ++ ++ final JsonArray allTicketsJson = new JsonArray(); ++ ret.add("tickets", allTicketsJson); ++ ++ for (final Iterator<ConcurrentLong2ReferenceChainedHashTable.TableEntry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.entryIterator(); ++ iterator.hasNext();) { ++ final ConcurrentLong2ReferenceChainedHashTable.TableEntry<SortedArraySet<Ticket<?>>> coordinateTickets = iterator.next(); ++ final long coordinate = coordinateTickets.getKey(); ++ final SortedArraySet<Ticket<?>> tickets = coordinateTickets.getValue(); ++ ++ final JsonObject coordinateJson = new JsonObject(); ++ allTicketsJson.add(coordinateJson); ++ ++ coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); ++ coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); ++ ++ final JsonArray ticketsSerialized = new JsonArray(); ++ coordinateJson.add("tickets", ticketsSerialized); ++ ++ // note: by using a copy of the backing array, we can avoid explicit exceptions we may trip when iterating ++ // directly over the set using the iterator ++ // however, it also means we need to null-check the values, and there is a possibility that we _miss_ an ++ // entry OR iterate over an entry multiple times ++ for (final Object ticketUncasted : ((ChunkSystemSortedArraySet<Ticket<?>>)tickets).moonrise$copyBackingArray()) { ++ if (ticketUncasted == null) { ++ continue; ++ } ++ final Ticket<?> ticket = (Ticket<?>)ticketUncasted; ++ final JsonObject ticketSerialized = new JsonObject(); ++ ticketsSerialized.add(ticketSerialized); ++ ++ ticketSerialized.addProperty("type", ticket.getType().toString()); ++ ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel())); ++ ticketSerialized.addProperty("identifier", Objects.toString(ticket.key)); ++ ticketSerialized.addProperty("remove_tick", Long.valueOf(((ChunkSystemTicket<?>)(Object)ticket).moonrise$getRemoveDelay())); ++ } ++ } ++ ++ return ret; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c1c119e2e788d5963de3a74a6b9724c71a168a8a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java +@@ -0,0 +1,1037 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue; ++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.JsonUtil; ++import ca.spottedleaf.moonrise.common.util.MoonriseCommon; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; ++import ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor.RadiusAwarePrioritisedExecutor; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkFullTask; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLightTask; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkUpgradeGenericStatusTask; ++import ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer; ++import ca.spottedleaf.moonrise.patches.chunk_system.status.ChunkSystemChunkStep; ++import ca.spottedleaf.moonrise.patches.chunk_system.util.ParallelSearchRadiusIteration; ++import com.mojang.logging.LogUtils; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonObject; ++import net.minecraft.CrashReport; ++import net.minecraft.CrashReportCategory; ++import net.minecraft.ReportedException; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ChunkLevel; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.server.level.GenerationChunkHolder; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.util.StaticCache2D; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.status.ChunkPyramid; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.status.ChunkStep; ++import net.minecraft.world.phys.Vec3; ++import org.slf4j.Logger; ++import java.io.File; ++import java.time.LocalDateTime; ++import java.time.format.DateTimeFormatter; ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.List; ++import java.util.Map; ++import java.util.Objects; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.function.Consumer; ++ ++public final class ChunkTaskScheduler { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ static int newChunkSystemIOThreads; ++ static int newChunkSystemGenParallelism; ++ static int newChunkSystemGenPopulationParallelism; ++ static int newChunkSystemLoadParallelism; ++ ++ private static boolean initialised = false; ++ ++ public static void init(io.papermc.paper.configuration.GlobalConfiguration.ChunkSystem chunkSystem) { ++ if (initialised) { ++ return; ++ } ++ initialised = true; ++ MoonriseCommon.init(chunkSystem); // Paper ++ newChunkSystemIOThreads = chunkSystem.ioThreads; ++ if (newChunkSystemIOThreads <= 0) { ++ newChunkSystemIOThreads = 1; ++ } else { ++ newChunkSystemIOThreads = Math.max(1, newChunkSystemIOThreads); ++ } ++ ++ String newChunkSystemGenParallelism = chunkSystem.genParallelism; ++ if (newChunkSystemGenParallelism.equalsIgnoreCase("default")) { ++ newChunkSystemGenParallelism = "true"; ++ } ++ ++ boolean useParallelGen; ++ if (newChunkSystemGenParallelism.equalsIgnoreCase("on") || newChunkSystemGenParallelism.equalsIgnoreCase("enabled") ++ || newChunkSystemGenParallelism.equalsIgnoreCase("true")) { ++ useParallelGen = true; ++ } else if (newChunkSystemGenParallelism.equalsIgnoreCase("off") || newChunkSystemGenParallelism.equalsIgnoreCase("disabled") ++ || newChunkSystemGenParallelism.equalsIgnoreCase("false")) { ++ useParallelGen = false; ++ } else { ++ throw new IllegalStateException("Invalid option for gen-parallelism: must be one of [on, off, enabled, disabled, true, false, default]"); ++ } ++ ++ ChunkTaskScheduler.newChunkSystemGenParallelism = MoonriseCommon.WORKER_THREADS; ++ ChunkTaskScheduler.newChunkSystemGenPopulationParallelism = useParallelGen ? MoonriseCommon.WORKER_THREADS : 1; ++ ChunkTaskScheduler.newChunkSystemLoadParallelism = MoonriseCommon.WORKER_THREADS; ++ ++ RegionFileIOThread.init(newChunkSystemIOThreads); ++ ++ LOGGER.info("Chunk system is using " + newChunkSystemIOThreads + " I/O threads, " + MoonriseCommon.WORKER_THREADS + " worker threads, and population gen parallelism of " + ChunkTaskScheduler.newChunkSystemGenPopulationParallelism + " threads"); ++ } ++ ++ public static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_system:chunk_load", Long::compareTo); ++ private static final AtomicLong CHUNK_LOAD_IDS = new AtomicLong(); ++ ++ public static Long getNextChunkLoadId() { ++ return Long.valueOf(CHUNK_LOAD_IDS.getAndIncrement()); ++ } ++ ++ public static final TicketType<Long> NON_FULL_CHUNK_LOAD = TicketType.create("chunk_system:non_full_load", Long::compareTo); ++ private static final AtomicLong NON_FULL_CHUNK_LOAD_IDS = new AtomicLong(); ++ ++ public static Long getNextNonFullLoadId() { ++ return Long.valueOf(NON_FULL_CHUNK_LOAD_IDS.getAndIncrement()); ++ } ++ ++ public static final TicketType<Long> ENTITY_LOAD = TicketType.create("chunk_system:entity_load", Long::compareTo); ++ private static final AtomicLong ENTITY_LOAD_IDS = new AtomicLong(); ++ ++ public static Long getNextEntityLoadId() { ++ return Long.valueOf(ENTITY_LOAD_IDS.getAndIncrement()); ++ } ++ ++ public static final TicketType<Long> POI_LOAD = TicketType.create("chunk_system:poi_load", Long::compareTo); ++ private static final AtomicLong POI_LOAD_IDS = new AtomicLong(); ++ ++ public static Long getNextPoiLoadId() { ++ return Long.valueOf(POI_LOAD_IDS.getAndIncrement()); ++ } ++ ++ public static final TicketType<Long> CHUNK_RELIGHT = TicketType.create("starlight:chunk_relight", Long::compareTo); ++ private static final AtomicLong CHUNK_RELIGHT_IDS = new AtomicLong(); ++ ++ public static Long getNextChunkRelightId() { ++ return Long.valueOf(CHUNK_RELIGHT_IDS.getAndIncrement()); ++ } ++ ++ ++ public static int getTicketLevel(final ChunkStatus status) { ++ return ChunkLevel.byStatus(status); ++ } ++ ++ public final ServerLevel world; ++ public final PrioritisedThreadPool workers; ++ public final RadiusAwarePrioritisedExecutor radiusAwareScheduler; ++ public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor; ++ private final PrioritisedThreadPool.PrioritisedPoolExecutor radiusAwareGenExecutor; ++ public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor; ++ ++ private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue(); ++ ++ public final ChunkHolderManager chunkHolderManager; ++ ++ static { ++ ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setWriteRadius(0); ++ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_STARTS).moonrise$setWriteRadius(0); ++ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setWriteRadius(0); ++ ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setWriteRadius(0); ++ ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setWriteRadius(0); ++ ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setWriteRadius(0); ++ ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setWriteRadius(0); ++ ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setWriteRadius(1); ++ ((ChunkSystemChunkStatus)ChunkStatus.INITIALIZE_LIGHT).moonrise$setWriteRadius(0); ++ ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$setWriteRadius(2); ++ ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setWriteRadius(0); ++ ((ChunkSystemChunkStatus)ChunkStatus.FULL).moonrise$setWriteRadius(0); ++ ++ ((ChunkSystemChunkStatus)ChunkStatus.EMPTY).moonrise$setEmptyLoadStatus(true); ++ ((ChunkSystemChunkStatus)ChunkStatus.STRUCTURE_REFERENCES).moonrise$setEmptyLoadStatus(true); ++ ((ChunkSystemChunkStatus)ChunkStatus.BIOMES).moonrise$setEmptyLoadStatus(true); ++ ((ChunkSystemChunkStatus)ChunkStatus.NOISE).moonrise$setEmptyLoadStatus(true); ++ ((ChunkSystemChunkStatus)ChunkStatus.SURFACE).moonrise$setEmptyLoadStatus(true); ++ ((ChunkSystemChunkStatus)ChunkStatus.CARVERS).moonrise$setEmptyLoadStatus(true); ++ ((ChunkSystemChunkStatus)ChunkStatus.FEATURES).moonrise$setEmptyLoadStatus(true); ++ ((ChunkSystemChunkStatus)ChunkStatus.SPAWN).moonrise$setEmptyLoadStatus(true); ++ ++ /* ++ It's important that the neighbour read radius is taken into account. If _any_ later status is using some chunk as ++ a neighbour, it must be also safe if that neighbour is being generated. i.e for any status later than FEATURES, ++ for a status to be parallel safe it must not read the block data from its neighbours. ++ */ ++ final List<ChunkStatus> parallelCapableStatus = Arrays.asList( ++ // No-op executor. ++ ChunkStatus.EMPTY, ++ ++ // This is parallel capable, as CB has fixed the concurrency issue with stronghold generations. ++ // Does not touch neighbour chunks. ++ ChunkStatus.STRUCTURE_STARTS, ++ ++ // Surprisingly this is parallel capable. It is simply reading the already-created structure starts ++ // into the structure references for the chunk. So while it reads from it neighbours, its neighbours ++ // will not change, even if executed in parallel. ++ ChunkStatus.STRUCTURE_REFERENCES, ++ ++ // Safe. Mojang runs it in parallel as well. ++ ChunkStatus.BIOMES, ++ ++ // Safe. Mojang runs it in parallel as well. ++ ChunkStatus.NOISE, ++ ++ // Parallel safe. Only touches the target chunk. Biome retrieval is now noise based, which is ++ // completely thread-safe. ++ ChunkStatus.SURFACE, ++ ++ // No global state is modified in the carvers. It only touches the specified chunk. So it is parallel safe. ++ ChunkStatus.CARVERS, ++ ++ // FEATURES is not parallel safe. It writes to neighbours. ++ ++ // no-op executor ++ ChunkStatus.INITIALIZE_LIGHT ++ ++ // LIGHT is not parallel safe. It also doesn't run on the generation executor, so no point. ++ ++ // Only writes to the specified chunk. State is not read by later statuses. Parallel safe. ++ // Note: it may look unsafe because it writes to a worldgenregion, but the region size is always 0 - ++ // see the task margin. ++ // However, if the neighbouring FEATURES chunk is unloaded, but then fails to load in again (for whatever ++ // reason), then it would write to this chunk - and since this status reads blocks from itself, it's not ++ // safe to execute this in parallel. ++ // SPAWN ++ ++ // FULL is executed on main. ++ ); ++ ++ for (final ChunkStatus status : parallelCapableStatus) { ++ ((ChunkSystemChunkStatus)status).moonrise$setParallelCapable(true); ++ } ++ } ++ ++ private static final int[] ACCESS_RADIUS_TABLE_LOAD = new int[ChunkStatus.getStatusList().size()]; ++ private static final int[] ACCESS_RADIUS_TABLE_GEN = new int[ChunkStatus.getStatusList().size()]; ++ private static final int[] ACCESS_RADIUS_TABLE = new int[ChunkStatus.getStatusList().size()]; ++ static { ++ Arrays.fill(ACCESS_RADIUS_TABLE_LOAD, -1); ++ Arrays.fill(ACCESS_RADIUS_TABLE_GEN, -1); ++ Arrays.fill(ACCESS_RADIUS_TABLE, -1); ++ } ++ ++ private static int getAccessRadius0(final ChunkStatus toStatus, final ChunkPyramid pyramid) { ++ if (toStatus == ChunkStatus.EMPTY) { ++ return 0; ++ } ++ ++ final ChunkStep chunkStep = pyramid.getStepTo(toStatus); ++ ++ final int radius = chunkStep.getAccumulatedRadiusOf(ChunkStatus.EMPTY); ++ int maxRange = radius; ++ ++ for (int dist = 0; dist <= radius; ++dist) { ++ final ChunkStatus requiredNeighbourStatus = ((ChunkSystemChunkStep)(Object)chunkStep).moonrise$getRequiredStatusAtRadius(dist); ++ final int rad = ACCESS_RADIUS_TABLE[requiredNeighbourStatus.getIndex()]; ++ if (rad == -1) { ++ throw new IllegalStateException(); ++ } ++ ++ maxRange = Math.max(maxRange, dist + rad); ++ } ++ ++ return maxRange; ++ } ++ ++ private static final int MAX_ACCESS_RADIUS; ++ ++ static { ++ final List<ChunkStatus> statuses = ChunkStatus.getStatusList(); ++ for (int i = 0, len = statuses.size(); i < len; ++i) { ++ final ChunkStatus status = statuses.get(i); ++ ACCESS_RADIUS_TABLE_LOAD[i] = getAccessRadius0(status, ChunkPyramid.LOADING_PYRAMID); ++ ACCESS_RADIUS_TABLE_GEN[i] = getAccessRadius0(status, ChunkPyramid.GENERATION_PYRAMID); ++ ACCESS_RADIUS_TABLE[i] = Math.max( ++ ACCESS_RADIUS_TABLE_LOAD[i], ++ ACCESS_RADIUS_TABLE_GEN[i] ++ ); ++ } ++ MAX_ACCESS_RADIUS = ACCESS_RADIUS_TABLE[ACCESS_RADIUS_TABLE.length - 1]; ++ } ++ ++ public static int getMaxAccessRadius() { ++ return MAX_ACCESS_RADIUS; ++ } ++ ++ public static int getAccessRadius(final ChunkStatus genStatus) { ++ return ACCESS_RADIUS_TABLE[genStatus.getIndex()]; ++ } ++ ++ public static int getAccessRadius(final FullChunkStatus status) { ++ return (status.ordinal() - 1) + getAccessRadius(ChunkStatus.FULL); ++ } ++ ++ ++ public final ReentrantAreaLock schedulingLockArea; ++ private final int lockShift; ++ ++ public final int getChunkSystemLockShift() { ++ return this.lockShift; ++ } ++ ++ public ChunkTaskScheduler(final ServerLevel world, final PrioritisedThreadPool workers) { ++ this.world = world; ++ this.workers = workers; ++ // must be >= region shift (in paper, doesn't exist) and must be >= ticket propagator section shift ++ // it must be >= region shift since the regioniser assumes ticket updates do not occur in parallel for the region sections ++ // it must be >= ticket propagator section shift so that the ticket propagator can assume that owning a position implies owning ++ // the entire section ++ // we just take the max, as we want the smallest shift that satisfies these properties ++ this.lockShift = Math.max(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift(), ThreadedTicketLevelPropagator.SECTION_SHIFT); ++ this.schedulingLockArea = new ReentrantAreaLock(this.getChunkSystemLockShift()); ++ ++ final String worldName = WorldUtil.getWorldName(world); ++ this.parallelGenExecutor = workers.createExecutor("Chunk parallel generation executor for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenParallelism)); ++ this.radiusAwareGenExecutor = workers.createExecutor("Chunk radius aware generator for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenPopulationParallelism)); ++ this.loadExecutor = workers.createExecutor("Chunk load executor for world '" + worldName + "'", 1, newChunkSystemLoadParallelism); ++ this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, Math.max(2, 1 + newChunkSystemGenPopulationParallelism)); ++ this.chunkHolderManager = new ChunkHolderManager(world, this); ++ } ++ ++ private final AtomicBoolean failedChunkSystem = new AtomicBoolean(); ++ ++ public static Object stringIfNull(final Object obj) { ++ return obj == null ? "null" : obj; ++ } ++ ++ public void unrecoverableChunkSystemFailure(final int chunkX, final int chunkZ, final Map<String, Object> objectsOfInterest, final Throwable thr) { ++ final NewChunkHolder holder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); ++ LOGGER.error("Chunk system error at chunk (" + chunkX + "," + chunkZ + "), holder: " + holder + ", exception:", new Throwable(thr)); ++ ++ if (this.failedChunkSystem.getAndSet(true)) { ++ return; ++ } ++ ++ final ReportedException reportedException = thr instanceof ReportedException ? (ReportedException)thr : new ReportedException(new CrashReport("Chunk system error", thr)); ++ ++ CrashReportCategory crashReportCategory = reportedException.getReport().addCategory("Chunk system details"); ++ crashReportCategory.setDetail("Chunk coordinate", new ChunkPos(chunkX, chunkZ).toString()); ++ crashReportCategory.setDetail("ChunkHolder", Objects.toString(holder)); ++ crashReportCategory.setDetail("unrecoverableChunkSystemFailure caller thread", Thread.currentThread().getName()); ++ ++ crashReportCategory = reportedException.getReport().addCategory("Chunk System Objects of Interest"); ++ for (final Map.Entry<String, Object> entry : objectsOfInterest.entrySet()) { ++ if (entry.getValue() instanceof Throwable thrObject) { ++ crashReportCategory.setDetailError(Objects.toString(entry.getKey()), thrObject); ++ } else { ++ crashReportCategory.setDetail(Objects.toString(entry.getKey()), Objects.toString(entry.getValue())); ++ } ++ } ++ ++ final Runnable crash = () -> { ++ throw new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException); ++ }; ++ ++ // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions ++ this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING); ++ // so, make the main thread pick it up ++ ((ChunkSystemMinecraftServer)this.world.getServer()).moonrise$setChunkSystemCrash(new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException)); ++ } ++ ++ public boolean executeMainThreadTask() { ++ io.papermc.paper.util.TickThread.ensureTickThread("Cannot execute main thread task off-main"); ++ return this.mainThreadExecutor.executeTask(); ++ } ++ ++ public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ this.chunkHolderManager.raisePriority(x, z, priority); ++ } ++ ++ public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ this.chunkHolderManager.setPriority(x, z, priority); ++ } ++ ++ public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ this.chunkHolderManager.lowerPriority(x, z, priority); ++ } ++ ++ public void scheduleTickingState(final int chunkX, final int chunkZ, final FullChunkStatus toStatus, ++ final boolean addTicket, final PrioritisedExecutor.Priority priority, ++ final Consumer<LevelChunk> onComplete) { ++ if (!io.papermc.paper.util.TickThread.isTickThread()) { ++ this.scheduleChunkTask(chunkX, chunkZ, () -> { ++ ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ }, priority); ++ return; ++ } ++ final int accessRadius = getAccessRadius(toStatus); ++ if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { ++ throw new IllegalStateException("Cannot schedule chunk load during ticket level update"); ++ } ++ if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { ++ throw new IllegalStateException("Cannot schedule chunk loading recursively"); ++ } ++ ++ if (toStatus == FullChunkStatus.INACCESSIBLE) { ++ throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); ++ } ++ ++ final int minLevel = 33 - (toStatus.ordinal() - 1); ++ final Long chunkReference = addTicket ? getNextChunkLoadId() : null; ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ if (addTicket) { ++ this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); ++ this.chunkHolderManager.processTicketUpdates(); ++ } ++ ++ final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> { ++ try { ++ if (onComplete != null) { ++ onComplete.accept(chunk); ++ } ++ } finally { ++ if (addTicket) { ++ ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); ++ } ++ } ++ }; ++ ++ final boolean scheduled; ++ final LevelChunk chunk; ++ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); ++ try { ++ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); ++ try { ++ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); ++ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { ++ scheduled = false; ++ chunk = null; ++ } else { ++ final FullChunkStatus currStatus = chunkHolder.getChunkStatus(); ++ if (currStatus.isOrAfter(toStatus)) { ++ scheduled = false; ++ chunk = (LevelChunk)chunkHolder.getCurrentChunk(); ++ } else { ++ scheduled = true; ++ chunk = null; ++ ++ final int radius = toStatus.ordinal() - 1; // 0 -> BORDER, 1 -> TICKING, 2 -> ENTITY_TICKING ++ for (int dz = -radius; dz <= radius; ++dz) { ++ for (int dx = -radius; dx <= radius; ++dx) { ++ final NewChunkHolder neighbour = ++ (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ); ++ if (neighbour != null) { ++ neighbour.raisePriority(priority); ++ } ++ } ++ } ++ ++ // ticket level should schedule for us ++ chunkHolder.addFullStatusConsumer(toStatus, loadCallback); ++ } ++ } ++ } finally { ++ this.schedulingLockArea.unlock(schedulingLock); ++ } ++ } finally { ++ this.chunkHolderManager.ticketLockArea.unlock(ticketLock); ++ } ++ ++ if (!scheduled) { ++ // couldn't schedule ++ try { ++ loadCallback.accept(chunk); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to process chunk full status callback", thr); ++ } ++ } ++ } ++ ++ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final boolean gen, final ChunkStatus toStatus, final boolean addTicket, ++ final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) { ++ if (gen) { ++ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ return; ++ } ++ this.scheduleChunkLoad(chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { ++ if (chunk == null) { ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } ++ } else { ++ if (chunk.getPersistedStatus().isOrAfter(toStatus)) { ++ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ } else { ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } ++ } ++ } ++ }); ++ } ++ ++ // only appropriate to use with syncLoadNonFull ++ public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus, ++ final PrioritisedExecutor.Priority priority) { ++ final int accessRadius = getAccessRadius(toStatus); ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus); ++ final List<ChunkProgressionTask> tasks = new ArrayList<>(); ++ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention ++ try { ++ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention ++ try { ++ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); ++ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { ++ return false; ++ } else { ++ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); ++ if (genStatus != null && genStatus.isOrAfter(toStatus)) { ++ return true; ++ } else { ++ chunkHolder.raisePriority(priority); ++ ++ if (!chunkHolder.upgradeGenTarget(toStatus)) { ++ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); ++ } ++ } ++ } ++ } finally { ++ this.schedulingLockArea.unlock(schedulingLock); ++ } ++ } finally { ++ this.chunkHolderManager.ticketLockArea.unlock(ticketLock); ++ } ++ ++ for (int i = 0, len = tasks.size(); i < len; ++i) { ++ tasks.get(i).schedule(); ++ } ++ ++ return true; ++ } ++ ++ // Note: on Moonrise the non-full sync load requires blocking on managedBlock, but this is fine since there is only ++ // one main thread. On Folia, it is required that the non-full load can occur completely asynchronously to avoid deadlock ++ // between regions ++ public ChunkAccess syncLoadNonFull(final int chunkX, final int chunkZ, final ChunkStatus status) { ++ if (status == null || status.isOrAfter(ChunkStatus.FULL)) { ++ throw new IllegalArgumentException("Status: " + status); ++ } ++ ChunkAccess loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status); ++ if (loaded != null) { ++ return loaded; ++ } ++ ++ final Long ticketId = getNextNonFullLoadId(); ++ final int ticketLevel = getTicketLevel(status); ++ this.chunkHolderManager.addTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId); ++ this.chunkHolderManager.processTicketUpdates(); ++ ++ this.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, PrioritisedExecutor.Priority.BLOCKING); ++ ++ // we could do a simple spinwait here, since we do not need to process tasks while performing this load ++ // but we process tasks only because it's a better use of the time spent ++ this.world.getChunkSource().mainThreadProcessor.managedBlock(() -> { ++ return ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status) != null; ++ }); ++ ++ loaded = ((ChunkSystemServerLevel)this.world).moonrise$getSpecificChunkIfLoaded(chunkX, chunkZ, status); ++ ++ this.chunkHolderManager.removeTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId); ++ ++ if (loaded == null) { ++ throw new IllegalStateException("Expected chunk to be loaded for status " + status); ++ } ++ ++ return loaded; ++ } ++ ++ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket, ++ final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) { ++ if (!io.papermc.paper.util.TickThread.isTickThread()) { ++ this.scheduleChunkTask(chunkX, chunkZ, () -> { ++ ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ }, priority); ++ return; ++ } ++ final int accessRadius = getAccessRadius(toStatus); ++ if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { ++ throw new IllegalStateException("Cannot schedule chunk load during ticket level update"); ++ } ++ if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { ++ throw new IllegalStateException("Cannot schedule chunk loading recursively"); ++ } ++ ++ if (toStatus == ChunkStatus.FULL) { ++ this.scheduleTickingState(chunkX, chunkZ, FullChunkStatus.FULL, addTicket, priority, (Consumer)onComplete); ++ return; ++ } ++ ++ final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus); ++ final Long chunkReference = addTicket ? getNextChunkLoadId() : null; ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ if (addTicket) { ++ this.chunkHolderManager.addTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); ++ this.chunkHolderManager.processTicketUpdates(); ++ } ++ ++ final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> { ++ try { ++ if (onComplete != null) { ++ onComplete.accept(chunk); ++ } ++ } finally { ++ if (addTicket) { ++ ChunkTaskScheduler.this.chunkHolderManager.removeTicketAtLevel(CHUNK_LOAD, chunkKey, minLevel, chunkReference); ++ } ++ } ++ }; ++ ++ final List<ChunkProgressionTask> tasks = new ArrayList<>(); ++ ++ final boolean scheduled; ++ final ChunkAccess chunk; ++ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); ++ try { ++ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); ++ try { ++ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); ++ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { ++ scheduled = false; ++ chunk = null; ++ } else { ++ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); ++ if (genStatus != null && genStatus.isOrAfter(toStatus)) { ++ scheduled = false; ++ chunk = chunkHolder.getCurrentChunk(); ++ } else { ++ scheduled = true; ++ chunk = null; ++ chunkHolder.raisePriority(priority); ++ ++ if (!chunkHolder.upgradeGenTarget(toStatus)) { ++ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); ++ } ++ chunkHolder.addStatusConsumer(toStatus, loadCallback); ++ } ++ } ++ } finally { ++ this.schedulingLockArea.unlock(schedulingLock); ++ } ++ } finally { ++ this.chunkHolderManager.ticketLockArea.unlock(ticketLock); ++ } ++ ++ for (int i = 0, len = tasks.size(); i < len; ++i) { ++ tasks.get(i).schedule(); ++ } ++ ++ if (!scheduled) { ++ // couldn't schedule ++ try { ++ loadCallback.accept(chunk); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to process chunk status callback", thr); ++ } ++ } ++ } ++ ++ private ChunkProgressionTask createTask(final int chunkX, final int chunkZ, final ChunkAccess chunk, ++ final NewChunkHolder chunkHolder, final StaticCache2D<GenerationChunkHolder> neighbours, ++ final ChunkStatus toStatus, final PrioritisedExecutor.Priority initialPriority) { ++ if (toStatus == ChunkStatus.EMPTY) { ++ return new ChunkLoadTask(this, this.world, chunkX, chunkZ, chunkHolder, initialPriority); ++ } ++ if (toStatus == ChunkStatus.LIGHT) { ++ return new ChunkLightTask(this, this.world, chunkX, chunkZ, chunk, initialPriority); ++ } ++ if (toStatus == ChunkStatus.FULL) { ++ return new ChunkFullTask(this, this.world, chunkX, chunkZ, chunkHolder, chunk, initialPriority); ++ } ++ ++ return new ChunkUpgradeGenericStatusTask(this, this.world, chunkX, chunkZ, chunk, neighbours, toStatus, initialPriority); ++ } ++ ++ ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, final NewChunkHolder chunkHolder, ++ final List<ChunkProgressionTask> allTasks) { ++ return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)); ++ } ++ ++ // rets new task scheduled for the _specified_ chunk ++ // note: this must hold the scheduling lock ++ // minPriority is only used to pass the priority through to neighbours, as priority calculation has not yet been done ++ // schedule will ignore the generation target, so it should be checked by the caller to ensure the target is not regressed! ++ private ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, ++ final NewChunkHolder chunkHolder, final List<ChunkProgressionTask> allTasks, ++ final PrioritisedExecutor.Priority minPriority) { ++ if (!this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, getAccessRadius(targetStatus))) { ++ throw new IllegalStateException("Not holding scheduling lock"); ++ } ++ ++ if (chunkHolder.hasGenerationTask()) { ++ chunkHolder.upgradeGenTarget(targetStatus); ++ return null; ++ } ++ ++ final PrioritisedExecutor.Priority requestedPriority = PrioritisedExecutor.Priority.max( ++ minPriority, chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) ++ ); ++ final ChunkStatus currentGenStatus = chunkHolder.getCurrentGenStatus(); ++ final ChunkAccess chunk = chunkHolder.getCurrentChunk(); ++ ++ if (currentGenStatus == null) { ++ // not yet loaded ++ final ChunkProgressionTask task = this.createTask( ++ chunkX, chunkZ, chunk, chunkHolder, null, ChunkStatus.EMPTY, requestedPriority ++ ); ++ ++ allTasks.add(task); ++ ++ final List<NewChunkHolder> chunkHolderNeighbours = new ArrayList<>(1); ++ chunkHolderNeighbours.add(chunkHolder); ++ ++ chunkHolder.setGenerationTarget(targetStatus); ++ chunkHolder.setGenerationTask(task, ChunkStatus.EMPTY, chunkHolderNeighbours); ++ ++ return task; ++ } ++ ++ if (currentGenStatus.isOrAfter(targetStatus)) { ++ // nothing to do ++ return null; ++ } ++ ++ // we know for sure now that we want to schedule _something_, so set the target ++ chunkHolder.setGenerationTarget(targetStatus); ++ ++ final ChunkStatus chunkRealStatus = chunk.getPersistedStatus(); ++ final ChunkStatus toStatus = ((ChunkSystemChunkStatus)currentGenStatus).moonrise$getNextStatus(); ++ final ChunkPyramid chunkPyramid = chunkRealStatus.isOrAfter(toStatus) ? ChunkPyramid.LOADING_PYRAMID : ChunkPyramid.GENERATION_PYRAMID; ++ final ChunkStep chunkStep = chunkPyramid.getStepTo(toStatus); ++ ++ final int neighbourReadRadius = Math.max( ++ 0, ++ chunkPyramid.getStepTo(toStatus).getAccumulatedRadiusOf(ChunkStatus.EMPTY) ++ ); ++ ++ boolean unGeneratedNeighbours = false; ++ ++ if (neighbourReadRadius > 0) { ++ final ChunkMap chunkMap = this.world.getChunkSource().chunkMap; ++ for (final long pos : ParallelSearchRadiusIteration.getSearchIteration(neighbourReadRadius)) { ++ final int x = CoordinateUtils.getChunkX(pos); ++ final int z = CoordinateUtils.getChunkZ(pos); ++ final int radius = Math.max(Math.abs(x), Math.abs(z)); ++ final ChunkStatus requiredNeighbourStatus = ((ChunkSystemChunkStep)(Object)chunkStep).moonrise$getRequiredStatusAtRadius(radius); ++ ++ unGeneratedNeighbours |= this.checkNeighbour( ++ chunkX + x, chunkZ + z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority ++ ); ++ } ++ } ++ ++ if (unGeneratedNeighbours) { ++ // can't schedule, but neighbour completion will schedule for us when they're ALL done ++ ++ // propagate our priority to neighbours ++ chunkHolder.recalculateNeighbourPriorities(); ++ return null; ++ } ++ ++ // need to gather neighbours ++ ++ final List<NewChunkHolder> chunkHolderNeighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1)); ++ final StaticCache2D<GenerationChunkHolder> neighbours = StaticCache2D ++ .create(chunkX, chunkZ, neighbourReadRadius, (final int nx, final int nz) -> { ++ final NewChunkHolder holder = nx == chunkX && nz == chunkZ ? chunkHolder : this.chunkHolderManager.getChunkHolder(nx, nz); ++ chunkHolderNeighbours.add(holder); ++ ++ return holder.vanillaChunkHolder; ++ }); ++ ++ final ChunkProgressionTask task = this.createTask( ++ chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus, ++ chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) ++ ); ++ allTasks.add(task); ++ ++ chunkHolder.setGenerationTask(task, toStatus, chunkHolderNeighbours); ++ ++ return task; ++ } ++ ++ // rets true if the neighbour is not at the required status, false otherwise ++ private boolean checkNeighbour(final int chunkX, final int chunkZ, final ChunkStatus requiredStatus, final NewChunkHolder center, ++ final List<ChunkProgressionTask> tasks, final PrioritisedExecutor.Priority minPriority) { ++ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); ++ ++ if (chunkHolder == null) { ++ throw new IllegalStateException("Missing chunkholder when required"); ++ } ++ ++ final ChunkStatus holderStatus = chunkHolder.getCurrentGenStatus(); ++ if (holderStatus != null && holderStatus.isOrAfter(requiredStatus)) { ++ return false; ++ } ++ ++ if (chunkHolder.hasFailedGeneration()) { ++ return true; ++ } ++ ++ center.addGenerationBlockingNeighbour(chunkHolder); ++ chunkHolder.addWaitingNeighbour(center, requiredStatus); ++ ++ if (chunkHolder.upgradeGenTarget(requiredStatus)) { ++ return true; ++ } ++ ++ // not at status required, so we need to schedule its generation ++ this.schedule( ++ chunkX, chunkZ, requiredStatus, chunkHolder, tasks, minPriority ++ ); ++ ++ return true; ++ } ++ ++ /** ++ * @deprecated Chunk tasks must be tied to coordinates in the future ++ */ ++ @Deprecated ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) { ++ return this.scheduleChunkTask(run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ /** ++ * @deprecated Chunk tasks must be tied to coordinates in the future ++ */ ++ @Deprecated ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return this.mainThreadExecutor.queueRunnable(run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { ++ return this.createChunkTask(chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, ++ final PrioritisedExecutor.Priority priority) { ++ return this.mainThreadExecutor.createTask(run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { ++ return this.mainThreadExecutor.queueRunnable(run); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, ++ final PrioritisedExecutor.Priority priority) { ++ return this.mainThreadExecutor.queueRunnable(run, priority); ++ } ++ ++ public boolean halt(final boolean sync, final long maxWaitNS) { ++ this.radiusAwareGenExecutor.halt(); ++ this.parallelGenExecutor.halt(); ++ this.loadExecutor.halt(); ++ final long time = System.nanoTime(); ++ if (sync) { ++ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { ++ if ( ++ !this.radiusAwareGenExecutor.isActive() && ++ !this.parallelGenExecutor.isActive() && ++ !this.loadExecutor.isActive() ++ ) { ++ return true; ++ } ++ if ((System.nanoTime() - time) >= maxWaitNS) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ public static final ArrayDeque<ChunkInfo> WAITING_CHUNKS = new ArrayDeque<>(); // stack ++ ++ public static final class ChunkInfo { ++ ++ public final int chunkX; ++ public final int chunkZ; ++ public final ServerLevel world; ++ ++ public ChunkInfo(final int chunkX, final int chunkZ, final ServerLevel world) { ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.world = world; ++ } ++ ++ public JsonObject toJson() { ++ final JsonObject ret = new JsonObject(); ++ ++ ret.addProperty("chunk-x", Integer.valueOf(this.chunkX)); ++ ret.addProperty("chunk-z", Integer.valueOf(this.chunkZ)); ++ ret.addProperty("world-name", WorldUtil.getWorldName(this.world)); ++ ++ return ret; ++ } ++ ++ @Override ++ public String toString() { ++ return "[( " + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "']"; ++ } ++ } ++ ++ public static void pushChunkWait(final ServerLevel world, final int chunkX, final int chunkZ) { ++ synchronized (WAITING_CHUNKS) { ++ WAITING_CHUNKS.push(new ChunkInfo(chunkX, chunkZ, world)); ++ } ++ } ++ ++ public static void popChunkWait() { ++ synchronized (WAITING_CHUNKS) { ++ WAITING_CHUNKS.pop(); ++ } ++ } ++ ++ public static ChunkInfo[] getChunkInfos() { ++ synchronized (WAITING_CHUNKS) { ++ return WAITING_CHUNKS.toArray(new ChunkInfo[0]); ++ } ++ } ++ ++ private static JsonObject debugPlayer(final ServerPlayer player) { ++ final Level world = player.level(); ++ ++ final JsonObject ret = new JsonObject(); ++ ++ ret.addProperty("name", player.getScoreboardName()); ++ ret.addProperty("uuid", player.getUUID().toString()); ++ ret.addProperty("real", ((ChunkSystemServerPlayer)player).moonrise$isRealPlayer()); ++ ++ ret.addProperty("world-name", WorldUtil.getWorldName(world)); ++ ++ final Vec3 pos = player.position(); ++ ++ ret.addProperty("x", pos.x); ++ ret.addProperty("y", pos.y); ++ ret.addProperty("z", pos.z); ++ ++ final Entity.RemovalReason removalReason = player.getRemovalReason(); ++ ++ ret.addProperty("removal-reason", removalReason == null ? "null" : removalReason.name()); ++ ++ ret.add("view-distances", ((ChunkSystemServerPlayer)player).moonrise$getViewDistanceHolder().toJson()); ++ ++ return ret; ++ } ++ ++ public JsonObject getDebugJson() { ++ final JsonObject ret = new JsonObject(); ++ ++ ret.addProperty("lock_shift", Integer.valueOf(this.getChunkSystemLockShift())); ++ ret.addProperty("ticket_shift", Integer.valueOf(ThreadedTicketLevelPropagator.SECTION_SHIFT)); ++ ret.addProperty("region_shift", Integer.valueOf(((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift())); ++ ++ ret.addProperty("name", WorldUtil.getWorldName(this.world)); ++ ret.addProperty("view-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPIViewDistance()); ++ ret.addProperty("tick-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPITickDistance()); ++ ret.addProperty("send-distance", ((ChunkSystemServerLevel)this.world).moonrise$getPlayerChunkLoader().getAPISendViewDistance()); ++ ++ final JsonArray players = new JsonArray(); ++ ret.add("players", players); ++ ++ for (final ServerPlayer player : this.world.players()) { ++ players.add(debugPlayer(player)); ++ } ++ ++ ret.add("chunk-holder-manager", this.chunkHolderManager.getDebugJson()); ++ ++ return ret; ++ } ++ ++ public static JsonObject debugAllWorlds(final MinecraftServer server) { ++ final JsonObject ret = new JsonObject(); ++ ++ ret.addProperty("data-version", 2); ++ ++ final JsonArray allPlayers = new JsonArray(); ++ ret.add("all-players", allPlayers); ++ ++ for (final ServerPlayer player : server.getPlayerList().getPlayers()) { ++ allPlayers.add(debugPlayer(player)); ++ } ++ ++ final JsonArray chunkWaitInfos = new JsonArray(); ++ ret.add("chunk-wait-infos", chunkWaitInfos); ++ ++ for (final ChunkTaskScheduler.ChunkInfo info : getChunkInfos()) { ++ chunkWaitInfos.add(info.toJson()); ++ } ++ ++ final JsonArray worlds = new JsonArray(); ++ ret.add("worlds", worlds); ++ ++ for (final ServerLevel world : server.getAllLevels()) { ++ worlds.add(((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().getDebugJson()); ++ } ++ ++ return ret; ++ } ++ ++ public static File getChunkDebugFile() { ++ return new File( ++ new File(new File("."), "debug"), ++ "chunks-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt" ++ ); ++ } ++ ++ public static void dumpAllChunkLoadInfo(final MinecraftServer server, final boolean writeDebugInfo) { ++ final ChunkInfo[] chunkInfos = getChunkInfos(); ++ if (chunkInfos.length > 0) { ++ LOGGER.error("Chunk wait task info below: "); ++ for (final ChunkInfo chunkInfo : chunkInfos) { ++ final NewChunkHolder holder = ((ChunkSystemServerLevel)chunkInfo.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkInfo.chunkX, chunkInfo.chunkZ); ++ LOGGER.error("Chunk wait: " + chunkInfo); ++ LOGGER.error("Chunk holder: " + holder); ++ } ++ ++ if (writeDebugInfo) { ++ final File file = getChunkDebugFile(); ++ LOGGER.error("Writing chunk information dump to " + file); ++ try { ++ JsonUtil.writeJson(ChunkTaskScheduler.debugAllWorlds(server), file); ++ LOGGER.error("Successfully written chunk information!"); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to dump chunk information to file " + file.toString(), thr); ++ } ++ } ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d5fc5756ea960096ff23376a6b7ac68a2a462d22 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +@@ -0,0 +1,2034 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.completable.Completable; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem; ++import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemFeatures; ++import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkLoadTask; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.GenericDataLoadTask; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonElement; ++import com.google.gson.JsonNull; ++import com.google.gson.JsonObject; ++import com.google.gson.JsonPrimitive; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ChunkLevel; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ImposterProtoChunk; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.storage.ChunkSerializer; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayList; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++import java.util.Objects; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.function.Consumer; ++ ++public final class NewChunkHolder { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(NewChunkHolder.class); ++ ++ public final ServerLevel world; ++ public final int chunkX; ++ public final int chunkZ; ++ ++ public final ChunkTaskScheduler scheduler; ++ ++ // load/unload state ++ ++ // chunk data state ++ ++ private ChunkEntitySlices entityChunk; ++ // entity chunk that is loaded, but not yet deserialized ++ private CompoundTag pendingEntityChunk; ++ ++ ChunkEntitySlices loadInEntityChunk(final boolean transientChunk) { ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot sync load entity data off-main"); ++ final CompoundTag entityChunk; ++ final ChunkEntitySlices ret; ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ if (this.entityChunk != null && (transientChunk || !this.entityChunk.isTransient())) { ++ return this.entityChunk; ++ } ++ final CompoundTag pendingEntityChunk = this.pendingEntityChunk; ++ if (!transientChunk && pendingEntityChunk == null) { ++ throw new IllegalStateException("Must load entity data from disk before loading in the entity chunk!"); ++ } ++ ++ if (this.entityChunk == null) { ++ ret = this.entityChunk = new ChunkEntitySlices( ++ this.world, this.chunkX, this.chunkZ, this.getChunkStatus(), ++ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) ++ ); ++ ++ ret.setTransient(transientChunk); ++ ++ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionLoad(this.chunkX, this.chunkZ, ret); ++ } else { ++ // transientChunk = false here ++ ret = this.entityChunk; ++ this.entityChunk.setTransient(false); ++ } ++ ++ if (!transientChunk) { ++ this.pendingEntityChunk = null; ++ entityChunk = pendingEntityChunk == EMPTY_ENTITY_CHUNK ? null : pendingEntityChunk; ++ } else { ++ entityChunk = null; ++ } ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ ++ if (!transientChunk) { ++ if (entityChunk != null) { ++ final List<Entity> entities = ChunkEntitySlices.readEntities(this.world, entityChunk); ++ ++ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().addEntityChunkEntities(entities, new ChunkPos(this.chunkX, this.chunkZ)); ++ } ++ } ++ ++ return ret; ++ } ++ ++ // needed to distinguish whether the entity chunk has been read from disk but is empty or whether it has _not_ ++ // been read from disk ++ private static final CompoundTag EMPTY_ENTITY_CHUNK = new CompoundTag(); ++ ++ private ChunkLoadTask.EntityDataLoadTask entityDataLoadTask; ++ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0, ++ // then the task is rescheduled ++ private List<GenericDataLoadTaskCallback> entityDataLoadTaskWaiters; ++ ++ public ChunkLoadTask.EntityDataLoadTask getEntityDataLoadTask() { ++ return this.entityDataLoadTask; ++ } ++ ++ // must hold schedule lock for the two below functions ++ ++ // returns only if the data has been loaded from disk, DOES NOT relate to whether it has been deserialized ++ // or added into the world (or even into entityChunk) ++ public boolean isEntityChunkNBTLoaded() { ++ return (this.entityChunk != null && !this.entityChunk.isTransient()) || this.pendingEntityChunk != null; ++ } ++ ++ private void completeEntityLoad(final GenericDataLoadTask.TaskResult<CompoundTag, Throwable> result) { ++ final List<GenericDataLoadTaskCallback> completeWaiters; ++ ChunkLoadTask.EntityDataLoadTask entityDataLoadTask = null; ++ boolean scheduleEntityTask = false; ++ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ final List<GenericDataLoadTaskCallback> waiters = this.entityDataLoadTaskWaiters; ++ this.entityDataLoadTask = null; ++ if (result != null) { ++ this.entityDataLoadTaskWaiters = null; ++ this.pendingEntityChunk = result.left() == null ? EMPTY_ENTITY_CHUNK : result.left(); ++ if (result.right() != null) { ++ LOGGER.error("Unhandled entity data load exception, data data will be lost: ", result.right()); ++ } ++ ++ for (final GenericDataLoadTaskCallback callback : waiters) { ++ callback.markCompleted(); ++ } ++ ++ completeWaiters = waiters; ++ } else { ++ // cancelled ++ completeWaiters = null; ++ ++ // need to re-schedule? ++ if (waiters.isEmpty()) { ++ this.entityDataLoadTaskWaiters = null; ++ // no tasks to schedule _for_ ++ } else { ++ entityDataLoadTask = this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) ++ ); ++ entityDataLoadTask.addCallback(this::completeEntityLoad); ++ // need one schedule() per waiter ++ for (final GenericDataLoadTaskCallback callback : waiters) { ++ scheduleEntityTask |= entityDataLoadTask.schedule(true); ++ } ++ } ++ } ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ ++ if (scheduleEntityTask) { ++ entityDataLoadTask.scheduleNow(); ++ } ++ ++ // avoid holding the scheduling lock while completing ++ if (completeWaiters != null) { ++ for (final GenericDataLoadTaskCallback callback : completeWaiters) { ++ callback.acceptCompleted(result); ++ } ++ } ++ ++ schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ this.checkUnload(); ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } ++ ++ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held ++ // however, when the consumer is invoked, it will hold the schedule lock ++ public GenericDataLoadTaskCallback getOrLoadEntityData(final Consumer<GenericDataLoadTask.TaskResult<CompoundTag, Throwable>> consumer) { ++ if (this.isEntityChunkNBTLoaded()) { ++ throw new IllegalStateException("Cannot load entity data, it is already loaded"); ++ } ++ // why not just acquire the lock? because the caller NEEDS to call isEntityChunkNBTLoaded before this! ++ if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) { ++ throw new IllegalStateException("Must hold scheduling lock"); ++ } ++ ++ final GenericDataLoadTaskCallback ret = new EntityDataLoadTaskCallback((Consumer)consumer, this); ++ ++ if (this.entityDataLoadTask == null) { ++ this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) ++ ); ++ this.entityDataLoadTask.addCallback(this::completeEntityLoad); ++ this.entityDataLoadTaskWaiters = new ArrayList<>(); ++ } ++ this.entityDataLoadTaskWaiters.add(ret); ++ if (this.entityDataLoadTask.schedule(true)) { ++ ret.schedule = this.entityDataLoadTask; ++ } ++ this.checkUnload(); ++ ++ return ret; ++ } ++ ++ private static final class EntityDataLoadTaskCallback extends GenericDataLoadTaskCallback { ++ ++ public EntityDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer, final NewChunkHolder chunkHolder) { ++ super(consumer, chunkHolder); ++ } ++ ++ @Override ++ void internalCancel() { ++ this.chunkHolder.entityDataLoadTaskWaiters.remove(this); ++ this.chunkHolder.entityDataLoadTask.cancel(); ++ } ++ } ++ ++ private PoiChunk poiChunk; ++ ++ private ChunkLoadTask.PoiDataLoadTask poiDataLoadTask; ++ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0, ++ // then the task is rescheduled ++ private List<GenericDataLoadTaskCallback> poiDataLoadTaskWaiters; ++ ++ public ChunkLoadTask.PoiDataLoadTask getPoiDataLoadTask() { ++ return this.poiDataLoadTask; ++ } ++ ++ // must hold schedule lock for the two below functions ++ ++ public boolean isPoiChunkLoaded() { ++ return this.poiChunk != null; ++ } ++ ++ private void completePoiLoad(final GenericDataLoadTask.TaskResult<PoiChunk, Throwable> result) { ++ final List<GenericDataLoadTaskCallback> completeWaiters; ++ ChunkLoadTask.PoiDataLoadTask poiDataLoadTask = null; ++ boolean schedulePoiTask = false; ++ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ final List<GenericDataLoadTaskCallback> waiters = this.poiDataLoadTaskWaiters; ++ this.poiDataLoadTask = null; ++ if (result != null) { ++ this.poiDataLoadTaskWaiters = null; ++ this.poiChunk = result.left(); ++ if (result.right() != null) { ++ LOGGER.error("Unhandled poi load exception, poi data will be lost: ", result.right()); ++ } ++ ++ for (final GenericDataLoadTaskCallback callback : waiters) { ++ callback.markCompleted(); ++ } ++ ++ completeWaiters = waiters; ++ } else { ++ // cancelled ++ completeWaiters = null; ++ ++ // need to re-schedule? ++ if (waiters.isEmpty()) { ++ this.poiDataLoadTaskWaiters = null; ++ // no tasks to schedule _for_ ++ } else { ++ poiDataLoadTask = this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) ++ ); ++ poiDataLoadTask.addCallback(this::completePoiLoad); ++ // need one schedule() per waiter ++ for (final GenericDataLoadTaskCallback callback : waiters) { ++ schedulePoiTask |= poiDataLoadTask.schedule(true); ++ } ++ } ++ } ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ ++ if (schedulePoiTask) { ++ poiDataLoadTask.scheduleNow(); ++ } ++ ++ // avoid holding the scheduling lock while completing ++ if (completeWaiters != null) { ++ for (final GenericDataLoadTaskCallback callback : completeWaiters) { ++ callback.acceptCompleted(result); ++ } ++ } ++ schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ this.checkUnload(); ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } ++ ++ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held ++ // however, when the consumer is invoked, it will hold the schedule lock ++ public GenericDataLoadTaskCallback getOrLoadPoiData(final Consumer<GenericDataLoadTask.TaskResult<PoiChunk, Throwable>> consumer) { ++ if (this.isPoiChunkLoaded()) { ++ throw new IllegalStateException("Cannot load poi data, it is already loaded"); ++ } ++ // why not just acquire the lock? because the caller NEEDS to call isPoiChunkLoaded before this! ++ if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) { ++ throw new IllegalStateException("Must hold scheduling lock"); ++ } ++ ++ final GenericDataLoadTaskCallback ret = new PoiDataLoadTaskCallback((Consumer)consumer, this); ++ ++ if (this.poiDataLoadTask == null) { ++ this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL) ++ ); ++ this.poiDataLoadTask.addCallback(this::completePoiLoad); ++ this.poiDataLoadTaskWaiters = new ArrayList<>(); ++ } ++ this.poiDataLoadTaskWaiters.add(ret); ++ if (this.poiDataLoadTask.schedule(true)) { ++ ret.schedule = this.poiDataLoadTask; ++ } ++ this.checkUnload(); ++ ++ return ret; ++ } ++ ++ private static final class PoiDataLoadTaskCallback extends GenericDataLoadTaskCallback { ++ ++ public PoiDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer, final NewChunkHolder chunkHolder) { ++ super(consumer, chunkHolder); ++ } ++ ++ @Override ++ void internalCancel() { ++ this.chunkHolder.poiDataLoadTaskWaiters.remove(this); ++ this.chunkHolder.poiDataLoadTask.cancel(); ++ } ++ } ++ ++ public static abstract class GenericDataLoadTaskCallback implements Cancellable { ++ ++ protected final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer; ++ protected final NewChunkHolder chunkHolder; ++ protected boolean completed; ++ protected GenericDataLoadTask<?, ?> schedule; ++ protected final AtomicBoolean scheduled = new AtomicBoolean(); ++ ++ public GenericDataLoadTaskCallback(final Consumer<GenericDataLoadTask.TaskResult<?, Throwable>> consumer, ++ final NewChunkHolder chunkHolder) { ++ this.consumer = consumer; ++ this.chunkHolder = chunkHolder; ++ } ++ ++ public void schedule() { ++ if (this.scheduled.getAndSet(true)) { ++ throw new IllegalStateException("Double calling schedule()"); ++ } ++ if (this.schedule != null) { ++ this.schedule.scheduleNow(); ++ this.schedule = null; ++ } ++ } ++ ++ boolean isCompleted() { ++ return this.completed; ++ } ++ ++ // must hold scheduling lock ++ private boolean setCompleted() { ++ if (this.completed) { ++ return false; ++ } ++ return this.completed = true; ++ } ++ ++ // must hold scheduling lock ++ void markCompleted() { ++ if (this.completed) { ++ throw new IllegalStateException("May not be completed here"); ++ } ++ this.completed = true; ++ } ++ ++ void acceptCompleted(final GenericDataLoadTask.TaskResult<?, Throwable> result) { ++ if (result != null) { ++ if (this.completed) { ++ this.consumer.accept(result); ++ } else { ++ throw new IllegalStateException("Cannot be uncompleted at this point"); ++ } ++ } else { ++ throw new NullPointerException("Result cannot be null (cancelled)"); ++ } ++ } ++ ++ // holds scheduling lock ++ abstract void internalCancel(); ++ ++ @Override ++ public boolean cancel() { ++ final NewChunkHolder holder = this.chunkHolder; ++ final ReentrantAreaLock.Node schedulingLock = holder.scheduler.schedulingLockArea.lock(holder.chunkX, holder.chunkZ); ++ try { ++ if (!this.completed) { ++ this.completed = true; ++ this.internalCancel(); ++ return true; ++ } ++ return false; ++ } finally { ++ holder.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } ++ } ++ ++ private ChunkAccess currentChunk; ++ ++ // generation status state ++ ++ /** ++ * Current status the chunk has been brought up to by the chunk system. null indicates no work at all ++ */ ++ private ChunkStatus currentGenStatus; ++ ++ // This allows lockless access to the chunk and last gen status ++ private static final ChunkStatus[] ALL_STATUSES = ChunkStatus.getStatusList().toArray(new ChunkStatus[0]); ++ ++ public static final record ChunkCompletion(ChunkAccess chunk, ChunkStatus genStatus) {}; ++ private static final VarHandle CHUNK_COMPLETION_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(ChunkCompletion[].class); ++ private final ChunkCompletion[] chunkCompletions = new ChunkCompletion[ALL_STATUSES.length]; ++ ++ private volatile ChunkCompletion lastChunkCompletion; ++ ++ public ChunkCompletion getLastChunkCompletion() { ++ return this.lastChunkCompletion; ++ } ++ ++ public ChunkAccess getChunkIfPresentUnchecked(final ChunkStatus status) { ++ final ChunkCompletion completion = (ChunkCompletion)CHUNK_COMPLETION_ARRAY_HANDLE.getVolatile(this.chunkCompletions, status.getIndex()); ++ return completion == null ? null : completion.chunk; ++ } ++ ++ public ChunkAccess getChunkIfPresent(final ChunkStatus status) { ++ final ChunkStatus maxStatus = ChunkLevel.generationStatus(this.getTicketLevel()); ++ ++ if (maxStatus == null || status.isAfter(maxStatus)) { ++ return null; ++ } ++ ++ return this.getChunkIfPresentUnchecked(status); ++ } ++ ++ public void replaceProtoChunk(final ImposterProtoChunk imposterProtoChunk) { ++ for (int i = 0, max = ChunkStatus.FULL.getIndex(); i < max; ++i) { ++ CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, i, new ChunkCompletion(imposterProtoChunk, ALL_STATUSES[i])); ++ } ++ } ++ ++ /** ++ * The target final chunk status the chunk system will bring the chunk to. ++ */ ++ private ChunkStatus requestedGenStatus; ++ ++ private ChunkProgressionTask generationTask; ++ private ChunkStatus generationTaskStatus; ++ ++ /** ++ * contains the neighbours that this chunk generation is blocking on ++ */ ++ private final ReferenceLinkedOpenHashSet<NewChunkHolder> neighboursBlockingGenTask = new ReferenceLinkedOpenHashSet<>(4); ++ ++ /** ++ * map of ChunkHolder -> Required Status for this chunk ++ */ ++ private final Reference2ObjectLinkedOpenHashMap<NewChunkHolder, ChunkStatus> neighboursWaitingForUs = new Reference2ObjectLinkedOpenHashMap<>(); ++ ++ public void addGenerationBlockingNeighbour(final NewChunkHolder neighbour) { ++ this.neighboursBlockingGenTask.add(neighbour); ++ } ++ ++ public void addWaitingNeighbour(final NewChunkHolder neighbour, final ChunkStatus requiredStatus) { ++ final boolean wasEmpty = this.neighboursWaitingForUs.isEmpty(); ++ this.neighboursWaitingForUs.put(neighbour, requiredStatus); ++ if (wasEmpty) { ++ this.checkUnload(); ++ } ++ } ++ ++ // priority state ++ ++ // the target priority for this chunk to generate at ++ private PrioritisedExecutor.Priority priority = null; ++ private boolean priorityLocked; ++ ++ // the priority neighbouring chunks have requested this chunk generate at ++ private PrioritisedExecutor.Priority neighbourRequestedPriority = null; ++ ++ public PrioritisedExecutor.Priority getEffectivePriority(final PrioritisedExecutor.Priority dfl) { ++ final PrioritisedExecutor.Priority neighbour = this.neighbourRequestedPriority; ++ final PrioritisedExecutor.Priority us = this.priority; ++ ++ if (neighbour == null) { ++ return us == null ? dfl : us; ++ } ++ if (us == null) { ++ return dfl; ++ } ++ ++ return PrioritisedExecutor.Priority.max(us, neighbour); ++ } ++ ++ private void recalculateNeighbourRequestedPriority() { ++ if (this.neighboursWaitingForUs.isEmpty()) { ++ this.neighbourRequestedPriority = null; ++ return; ++ } ++ ++ PrioritisedExecutor.Priority max = null; ++ ++ for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) { ++ final PrioritisedExecutor.Priority neighbourPriority = holder.getEffectivePriority(null); ++ if (neighbourPriority != null && (max == null || neighbourPriority.isHigherPriority(max))) { ++ max = neighbourPriority; ++ } ++ } ++ ++ final PrioritisedExecutor.Priority current = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL); ++ this.neighbourRequestedPriority = max; ++ final PrioritisedExecutor.Priority next = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL); ++ ++ if (current == next) { ++ return; ++ } ++ ++ // our effective priority has changed, so change our task ++ if (this.generationTask != null) { ++ this.generationTask.setPriority(next); ++ } ++ ++ // now propagate this to our neighbours ++ this.recalculateNeighbourPriorities(); ++ } ++ ++ public void recalculateNeighbourPriorities() { ++ for (final NewChunkHolder holder : this.neighboursBlockingGenTask) { ++ holder.recalculateNeighbourRequestedPriority(); ++ } ++ } ++ ++ // must hold scheduling lock ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (this.priority == null || this.priority.isHigherOrEqualPriority(priority)) { ++ return; ++ } ++ this.setPriority(priority); ++ } ++ ++ private void lockPriority() { ++ this.priority = null; ++ this.priorityLocked = true; ++ } ++ ++ // must hold scheduling lock ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (this.priorityLocked) { ++ return; ++ } ++ final PrioritisedExecutor.Priority old = this.getEffectivePriority(null); ++ this.priority = priority; ++ final PrioritisedExecutor.Priority newPriority = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL); ++ ++ if (old != newPriority) { ++ if (this.generationTask != null) { ++ this.generationTask.setPriority(newPriority); ++ } ++ } ++ ++ this.recalculateNeighbourPriorities(); ++ } ++ ++ // must hold scheduling lock ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (this.priority == null || this.priority.isLowerOrEqualPriority(priority)) { ++ return; ++ } ++ this.setPriority(priority); ++ } ++ ++ // error handling state ++ private ChunkStatus failedGenStatus; ++ private Throwable genTaskException; ++ private Thread genTaskFailedThread; ++ ++ private boolean failedLightUpdate; ++ ++ public void failedLightUpdate() { ++ this.failedLightUpdate = true; ++ } ++ ++ public boolean hasFailedGeneration() { ++ return this.genTaskException != null; ++ } ++ ++ // ticket level state ++ public int oldTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1; ++ private int currentTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1; ++ ++ public int getTicketLevel() { ++ return this.currentTicketLevel; ++ } ++ ++ public final ChunkHolder vanillaChunkHolder; ++ ++ public NewChunkHolder(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkTaskScheduler scheduler) { ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.scheduler = scheduler; ++ this.vanillaChunkHolder = new ChunkHolder( ++ new ChunkPos(chunkX, chunkZ), ChunkHolderManager.MAX_TICKET_LEVEL, world, ++ world.getLightEngine(), null, world.getChunkSource().chunkMap ++ ); ++ ((ChunkSystemChunkHolder)this.vanillaChunkHolder).moonrise$setRealChunkHolder(this); ++ } ++ ++ public ChunkAccess getCurrentChunk() { ++ return this.currentChunk; ++ } ++ ++ int getCurrentTicketLevel() { ++ return this.currentTicketLevel; ++ } ++ ++ void updateTicketLevel(final int toLevel) { ++ this.currentTicketLevel = toLevel; ++ } ++ ++ private int totalNeighboursUsingThisChunk = 0; ++ ++ // holds schedule lock ++ public void addNeighbourUsingChunk() { ++ final int now = ++this.totalNeighboursUsingThisChunk; ++ ++ if (now == 1) { ++ this.checkUnload(); ++ } ++ } ++ ++ // holds schedule lock ++ public void removeNeighbourUsingChunk() { ++ final int now = --this.totalNeighboursUsingThisChunk; ++ ++ if (now == 0) { ++ this.checkUnload(); ++ } ++ ++ if (now < 0) { ++ throw new IllegalStateException("Neighbours using this chunk cannot be negative"); ++ } ++ } ++ ++ // must hold scheduling lock ++ // returns string reason for why chunk should remain loaded, null otherwise ++ public final String isSafeToUnload() { ++ // is ticket level below threshold? ++ if (this.oldTicketLevel <= ChunkHolderManager.MAX_TICKET_LEVEL) { ++ return "ticket_level"; ++ } ++ ++ // are we being used by another chunk for generation? ++ if (this.totalNeighboursUsingThisChunk != 0) { ++ return "neighbours_generating"; ++ } ++ ++ // are we going to be used by another chunk for generation? ++ if (!this.neighboursWaitingForUs.isEmpty()) { ++ return "neighbours_waiting"; ++ } ++ ++ // chunk must be marked inaccessible (i.e. unloaded to plugins) ++ if (this.getChunkStatus() != FullChunkStatus.INACCESSIBLE) { ++ return "fullchunkstatus"; ++ } ++ ++ // are we currently generating anything, or have requested generation? ++ if (this.generationTask != null) { ++ return "generating"; ++ } ++ if (this.requestedGenStatus != null) { ++ return "requested_generation"; ++ } ++ ++ // entity data requested? ++ if (this.entityDataLoadTask != null) { ++ return "entity_data_requested"; ++ } ++ ++ // poi data requested? ++ if (this.poiDataLoadTask != null) { ++ return "poi_data_requested"; ++ } ++ ++ // are we pending serialization? ++ if (this.entityDataUnload != null) { ++ return "entity_serialization"; ++ } ++ if (this.poiDataUnload != null) { ++ return "poi_serialization"; ++ } ++ if (this.chunkDataUnload != null) { ++ return "chunk_serialization"; ++ } ++ ++ // Note: light tasks do not need a check, as they add a ticket. ++ ++ // nothing is using this chunk, so it should be unloaded ++ return null; ++ } ++ ++ /** Unloaded from chunk map */ ++ private boolean unloaded; ++ ++ void markUnloaded() { ++ this.unloaded = true; ++ } ++ ++ private boolean inUnloadQueue = false; ++ ++ void removeFromUnloadQueue() { ++ this.inUnloadQueue = false; ++ } ++ ++ // must hold scheduling lock ++ private void checkUnload() { ++ if (this.unloaded) { ++ return; ++ } ++ if (this.isSafeToUnload() == null) { ++ // ensure in unload queue ++ if (!this.inUnloadQueue) { ++ this.inUnloadQueue = true; ++ this.scheduler.chunkHolderManager.unloadQueue.addChunk(this.chunkX, this.chunkZ); ++ } ++ } else { ++ // ensure not in unload queue ++ if (this.inUnloadQueue) { ++ this.inUnloadQueue = false; ++ this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ); ++ } ++ } ++ } ++ ++ static final record UnloadState(NewChunkHolder holder, ChunkAccess chunk, ChunkEntitySlices entityChunk, PoiChunk poiChunk) {}; ++ ++ // note: these are completed with null to indicate that no write occurred ++ // they are also completed with null to indicate a null write occurred ++ private UnloadTask chunkDataUnload; ++ private UnloadTask entityDataUnload; ++ private UnloadTask poiDataUnload; ++ ++ public static final record UnloadTask(Completable<CompoundTag> completable, DelayedPrioritisedTask task) {} ++ ++ public UnloadTask getUnloadTask(final RegionFileIOThread.RegionFileType type) { ++ switch (type) { ++ case CHUNK_DATA: ++ return this.chunkDataUnload; ++ case ENTITY_DATA: ++ return this.entityDataUnload; ++ case POI_DATA: ++ return this.poiDataUnload; ++ default: ++ throw new IllegalStateException("Unknown regionfile type " + type); ++ } ++ } ++ ++ private void removeUnloadTask(final RegionFileIOThread.RegionFileType type) { ++ switch (type) { ++ case CHUNK_DATA: { ++ this.chunkDataUnload = null; ++ return; ++ } ++ case ENTITY_DATA: { ++ this.entityDataUnload = null; ++ return; ++ } ++ case POI_DATA: { ++ this.poiDataUnload = null; ++ return; ++ } ++ default: ++ throw new IllegalStateException("Unknown regionfile type " + type); ++ } ++ } ++ ++ private UnloadState unloadState; ++ ++ // holds schedule lock ++ UnloadState unloadStage1() { ++ // because we hold the scheduling lock, we cannot actually unload anything ++ // so, what we do here instead is to null this chunk's state and setup the unload tasks ++ // the unload tasks will ensure that any loads that take place after stage1 (i.e during stage2, in which ++ // we do not hold the lock) c ++ final ChunkAccess chunk = this.currentChunk; ++ final ChunkEntitySlices entityChunk = this.entityChunk; ++ final PoiChunk poiChunk = this.poiChunk; ++ // chunk state ++ this.currentChunk = null; ++ this.currentGenStatus = null; ++ this.lastChunkCompletion = null; ++ for (int i = 0; i < this.chunkCompletions.length; ++i) { ++ CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, i, (ChunkCompletion)null); ++ } ++ // entity chunk state ++ this.entityChunk = null; ++ this.pendingEntityChunk = null; ++ ++ // poi chunk state ++ this.poiChunk = null; ++ ++ // priority state ++ this.priorityLocked = false; ++ ++ if (chunk != null) { ++ this.chunkDataUnload = new UnloadTask(new Completable<>(), new DelayedPrioritisedTask(PrioritisedExecutor.Priority.NORMAL)); ++ } ++ if (poiChunk != null) { ++ this.poiDataUnload = new UnloadTask(new Completable<>(), null); ++ } ++ if (entityChunk != null) { ++ this.entityDataUnload = new UnloadTask(new Completable<>(), null); ++ } ++ ++ return this.unloadState = (chunk != null || entityChunk != null || poiChunk != null) ? new UnloadState(this, chunk, entityChunk, poiChunk) : null; ++ } ++ ++ // data is null if failed or does not need to be saved ++ void completeAsyncUnloadDataSave(final RegionFileIOThread.RegionFileType type, final CompoundTag data) { ++ if (data != null) { ++ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, data, type); ++ } ++ ++ this.getUnloadTask(type).completable().complete(data); ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ // can only write to these fields while holding the schedule lock ++ this.removeUnloadTask(type); ++ this.checkUnload(); ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } ++ ++ void unloadStage2(final UnloadState state) { ++ this.unloadState = null; ++ final ChunkAccess chunk = state.chunk(); ++ final ChunkEntitySlices entityChunk = state.entityChunk(); ++ final PoiChunk poiChunk = state.poiChunk(); ++ ++ final boolean shouldLevelChunkNotSave = ChunkSystemFeatures.forceNoSave(chunk); ++ ++ // unload chunk data ++ if (chunk != null) { ++ if (chunk instanceof LevelChunk levelChunk) { ++ levelChunk.setLoaded(false); ++ } ++ ++ if (!shouldLevelChunkNotSave) { ++ this.saveChunk(chunk, true); ++ } else { ++ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null); ++ } ++ ++ if (chunk instanceof LevelChunk levelChunk) { ++ this.world.unload(levelChunk); ++ } ++ } ++ ++ // unload entity data ++ if (entityChunk != null) { ++ this.saveEntities(entityChunk, true); ++ // yes this is a hack to pass the compound tag through... ++ final CompoundTag lastEntityUnload = this.lastEntityUnload; ++ this.lastEntityUnload = null; ++ ++ if (entityChunk.unload()) { ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ entityChunk.setTransient(true); ++ this.entityChunk = entityChunk; ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } else { ++ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().entitySectionUnload(this.chunkX, this.chunkZ); ++ } ++ // we need to delay the callback until after determining transience, otherwise a potential loader could ++ // set entityChunk before we do ++ this.entityDataUnload.completable().complete(lastEntityUnload); ++ } ++ ++ // unload poi data ++ if (poiChunk != null) { ++ if (poiChunk.isDirty() && !shouldLevelChunkNotSave) { ++ this.savePOI(poiChunk, true); ++ } else { ++ this.poiDataUnload.completable().complete(null); ++ } ++ ++ if (poiChunk.isLoaded()) { ++ ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$onUnload(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ)); ++ } ++ } ++ } ++ ++ boolean unloadStage3() { ++ // can only write to these while holding the schedule lock, and we instantly complete them in stage2 ++ this.poiDataUnload = null; ++ this.entityDataUnload = null; ++ ++ // we need to check if anything has been loaded in the meantime (or if we have transient entities) ++ if (this.entityChunk != null || this.poiChunk != null || this.currentChunk != null) { ++ return false; ++ } ++ ++ return this.isSafeToUnload() == null; ++ } ++ ++ private void cancelGenTask() { ++ if (this.generationTask != null) { ++ this.generationTask.cancel(); ++ } else { ++ // otherwise, we are blocking on neighbours, so remove them ++ if (!this.neighboursBlockingGenTask.isEmpty()) { ++ for (final NewChunkHolder neighbour : this.neighboursBlockingGenTask) { ++ if (neighbour.neighboursWaitingForUs.remove(this) == null) { ++ throw new IllegalStateException("Corrupt state"); ++ } ++ if (neighbour.neighboursWaitingForUs.isEmpty()) { ++ neighbour.checkUnload(); ++ } ++ } ++ this.neighboursBlockingGenTask.clear(); ++ this.checkUnload(); ++ } ++ } ++ } ++ ++ // holds: ticket level update lock ++ // holds: schedule lock ++ public void processTicketLevelUpdate(final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedLoadStatus) { ++ final int oldLevel = this.oldTicketLevel; ++ final int newLevel = this.currentTicketLevel; ++ ++ if (oldLevel == newLevel) { ++ return; ++ } ++ ++ this.oldTicketLevel = newLevel; ++ ++ final FullChunkStatus oldState = ChunkLevel.fullStatus(oldLevel); ++ final FullChunkStatus newState = ChunkLevel.fullStatus(newLevel); ++ final boolean oldUnloaded = oldLevel > ChunkHolderManager.MAX_TICKET_LEVEL; ++ final boolean newUnloaded = newLevel > ChunkHolderManager.MAX_TICKET_LEVEL; ++ ++ final ChunkStatus maxGenerationStatusOld = ChunkLevel.generationStatus(oldLevel); ++ final ChunkStatus maxGenerationStatusNew = ChunkLevel.generationStatus(newLevel); ++ ++ // check for cancellations from downgrading ticket level ++ if (this.requestedGenStatus != null && !newState.isOrAfter(FullChunkStatus.FULL) && newLevel > oldLevel) { ++ // note: cancel() may invoke onChunkGenComplete synchronously here ++ if (newUnloaded) { ++ // need to cancel all tasks ++ // note: requested status must be set to null here before cancellation, to indicate to the ++ // completion logic that we do not want rescheduling to occur ++ this.requestedGenStatus = null; ++ this.cancelGenTask(); ++ } else { ++ final ChunkStatus toCancel = ((ChunkSystemChunkStatus)maxGenerationStatusNew).moonrise$getNextStatus(); ++ final ChunkStatus currentRequestedStatus = this.requestedGenStatus; ++ ++ if (currentRequestedStatus.isOrAfter(toCancel)) { ++ // we do have to cancel something here ++ // clamp requested status to the maximum ++ if (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(maxGenerationStatusNew)) { ++ // already generated to status, so we must cancel ++ this.requestedGenStatus = null; ++ this.cancelGenTask(); ++ } else { ++ // not generated to status, so we may have to cancel ++ // note: gen task is always 1 status above current gen status if not null ++ this.requestedGenStatus = maxGenerationStatusNew; ++ if (this.generationTaskStatus != null && this.generationTaskStatus.isOrAfter(toCancel)) { ++ // TOOD is this even possible? i don't think so ++ throw new IllegalStateException("?????"); ++ } ++ } ++ } ++ } ++ } ++ ++ if (oldState != newState) { ++ if (newState.isOrAfter(oldState)) { ++ // status upgrade ++ if (!oldState.isOrAfter(FullChunkStatus.FULL) && newState.isOrAfter(FullChunkStatus.FULL)) { ++ // may need to schedule full load ++ if (this.currentGenStatus != ChunkStatus.FULL) { ++ if (this.requestedGenStatus != null) { ++ this.requestedGenStatus = ChunkStatus.FULL; ++ } else { ++ this.scheduler.schedule( ++ this.chunkX, this.chunkZ, ChunkStatus.FULL, this, scheduledTasks ++ ); ++ } ++ } ++ } ++ } else { ++ // status downgrade ++ if (!newState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && oldState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { ++ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, null); ++ } ++ ++ if (!newState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && oldState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { ++ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, null); ++ } ++ ++ if (!newState.isOrAfter(FullChunkStatus.FULL) && oldState.isOrAfter(FullChunkStatus.FULL)) { ++ this.completeFullStatusConsumers(FullChunkStatus.FULL, null); ++ } ++ } ++ ++ if (this.updatePendingStatus()) { ++ changedLoadStatus.add(this); ++ } ++ } ++ ++ if (oldUnloaded != newUnloaded) { ++ this.checkUnload(); ++ } ++ } ++ ++ static final int NEIGHBOUR_RADIUS = 2; ++ private long fullNeighbourChunksLoadedBitset; ++ ++ private static int getFullNeighbourIndex(final int relativeX, final int relativeZ) { ++ // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1) ++ // optimised variant of the above by moving some of the ops to compile time ++ return relativeX + (relativeZ * (NEIGHBOUR_RADIUS * 2 + 1)) + (NEIGHBOUR_RADIUS + NEIGHBOUR_RADIUS * ((NEIGHBOUR_RADIUS * 2 + 1))); ++ } ++ public final boolean isNeighbourFullLoaded(final int relativeX, final int relativeZ) { ++ return (this.fullNeighbourChunksLoadedBitset & (1L << getFullNeighbourIndex(relativeX, relativeZ))) != 0; ++ } ++ ++ // returns true if this chunk changed pending full status ++ // must hold scheduling lock ++ public final boolean setNeighbourFullLoaded(final int relativeX, final int relativeZ) { ++ final int index = getFullNeighbourIndex(relativeX, relativeZ); ++ this.fullNeighbourChunksLoadedBitset |= (1L << index); ++ return this.updatePendingStatus(); ++ } ++ ++ // returns true if this chunk changed pending full status ++ // must hold scheduling lock ++ public final boolean setNeighbourFullUnloaded(final int relativeX, final int relativeZ) { ++ final int index = getFullNeighbourIndex(relativeX, relativeZ); ++ this.fullNeighbourChunksLoadedBitset &= ~(1L << index); ++ return this.updatePendingStatus(); ++ } ++ ++ private static long getLoadedMask(final int radius) { ++ long mask = 0L; ++ for (int dx = -radius; dx <= radius; ++dx) { ++ for (int dz = -radius; dz <= radius; ++dz) { ++ mask |= (1L << getFullNeighbourIndex(dx, dz)); ++ } ++ } ++ ++ return mask; ++ } ++ ++ private static final long CHUNK_LOADED_MASK_RAD0 = getLoadedMask(0); ++ private static final long CHUNK_LOADED_MASK_RAD1 = getLoadedMask(1); ++ private static final long CHUNK_LOADED_MASK_RAD2 = getLoadedMask(2); ++ ++ public static boolean areNeighboursFullLoaded(final long bitset, final int radius) { ++ switch (radius) { ++ case 0: { ++ return (bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0; ++ } ++ case 1: { ++ return (bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1; ++ } ++ case 2: { ++ return (bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2; ++ } ++ ++ default: { ++ throw new IllegalArgumentException("Radius not recognized: " + radius); ++ } ++ } ++ } ++ ++ // only updated while holding scheduling lock ++ private FullChunkStatus pendingFullChunkStatus = FullChunkStatus.INACCESSIBLE; ++ // updated while holding no locks, but adds a ticket before to prevent pending status from dropping ++ // so, current will never update to a value higher than pending ++ private FullChunkStatus currentFullChunkStatus = FullChunkStatus.INACCESSIBLE; ++ ++ public FullChunkStatus getChunkStatus() { ++ // no volatile access, access off-main is considered racey anyways ++ return this.currentFullChunkStatus; ++ } ++ ++ public boolean isEntityTickingReady() { ++ return this.getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING); ++ } ++ ++ public boolean isTickingReady() { ++ return this.getChunkStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING); ++ } ++ ++ public boolean isFullChunkReady() { ++ return this.getChunkStatus().isOrAfter(FullChunkStatus.FULL); ++ } ++ ++ private static FullChunkStatus getStatusForBitset(final long bitset) { ++ if ((bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2) { ++ return FullChunkStatus.ENTITY_TICKING; ++ } else if ((bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1) { ++ return FullChunkStatus.BLOCK_TICKING; ++ } else if ((bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0) { ++ return FullChunkStatus.FULL; ++ } else { ++ return FullChunkStatus.INACCESSIBLE; ++ } ++ } ++ ++ // must hold scheduling lock ++ // returns whether the pending status was changed ++ private boolean updatePendingStatus() { ++ final FullChunkStatus byTicketLevel = ChunkLevel.fullStatus(this.oldTicketLevel); // oldTicketLevel is controlled by scheduling lock ++ ++ FullChunkStatus pending = getStatusForBitset(this.fullNeighbourChunksLoadedBitset); ++ if (pending == FullChunkStatus.INACCESSIBLE && byTicketLevel.isOrAfter(FullChunkStatus.FULL) && this.currentGenStatus == ChunkStatus.FULL) { ++ // the bitset is only for chunks that have gone through the status updater ++ // but here we are ready to go to FULL ++ pending = FullChunkStatus.FULL; ++ } ++ ++ if (pending.isOrAfter(byTicketLevel)) { // pending >= byTicketLevel ++ // cannot set above ticket level ++ pending = byTicketLevel; ++ } ++ ++ if (this.pendingFullChunkStatus == pending) { ++ return false; ++ } ++ ++ this.pendingFullChunkStatus = pending; ++ ++ return true; ++ } ++ ++ private void onFullChunkLoadChange(final boolean loaded, final List<NewChunkHolder> changedFullStatus) { ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, NEIGHBOUR_RADIUS); ++ try { ++ for (int dz = -NEIGHBOUR_RADIUS; dz <= NEIGHBOUR_RADIUS; ++dz) { ++ for (int dx = -NEIGHBOUR_RADIUS; dx <= NEIGHBOUR_RADIUS; ++dx) { ++ final NewChunkHolder holder = (dx | dz) == 0 ? this : this.scheduler.chunkHolderManager.getChunkHolder(dx + this.chunkX, dz + this.chunkZ); ++ if (loaded) { ++ if (holder.setNeighbourFullLoaded(-dx, -dz)) { ++ changedFullStatus.add(holder); ++ } ++ } else { ++ if (holder != null && holder.setNeighbourFullUnloaded(-dx, -dz)) { ++ changedFullStatus.add(holder); ++ } ++ } ++ } ++ } ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } ++ ++ private void changeEntityChunkStatus(final FullChunkStatus toStatus) { ++ ((ChunkSystemServerLevel)this.world).moonrise$getEntityLookup().chunkStatusChange(this.chunkX, this.chunkZ, toStatus); ++ } ++ ++ private boolean processingFullStatus = false; ++ ++ private void updateCurrentState(final FullChunkStatus to) { ++ this.currentFullChunkStatus = to; ++ } ++ ++ // only to be called on the main thread, no locks need to be held ++ public boolean handleFullStatusChange(final List<NewChunkHolder> changedFullStatus) { ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot update full status thread off-main"); ++ ++ boolean ret = false; ++ ++ if (this.processingFullStatus) { ++ // we cannot process updates recursively, as we may be in the middle of logic to upgrade/downgrade status ++ return ret; ++ } ++ ++ this.processingFullStatus = true; ++ try { ++ for (;;) { ++ // check if we have any remaining work to do ++ ++ // we do not need to hold the scheduling lock to read pending, as changes to pending ++ // will queue a status update ++ ++ final FullChunkStatus pending = this.pendingFullChunkStatus; ++ FullChunkStatus current = this.currentFullChunkStatus; ++ ++ if (pending == current) { ++ if (pending == FullChunkStatus.INACCESSIBLE) { ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ this.checkUnload(); ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } ++ return ret; ++ } ++ ++ ret = true; ++ ++ // note: because the chunk system delays any ticket downgrade to the chunk holder manager tick, we ++ // do not need to consider cases where the ticket level may decrease during this call by asynchronous ++ // ticket changes ++ ++ // chunks cannot downgrade state while status is pending a change ++ // note: currentChunk must be LevelChunk, as current != pending which means that at least one is not ACCESSIBLE ++ final LevelChunk chunk = (LevelChunk)this.currentChunk; ++ ++ // Note: we assume that only load/unload contain plugin logic ++ // plugin logic is anything stupid enough to possibly change the chunk status while it is already ++ // being changed (i.e during load it is possible it will try to set to full ticking) ++ // in order to allow this change, we also need this plugin logic to be contained strictly after all ++ // of the chunk system load callbacks are invoked ++ if (pending.isOrAfter(current)) { ++ // state upgrade ++ if (!current.isOrAfter(FullChunkStatus.FULL) && pending.isOrAfter(FullChunkStatus.FULL)) { ++ this.updateCurrentState(FullChunkStatus.FULL); ++ ChunkSystem.onChunkPreBorder(chunk, this.vanillaChunkHolder); ++ this.scheduler.chunkHolderManager.ensureInAutosave(this); ++ this.changeEntityChunkStatus(FullChunkStatus.FULL); ++ ChunkSystem.onChunkBorder(chunk, this.vanillaChunkHolder); ++ this.onFullChunkLoadChange(true, changedFullStatus); ++ this.completeFullStatusConsumers(FullChunkStatus.FULL, chunk); ++ } ++ ++ if (!current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { ++ this.updateCurrentState(FullChunkStatus.BLOCK_TICKING); ++ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING); ++ ChunkSystem.onChunkTicking(chunk, this.vanillaChunkHolder); ++ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, chunk); ++ } ++ ++ if (!current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { ++ this.updateCurrentState(FullChunkStatus.ENTITY_TICKING); ++ this.changeEntityChunkStatus(FullChunkStatus.ENTITY_TICKING); ++ ChunkSystem.onChunkEntityTicking(chunk, this.vanillaChunkHolder); ++ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, chunk); ++ } ++ } else { ++ if (current.isOrAfter(FullChunkStatus.ENTITY_TICKING) && !pending.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { ++ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING); ++ ChunkSystem.onChunkNotEntityTicking(chunk, this.vanillaChunkHolder); ++ this.updateCurrentState(FullChunkStatus.BLOCK_TICKING); ++ } ++ ++ if (current.isOrAfter(FullChunkStatus.BLOCK_TICKING) && !pending.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { ++ this.changeEntityChunkStatus(FullChunkStatus.FULL); ++ ChunkSystem.onChunkNotTicking(chunk, this.vanillaChunkHolder); ++ this.updateCurrentState(FullChunkStatus.FULL); ++ } ++ ++ if (current.isOrAfter(FullChunkStatus.FULL) && !pending.isOrAfter(FullChunkStatus.FULL)) { ++ this.onFullChunkLoadChange(false, changedFullStatus); ++ this.changeEntityChunkStatus(FullChunkStatus.INACCESSIBLE); ++ ChunkSystem.onChunkNotBorder(chunk, this.vanillaChunkHolder); ++ ChunkSystem.onChunkPostNotBorder(chunk, this.vanillaChunkHolder); ++ this.updateCurrentState(FullChunkStatus.INACCESSIBLE); ++ } ++ } ++ } ++ } finally { ++ this.processingFullStatus = false; ++ } ++ } ++ ++ // note: must hold scheduling lock ++ // rets true if the current requested gen status is not null (effectively, whether further scheduling is not needed) ++ boolean upgradeGenTarget(final ChunkStatus toStatus) { ++ if (toStatus == null) { ++ throw new NullPointerException("toStatus cannot be null"); ++ } ++ if (this.requestedGenStatus == null && this.generationTask == null) { ++ return false; ++ } ++ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(toStatus)) { ++ this.requestedGenStatus = toStatus; ++ } ++ return true; ++ } ++ ++ public void setGenerationTarget(final ChunkStatus toStatus) { ++ this.requestedGenStatus = toStatus; ++ } ++ ++ public boolean hasGenerationTask() { ++ return this.generationTask != null; ++ } ++ ++ public ChunkStatus getCurrentGenStatus() { ++ return this.currentGenStatus; ++ } ++ ++ public ChunkStatus getRequestedGenStatus() { ++ return this.requestedGenStatus; ++ } ++ ++ private final Reference2ObjectOpenHashMap<ChunkStatus, List<Consumer<ChunkAccess>>> statusWaiters = new Reference2ObjectOpenHashMap<>(); ++ ++ void addStatusConsumer(final ChunkStatus status, final Consumer<ChunkAccess> consumer) { ++ this.statusWaiters.computeIfAbsent(status, (final ChunkStatus keyInMap) -> { ++ return new ArrayList<>(4); ++ }).add(consumer); ++ } ++ ++ private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) { ++ // need to tell future statuses to complete if cancelled ++ do { ++ this.completeStatusConsumers0(status, chunk); ++ } while (chunk == null && status != (status = ((ChunkSystemChunkStatus)status).moonrise$getNextStatus())); ++ } ++ ++ private void completeStatusConsumers0(final ChunkStatus status, final ChunkAccess chunk) { ++ final List<Consumer<ChunkAccess>> consumers; ++ consumers = this.statusWaiters.remove(status); ++ ++ if (consumers == null) { ++ return; ++ } ++ ++ // must be scheduled to main, we do not trust the callback to not do anything stupid ++ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { ++ for (final Consumer<ChunkAccess> consumer : consumers) { ++ try { ++ consumer.accept(chunk); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to process chunk status callback", thr); ++ } ++ } ++ }, PrioritisedExecutor.Priority.HIGHEST); ++ } ++ ++ private final Reference2ObjectOpenHashMap<FullChunkStatus, List<Consumer<LevelChunk>>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>(); ++ ++ void addFullStatusConsumer(final FullChunkStatus status, final Consumer<LevelChunk> consumer) { ++ this.fullStatusWaiters.computeIfAbsent(status, (final FullChunkStatus keyInMap) -> { ++ return new ArrayList<>(4); ++ }).add(consumer); ++ } ++ ++ private void completeFullStatusConsumers(FullChunkStatus status, final LevelChunk chunk) { ++ final List<Consumer<LevelChunk>> consumers; ++ consumers = this.fullStatusWaiters.remove(status); ++ ++ if (consumers == null) { ++ return; ++ } ++ ++ // must be scheduled to main, we do not trust the callback to not do anything stupid ++ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { ++ for (final Consumer<LevelChunk> consumer : consumers) { ++ try { ++ consumer.accept(chunk); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to process chunk status callback", thr); ++ } ++ } ++ }, PrioritisedExecutor.Priority.HIGHEST); ++ } ++ ++ // note: must hold scheduling lock ++ private void onChunkGenComplete(final ChunkAccess newChunk, final ChunkStatus newStatus, ++ final List<ChunkProgressionTask> scheduleList, final List<NewChunkHolder> changedLoadStatus) { ++ if (!this.neighboursBlockingGenTask.isEmpty()) { ++ throw new IllegalStateException("Cannot have neighbours blocking this gen task"); ++ } ++ if (newChunk != null || (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(newStatus))) { ++ this.completeStatusConsumers(newStatus, newChunk); ++ } ++ // done now, clear state (must be done before scheduling new tasks) ++ this.generationTask = null; ++ this.generationTaskStatus = null; ++ if (newChunk == null) { ++ // task was cancelled ++ // should be careful as this could be called while holding the schedule lock and/or inside the ++ // ticket level update ++ // while a task may be cancelled, it is possible for it to be later re-scheduled ++ // however, because generationTask is only set to null on _completion_, the scheduler leaves ++ // the rescheduling logic to us here ++ final ChunkStatus requestedGenStatus = this.requestedGenStatus; ++ this.requestedGenStatus = null; ++ if (requestedGenStatus != null) { ++ // it looks like it has been requested, so we must reschedule ++ if (!this.neighboursWaitingForUs.isEmpty()) { ++ for (final Iterator<Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus>> iterator = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry = iterator.next(); ++ ++ final NewChunkHolder chunkHolder = entry.getKey(); ++ final ChunkStatus toStatus = entry.getValue(); ++ ++ if (!requestedGenStatus.isOrAfter(toStatus)) { ++ // if we were cancelled, we are responsible for removing the waiter ++ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) { ++ throw new IllegalStateException("Corrupt state"); ++ } ++ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) { ++ chunkHolder.checkUnload(); ++ } ++ iterator.remove(); ++ continue; ++ } ++ } ++ } ++ ++ // note: only after generationTask -> null, generationTaskStatus -> null, and requestedGenStatus -> null ++ this.scheduler.schedule( ++ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList ++ ); ++ ++ // return, can't do anything further ++ return; ++ } ++ ++ if (!this.neighboursWaitingForUs.isEmpty()) { ++ for (final NewChunkHolder chunkHolder : this.neighboursWaitingForUs.keySet()) { ++ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) { ++ throw new IllegalStateException("Corrupt state"); ++ } ++ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) { ++ chunkHolder.checkUnload(); ++ } ++ } ++ this.neighboursWaitingForUs.clear(); ++ } ++ // reset priority, we have nothing left to generate to ++ this.setPriority(null); ++ this.checkUnload(); ++ return; ++ } ++ ++ this.currentChunk = newChunk; ++ this.currentGenStatus = newStatus; ++ final ChunkCompletion completion = new ChunkCompletion(newChunk, newStatus); ++ CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, newStatus.getIndex(), completion); ++ this.lastChunkCompletion = completion; ++ ++ final ChunkStatus requestedGenStatus = this.requestedGenStatus; ++ ++ List<NewChunkHolder> needsScheduling = null; ++ boolean recalculatePriority = false; ++ for (final Iterator<Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus>> iterator ++ = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry = iterator.next(); ++ final NewChunkHolder neighbour = entry.getKey(); ++ final ChunkStatus requiredStatus = entry.getValue(); ++ ++ if (!newStatus.isOrAfter(requiredStatus)) { ++ if (requestedGenStatus == null || !requestedGenStatus.isOrAfter(requiredStatus)) { ++ // if we're cancelled, still need to clear this map ++ if (!neighbour.neighboursBlockingGenTask.remove(this)) { ++ throw new IllegalStateException("Neighbour is not waiting for us?"); ++ } ++ if (neighbour.neighboursBlockingGenTask.isEmpty()) { ++ neighbour.checkUnload(); ++ } ++ ++ iterator.remove(); ++ } ++ continue; ++ } ++ ++ // doesn't matter what isCancelled is here, we need to schedule if we can ++ ++ recalculatePriority = true; ++ if (!neighbour.neighboursBlockingGenTask.remove(this)) { ++ throw new IllegalStateException("Neighbour is not waiting for us?"); ++ } ++ ++ if (neighbour.neighboursBlockingGenTask.isEmpty()) { ++ if (neighbour.requestedGenStatus != null) { ++ if (needsScheduling == null) { ++ needsScheduling = new ArrayList<>(); ++ } ++ needsScheduling.add(neighbour); ++ } else { ++ neighbour.checkUnload(); ++ } ++ } ++ ++ // remove last; access to entry will throw if removed ++ iterator.remove(); ++ } ++ ++ if (newStatus == ChunkStatus.FULL) { ++ this.lockPriority(); ++ // try to push pending to FULL ++ if (this.updatePendingStatus()) { ++ changedLoadStatus.add(this); ++ } ++ } ++ ++ if (recalculatePriority) { ++ this.recalculateNeighbourRequestedPriority(); ++ } ++ ++ if (requestedGenStatus != null && !newStatus.isOrAfter(requestedGenStatus)) { ++ this.scheduleNeighbours(needsScheduling, scheduleList); ++ ++ // we need to schedule more tasks now ++ this.scheduler.schedule( ++ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList ++ ); ++ } else { ++ // we're done now ++ if (requestedGenStatus != null) { ++ this.requestedGenStatus = null; ++ } ++ // reached final stage, so stop scheduling now ++ this.setPriority(null); ++ this.checkUnload(); ++ ++ this.scheduleNeighbours(needsScheduling, scheduleList); ++ } ++ } ++ ++ private void scheduleNeighbours(final List<NewChunkHolder> needsScheduling, final List<ChunkProgressionTask> scheduleList) { ++ if (needsScheduling != null) { ++ for (int i = 0, len = needsScheduling.size(); i < len; ++i) { ++ final NewChunkHolder neighbour = needsScheduling.get(i); ++ ++ this.scheduler.schedule( ++ neighbour.chunkX, neighbour.chunkZ, neighbour.requestedGenStatus, neighbour, scheduleList ++ ); ++ } ++ } ++ } ++ ++ public void setGenerationTask(final ChunkProgressionTask generationTask, final ChunkStatus taskStatus, ++ final List<NewChunkHolder> neighbours) { ++ if (this.generationTask != null || (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(taskStatus))) { ++ throw new IllegalStateException("Currently generating or provided task is trying to generate to a level we are already at!"); ++ } ++ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(taskStatus)) { ++ throw new IllegalStateException("Cannot schedule generation task when not requested"); ++ } ++ this.generationTask = generationTask; ++ this.generationTaskStatus = taskStatus; ++ ++ for (int i = 0, len = neighbours.size(); i < len; ++i) { ++ neighbours.get(i).addNeighbourUsingChunk(); ++ } ++ ++ this.checkUnload(); ++ ++ generationTask.onComplete((final ChunkAccess access, final Throwable thr) -> { ++ if (generationTask != this.generationTask) { ++ throw new IllegalStateException( ++ "Cannot complete generation task '" + generationTask + "' because we are waiting on '" + this.generationTask + "' instead!" ++ ); ++ } ++ if (thr != null) { ++ if (this.genTaskException != null) { ++ LOGGER.warn("Ignoring exception for " + this.toString(), thr); ++ return; ++ } ++ // don't set generation task to null, so that scheduling will not attempt to create another task and it ++ // will automatically block any further scheduling usage of this chunk as it will wait forever for a failed ++ // task to complete ++ this.genTaskException = thr; ++ this.failedGenStatus = taskStatus; ++ this.genTaskFailedThread = Thread.currentThread(); ++ ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Generation task", ChunkTaskScheduler.stringIfNull(generationTask), ++ "Task to status", ChunkTaskScheduler.stringIfNull(taskStatus) ++ ), thr); ++ return; ++ } ++ ++ final boolean scheduleTasks; ++ List<ChunkProgressionTask> tasks = ChunkHolderManager.getCurrentTicketUpdateScheduling(); ++ if (tasks == null) { ++ scheduleTasks = true; ++ tasks = new ArrayList<>(); ++ } else { ++ scheduleTasks = false; ++ // we are currently updating ticket levels, so we already hold the schedule lock ++ // this means we have to leave the ticket level update to handle the scheduling ++ } ++ final List<NewChunkHolder> changedLoadStatus = new ArrayList<>(); ++ // theoretically, we could schedule a chunk at the max radius which performs another max radius access. So we need to double the radius. ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, 2 * ChunkTaskScheduler.getMaxAccessRadius()); ++ try { ++ for (int i = 0, len = neighbours.size(); i < len; ++i) { ++ neighbours.get(i).removeNeighbourUsingChunk(); ++ } ++ this.onChunkGenComplete(access, taskStatus, tasks, changedLoadStatus); ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ this.scheduler.chunkHolderManager.addChangedStatuses(changedLoadStatus); ++ ++ if (scheduleTasks) { ++ // can't hold the lock while scheduling, so we have to build the tasks and then schedule after ++ for (int i = 0, len = tasks.size(); i < len; ++i) { ++ tasks.get(i).schedule(); ++ } ++ } ++ }); ++ } ++ ++ public PoiChunk getPoiChunk() { ++ return this.poiChunk; ++ } ++ ++ public ChunkEntitySlices getEntityChunk() { ++ return this.entityChunk; ++ } ++ ++ public long lastAutoSave; ++ ++ public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {} ++ ++ public SaveStat save(final boolean shutdown) { ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main"); ++ ++ ChunkAccess chunk = this.getCurrentChunk(); ++ PoiChunk poi = this.getPoiChunk(); ++ ChunkEntitySlices entities = this.getEntityChunk(); ++ boolean executedUnloadTask = false; ++ ++ if (shutdown) { ++ // make sure that the async unloads complete ++ if (this.unloadState != null) { ++ // must have errored during unload ++ chunk = this.unloadState.chunk(); ++ poi = this.unloadState.poiChunk(); ++ entities = this.unloadState.entityChunk(); ++ } ++ final UnloadTask chunkUnloadTask = this.chunkDataUnload; ++ final DelayedPrioritisedTask chunkDataUnloadTask = chunkUnloadTask == null ? null : chunkUnloadTask.task(); ++ if (chunkDataUnloadTask != null) { ++ final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnloadTask.getTask(); ++ if (unloadTask != null) { ++ executedUnloadTask = unloadTask.execute(); ++ } ++ } ++ } ++ ++ final boolean forceNoSaveChunk = ChunkSystemFeatures.forceNoSave(chunk); ++ ++ // can only synchronously save worldgen chunks during shutdown ++ boolean canSaveChunk = !forceNoSaveChunk && (chunk != null && ((shutdown || chunk instanceof LevelChunk) && chunk.isUnsaved())); ++ boolean canSavePOI = !forceNoSaveChunk && (poi != null && poi.isDirty()); ++ boolean canSaveEntities = entities != null; ++ ++ if (canSaveChunk) { ++ canSaveChunk = this.saveChunk(chunk, false); ++ } ++ if (canSavePOI) { ++ canSavePOI = this.savePOI(poi, false); ++ } ++ if (canSaveEntities) { ++ // on shutdown, we need to force transient entity chunks to save ++ canSaveEntities = this.saveEntities(entities, shutdown); ++ if (shutdown) { ++ this.lastEntityUnload = null; ++ } ++ } ++ ++ return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ? new SaveStat(executedUnloadTask || canSaveChunk, canSaveEntities, canSavePOI): null; ++ } ++ ++ static final class AsyncChunkSerializeTask implements Runnable { ++ ++ private final ServerLevel world; ++ private final ChunkAccess chunk; ++ private final AsyncChunkSaveData asyncSaveData; ++ private final NewChunkHolder toComplete; ++ ++ public AsyncChunkSerializeTask(final ServerLevel world, final ChunkAccess chunk, final AsyncChunkSaveData asyncSaveData, ++ final NewChunkHolder toComplete) { ++ this.world = world; ++ this.chunk = chunk; ++ this.asyncSaveData = asyncSaveData; ++ this.toComplete = toComplete; ++ } ++ ++ @Override ++ public void run() { ++ final CompoundTag toSerialize; ++ try { ++ toSerialize = ChunkSystemFeatures.saveChunkAsync(this.world, this.chunk, this.asyncSaveData); ++ } catch (final Throwable throwable) { ++ LOGGER.error("Failed to asynchronously save chunk " + this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(this.world) + "', falling back to synchronous save", throwable); ++ final ChunkPos pos = this.chunk.getPos(); ++ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkTask(pos.x, pos.z, () -> { ++ final CompoundTag synchronousSave; ++ try { ++ synchronousSave = ChunkSystemFeatures.saveChunkAsync(AsyncChunkSerializeTask.this.world, AsyncChunkSerializeTask.this.chunk, AsyncChunkSerializeTask.this.asyncSaveData); ++ } catch (final Throwable throwable2) { ++ LOGGER.error("Failed to synchronously save chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(AsyncChunkSerializeTask.this.world) + "', chunk data will be lost", throwable2); ++ AsyncChunkSerializeTask.this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null); ++ return; ++ } ++ ++ AsyncChunkSerializeTask.this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, synchronousSave); ++ LOGGER.info("Successfully serialized chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(AsyncChunkSerializeTask.this.world) + "' synchronously"); ++ ++ }, PrioritisedExecutor.Priority.HIGHEST); ++ return; ++ } ++ this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, toSerialize); ++ } ++ ++ @Override ++ public String toString() { ++ return "AsyncChunkSerializeTask{" + ++ "chunk={pos=" + this.chunk.getPos() + ",world=\"" + WorldUtil.getWorldName(this.world) + "\"}" + ++ "}"; ++ } ++ } ++ ++ private boolean saveChunk(final ChunkAccess chunk, final boolean unloading) { ++ if (!chunk.isUnsaved()) { ++ if (unloading) { ++ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null); ++ } ++ return false; ++ } ++ boolean completing = false; ++ boolean failedAsyncPrepare = false; ++ try { ++ if (unloading && ChunkSystemFeatures.supportsAsyncChunkSave()) { ++ try { ++ final AsyncChunkSaveData asyncSaveData = ChunkSystemFeatures.getAsyncSaveData(this.world, chunk); ++ ++ final PrioritisedExecutor.PrioritisedTask task = this.scheduler.loadExecutor.createTask(new AsyncChunkSerializeTask(this.world, chunk, asyncSaveData, this)); ++ ++ this.chunkDataUnload.task().setTask(task); ++ ++ chunk.setUnsaved(false); ++ ++ task.queue(); ++ ++ return true; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to prepare async chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', falling back to synchronous save", thr); ++ failedAsyncPrepare = true; ++ // fall through to synchronous save ++ } ++ } ++ ++ final CompoundTag save = ChunkSerializer.write(this.world, chunk); ++ ++ if (unloading) { ++ completing = true; ++ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, save); ++ if (failedAsyncPrepare) { ++ LOGGER.info("Successfully serialized chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "' synchronously"); ++ } ++ } else { ++ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.CHUNK_DATA); ++ } ++ chunk.setUnsaved(false); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'"); ++ if (unloading && !completing) { ++ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null); ++ } ++ } ++ ++ return true; ++ } ++ ++ private boolean lastEntitySaveNull; ++ private CompoundTag lastEntityUnload; ++ private boolean saveEntities(final ChunkEntitySlices entities, final boolean unloading) { ++ try { ++ CompoundTag mergeFrom = null; ++ if (entities.isTransient()) { ++ if (!unloading) { ++ // if we're a transient chunk, we cannot save until unloading because otherwise a double save will ++ // result in double adding the entities ++ return false; ++ } ++ try { ++ mergeFrom = RegionFileIOThread.loadData(this.world, this.chunkX, this.chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, PrioritisedExecutor.Priority.BLOCKING); ++ } catch (final Exception ex) { ++ LOGGER.error("Cannot merge transient entities for chunk (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', data on disk will be replaced", ex); ++ } ++ } ++ ++ final CompoundTag save = entities.save(); ++ if (mergeFrom != null) { ++ if (save == null) { ++ // don't override the data on disk with nothing ++ return false; ++ } else { ++ ChunkEntitySlices.copyEntities(mergeFrom, save); ++ } ++ } ++ if (save == null && this.lastEntitySaveNull) { ++ return false; ++ } ++ ++ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.ENTITY_DATA); ++ this.lastEntitySaveNull = save == null; ++ if (unloading) { ++ this.lastEntityUnload = save; ++ } ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'"); ++ } ++ ++ return true; ++ } ++ ++ private boolean lastPoiSaveNull; ++ private boolean savePOI(final PoiChunk poi, final boolean unloading) { ++ try { ++ final CompoundTag save = poi.save(); ++ poi.setDirty(false); ++ if (save == null && this.lastPoiSaveNull) { ++ if (unloading) { ++ this.poiDataUnload.completable().complete(null); ++ } ++ return false; ++ } ++ ++ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.POI_DATA); ++ this.lastPoiSaveNull = save == null; ++ if (unloading) { ++ this.poiDataUnload.completable().complete(save); ++ } ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "'"); ++ } ++ ++ return true; ++ } ++ ++ @Override ++ public String toString() { ++ final ChunkCompletion lastCompletion = this.lastChunkCompletion; ++ final ChunkEntitySlices entityChunk = this.entityChunk; ++ final FullChunkStatus pendingFullStatus = this.pendingFullChunkStatus; ++ final FullChunkStatus currentFullStatus = this.currentFullChunkStatus; ++ return "NewChunkHolder{" + ++ "world=" + WorldUtil.getWorldName(this.world) + ++ ", chunkX=" + this.chunkX + ++ ", chunkZ=" + this.chunkZ + ++ ", entityChunkFromDisk=" + (entityChunk != null && !entityChunk.isTransient()) + ++ ", lastChunkCompletion={chunk_class=" + (lastCompletion == null || lastCompletion.chunk() == null ? "null" : lastCompletion.chunk().getClass().getName()) + ",status=" + (lastCompletion == null ? "null" : lastCompletion.genStatus()) + "}" + ++ ", currentGenStatus=" + this.currentGenStatus + ++ ", requestedGenStatus=" + this.requestedGenStatus + ++ ", generationTask=" + this.generationTask + ++ ", generationTaskStatus=" + this.generationTaskStatus + ++ ", priority=" + this.priority + ++ ", priorityLocked=" + this.priorityLocked + ++ ", neighbourRequestedPriority=" + this.neighbourRequestedPriority + ++ ", effective_priority=" + this.getEffectivePriority(null) + ++ ", oldTicketLevel=" + this.oldTicketLevel + ++ ", currentTicketLevel=" + this.currentTicketLevel + ++ ", totalNeighboursUsingThisChunk=" + this.totalNeighboursUsingThisChunk + ++ ", fullNeighbourChunksLoadedBitset=" + this.fullNeighbourChunksLoadedBitset + ++ ", currentChunkStatus=" + currentFullStatus + ++ ", pendingChunkStatus=" + pendingFullStatus + ++ ", is_unload_safe=" + this.isSafeToUnload() + ++ ", killed=" + this.unloaded + ++ '}'; ++ } ++ ++ private static JsonElement serializeStacktraceElement(final StackTraceElement element) { ++ return element == null ? JsonNull.INSTANCE : new JsonPrimitive(element.toString()); ++ } ++ ++ private static JsonObject serializeCompletable(final Completable<?> completable) { ++ final JsonObject ret = new JsonObject(); ++ ++ if (completable == null) { ++ return ret; ++ } ++ ++ ret.addProperty("valid", Boolean.TRUE); ++ ++ final boolean isCompleted = completable.isCompleted(); ++ ret.addProperty("completed", Boolean.valueOf(isCompleted)); ++ ++ if (isCompleted) { ++ final Throwable throwable = completable.getThrowable(); ++ if (throwable != null) { ++ final JsonArray throwableJson = new JsonArray(); ++ ret.add("throwable", throwableJson); ++ ++ for (final StackTraceElement element : throwable.getStackTrace()) { ++ throwableJson.add(serializeStacktraceElement(element)); ++ } ++ } else { ++ final Object result = completable.getResult(); ++ ret.add("result_class", result == null ? JsonNull.INSTANCE : new JsonPrimitive(result.getClass().getName())); ++ } ++ } ++ ++ return ret; ++ } ++ ++ // (probably) holds ticket and scheduling lock ++ public JsonObject getDebugJson() { ++ final JsonObject ret = new JsonObject(); ++ ++ final ChunkCompletion lastCompletion = this.lastChunkCompletion; ++ final ChunkEntitySlices slices = this.entityChunk; ++ final PoiChunk poiChunk = this.poiChunk; ++ ++ ret.addProperty("chunkX", Integer.valueOf(this.chunkX)); ++ ret.addProperty("chunkZ", Integer.valueOf(this.chunkZ)); ++ ret.addProperty("entity_chunk", slices == null ? "null" : "transient=" + slices.isTransient()); ++ ret.addProperty("poi_chunk", "null=" + (poiChunk == null)); ++ ret.addProperty("completed_chunk_class", lastCompletion == null ? "null" : lastCompletion.chunk().getClass().getName()); ++ ret.addProperty("completed_gen_status", lastCompletion == null ? "null" : lastCompletion.genStatus().toString()); ++ ret.addProperty("priority", Objects.toString(this.priority)); ++ ret.addProperty("neighbour_requested_priority", Objects.toString(this.neighbourRequestedPriority)); ++ ret.addProperty("generation_task", Objects.toString(this.generationTask)); ++ ret.addProperty("is_safe_unload", Objects.toString(this.isSafeToUnload())); ++ ret.addProperty("old_ticket_level", Integer.valueOf(this.oldTicketLevel)); ++ ret.addProperty("current_ticket_level", Integer.valueOf(this.currentTicketLevel)); ++ ret.addProperty("neighbours_using_chunk", Integer.valueOf(this.totalNeighboursUsingThisChunk)); ++ ++ final JsonObject neighbourWaitState = new JsonObject(); ++ ret.add("neighbour_state", neighbourWaitState); ++ ++ final JsonArray blockingGenNeighbours = new JsonArray(); ++ neighbourWaitState.add("blocking_gen_task", blockingGenNeighbours); ++ for (final NewChunkHolder blockingGenNeighbour : this.neighboursBlockingGenTask) { ++ final JsonObject neighbour = new JsonObject(); ++ blockingGenNeighbours.add(neighbour); ++ ++ neighbour.addProperty("chunkX", Integer.valueOf(blockingGenNeighbour.chunkX)); ++ neighbour.addProperty("chunkZ", Integer.valueOf(blockingGenNeighbour.chunkZ)); ++ } ++ ++ final JsonArray neighboursWaitingForUs = new JsonArray(); ++ neighbourWaitState.add("neighbours_waiting_on_us", neighboursWaitingForUs); ++ for (final Reference2ObjectMap.Entry<NewChunkHolder, ChunkStatus> entry : this.neighboursWaitingForUs.reference2ObjectEntrySet()) { ++ final NewChunkHolder holder = entry.getKey(); ++ final ChunkStatus status = entry.getValue(); ++ ++ final JsonObject neighbour = new JsonObject(); ++ neighboursWaitingForUs.add(neighbour); ++ ++ ++ neighbour.addProperty("chunkX", Integer.valueOf(holder.chunkX)); ++ neighbour.addProperty("chunkZ", Integer.valueOf(holder.chunkZ)); ++ neighbour.addProperty("waiting_for", Objects.toString(status)); ++ } ++ ++ ret.addProperty("pending_chunk_full_status", Objects.toString(this.pendingFullChunkStatus)); ++ ret.addProperty("current_chunk_full_status", Objects.toString(this.currentFullChunkStatus)); ++ ret.addProperty("generation_task", Objects.toString(this.generationTask)); ++ ret.addProperty("requested_generation", Objects.toString(this.requestedGenStatus)); ++ ret.addProperty("has_entity_load_task", Boolean.valueOf(this.entityDataLoadTask != null)); ++ ret.addProperty("has_poi_load_task", Boolean.valueOf(this.poiDataLoadTask != null)); ++ ++ final UnloadTask entityDataUnload = this.entityDataUnload; ++ final UnloadTask poiDataUnload = this.poiDataUnload; ++ final UnloadTask chunkDataUnload = this.chunkDataUnload; ++ ++ ret.add("entity_unload_completable", serializeCompletable(entityDataUnload == null ? null : entityDataUnload.completable())); ++ ret.add("poi_unload_completable", serializeCompletable(poiDataUnload == null ? null : poiDataUnload.completable())); ++ ret.add("chunk_unload_completable", serializeCompletable(chunkDataUnload == null ? null : chunkDataUnload.completable())); ++ ++ final DelayedPrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task(); ++ if (unloadTask == null) { ++ ret.addProperty("unload_task_priority", "null"); ++ ret.addProperty("unload_task_priority_raw", "null"); ++ } else { ++ ret.addProperty("unload_task_priority", Objects.toString(unloadTask.getPriority())); ++ ret.addProperty("unload_task_priority_raw", Integer.valueOf(unloadTask.getPriorityInternal())); ++ } ++ ++ ret.addProperty("killed", Boolean.valueOf(this.unloaded)); ++ ++ return ret; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..261e09454f49d04eb159c984ec695d7c7aa6a3a8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java +@@ -0,0 +1,215 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import java.lang.invoke.VarHandle; ++ ++public abstract class PriorityHolder { ++ ++ protected volatile int priority; ++ protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(PriorityHolder.class, "priority", int.class); ++ ++ protected static final int PRIORITY_SCHEDULED = Integer.MIN_VALUE >>> 0; ++ protected static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 1; ++ ++ protected final int getPriorityVolatile() { ++ return (int)PRIORITY_HANDLE.getVolatile((PriorityHolder)this); ++ } ++ ++ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) { ++ return (int)PRIORITY_HANDLE.compareAndExchange((PriorityHolder)this, (int)expect, (int)update); ++ } ++ ++ protected final int getAndOrPriorityVolatile(final int val) { ++ return (int)PRIORITY_HANDLE.getAndBitwiseOr((PriorityHolder)this, (int)val); ++ } ++ ++ protected final void setPriorityPlain(final int val) { ++ PRIORITY_HANDLE.set((PriorityHolder)this, (int)val); ++ } ++ ++ protected PriorityHolder(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.setPriorityPlain(priority.priority); ++ } ++ ++ // used only for debug json ++ public boolean isScheduled() { ++ return (this.getPriorityVolatile() & PRIORITY_SCHEDULED) != 0; ++ } ++ ++ // returns false if cancelled ++ public boolean markExecuting() { ++ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0; ++ } ++ ++ public boolean isMarkedExecuted() { ++ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0; ++ } ++ ++ public void cancel() { ++ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) { ++ // cancelled already ++ return; ++ } ++ this.cancelScheduled(); ++ } ++ ++ public void schedule() { ++ int priority = this.getPriorityVolatile(); ++ ++ if ((priority & PRIORITY_SCHEDULED) != 0) { ++ throw new IllegalStateException("schedule() called twice"); ++ } ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled ++ return; ++ } ++ ++ this.scheduleTask(PrioritisedExecutor.Priority.getPriority(priority)); ++ ++ int failures = 0; ++ for (;;) { ++ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SCHEDULED))) { ++ return; ++ } ++ ++ if ((priority & PRIORITY_SCHEDULED) != 0) { ++ throw new IllegalStateException("schedule() called twice"); ++ } ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ return; ++ } ++ ++ this.setPriorityScheduled(PrioritisedExecutor.Priority.getPriority(priority)); ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public final PrioritisedExecutor.Priority getPriority() { ++ final int ret = this.getPriorityVolatile(); ++ if ((ret & PRIORITY_EXECUTED) != 0) { ++ return PrioritisedExecutor.Priority.COMPLETING; ++ } ++ if ((ret & PRIORITY_SCHEDULED) != 0) { ++ return this.getScheduledPriority(); ++ } ++ return PrioritisedExecutor.Priority.getPriority(ret); ++ } ++ ++ public final void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ return; ++ } ++ ++ if ((curr & PRIORITY_SCHEDULED) != 0) { ++ this.lowerPriorityScheduled(priority); ++ return; ++ } ++ ++ if (!priority.isLowerPriority(curr)) { ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public final void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ return; ++ } ++ ++ if ((curr & PRIORITY_SCHEDULED) != 0) { ++ this.setPriorityScheduled(priority); ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public final void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ return; ++ } ++ ++ if ((curr & PRIORITY_SCHEDULED) != 0) { ++ this.raisePriorityScheduled(priority); ++ return; ++ } ++ ++ if (!priority.isHigherPriority(curr)) { ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ protected abstract void cancelScheduled(); ++ ++ protected abstract PrioritisedExecutor.Priority getScheduledPriority(); ++ ++ protected abstract void scheduleTask(final PrioritisedExecutor.Priority priority); ++ ++ protected abstract void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority); ++ ++ protected abstract void setPriorityScheduled(final PrioritisedExecutor.Priority priority); ++ ++ protected abstract void raisePriorityScheduled(final PrioritisedExecutor.Priority priority); ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java +new file mode 100644 +index 0000000000000000000000000000000000000000..310a8f80debadd64c2d962ebf83b7d0505ce6e42 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java +@@ -0,0 +1,1457 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; ++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkProgressionTask; ++import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.shorts.Short2ByteLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.shorts.Short2ByteMap; ++import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.List; ++import java.util.concurrent.locks.LockSupport; ++ ++public abstract class ThreadedTicketLevelPropagator { ++ ++ // sections are 64 in length ++ public static final int SECTION_SHIFT = 6; ++ public static final int SECTION_SIZE = 1 << SECTION_SHIFT; ++ private static final int LEVEL_BITS = SECTION_SHIFT; ++ private static final int LEVEL_COUNT = 1 << LEVEL_BITS; ++ private static final int MIN_SOURCE_LEVEL = 1; ++ // we limit the max source to 62 because the de-propagation code _must_ attempt to de-propagate ++ // a 1 level to 0; and if a source was 63 then it may cross more than 2 sections in de-propagation ++ private static final int MAX_SOURCE_LEVEL = 62; ++ ++ private static int getMaxSchedulingRadius() { ++ return 2 * ChunkTaskScheduler.getMaxAccessRadius(); ++ } ++ ++ private final UpdateQueue updateQueue; ++ private final ConcurrentLong2ReferenceChainedHashTable<Section> sections; ++ ++ public ThreadedTicketLevelPropagator() { ++ this.updateQueue = new UpdateQueue(); ++ this.sections = new ConcurrentLong2ReferenceChainedHashTable<>(); ++ } ++ ++ // must hold ticket lock for: ++ // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1)) ++ public void setSource(final int posX, final int posZ, final int to) { ++ if (to < 1 || to > MAX_SOURCE_LEVEL) { ++ throw new IllegalArgumentException("Source: " + to); ++ } ++ ++ final int sectionX = posX >> SECTION_SHIFT; ++ final int sectionZ = posZ >> SECTION_SHIFT; ++ ++ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ Section section = this.sections.get(coordinate); ++ if (section == null) { ++ if (null != this.sections.putIfAbsent(coordinate, section = new Section(sectionX, sectionZ))) { ++ throw new IllegalStateException("Race condition while creating new section"); ++ } ++ } ++ ++ final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); ++ final short sLocalIdx = (short)localIdx; ++ ++ final short sourceAndLevel = section.levels[localIdx]; ++ final int currentSource = (sourceAndLevel >>> 8) & 0xFF; ++ ++ if (currentSource == to) { ++ // nothing to do ++ // make sure to kill the current update, if any ++ section.queuedSources.replace(sLocalIdx, (byte)to); ++ return; ++ } ++ ++ if (section.queuedSources.put(sLocalIdx, (byte)to) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) { ++ this.queueSectionUpdate(section); ++ } ++ } ++ ++ // must hold ticket lock for: ++ // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1)) ++ public void removeSource(final int posX, final int posZ) { ++ final int sectionX = posX >> SECTION_SHIFT; ++ final int sectionZ = posZ >> SECTION_SHIFT; ++ ++ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ final Section section = this.sections.get(coordinate); ++ ++ if (section == null) { ++ return; ++ } ++ ++ final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); ++ final short sLocalIdx = (short)localIdx; ++ ++ final int currentSource = (section.levels[localIdx] >>> 8) & 0xFF; ++ ++ if (currentSource == 0) { ++ // we use replace here so that we do not possibly multi-queue a section for an update ++ section.queuedSources.replace(sLocalIdx, (byte)0); ++ return; ++ } ++ ++ if (section.queuedSources.put(sLocalIdx, (byte)0) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) { ++ this.queueSectionUpdate(section); ++ } ++ } ++ ++ private void queueSectionUpdate(final Section section) { ++ this.updateQueue.append(new UpdateQueue.UpdateQueueNode(section, null)); ++ } ++ ++ public boolean hasPendingUpdates() { ++ return !this.updateQueue.isEmpty(); ++ } ++ ++ // holds ticket lock for every chunk section represented by any position in the key set ++ // updates is modifiable and passed to processSchedulingUpdates after this call ++ protected abstract void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates); ++ ++ // holds ticket lock for every chunk section represented by any position in the key set ++ // holds scheduling lock in max access radius for every position held by the ticket lock ++ // updates is cleared after this call ++ protected abstract void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List<ChunkProgressionTask> scheduledTasks, ++ final List<NewChunkHolder> changedFullStatus); ++ ++ // must hold ticket lock for every position in the sections in one radius around sectionX,sectionZ ++ public boolean performUpdate(final int sectionX, final int sectionZ, final ReentrantAreaLock schedulingLock, ++ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) { ++ if (!this.hasPendingUpdates()) { ++ return false; ++ } ++ ++ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ final Section section = this.sections.get(coordinate); ++ ++ if (section == null || section.queuedSources.isEmpty()) { ++ // no section or no updates ++ return false; ++ } ++ ++ final Propagator propagator = Propagator.acquirePropagator(); ++ final boolean ret = this.performUpdate(section, null, propagator, ++ null, schedulingLock, scheduledTasks, changedFullStatus ++ ); ++ Propagator.returnPropagator(propagator); ++ return ret; ++ } ++ ++ private boolean performUpdate(final Section section, final UpdateQueue.UpdateQueueNode node, final Propagator propagator, ++ final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock, ++ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) { ++ final int sectionX = section.sectionX; ++ final int sectionZ = section.sectionZ; ++ ++ final int rad1MinX = (sectionX - 1) << SECTION_SHIFT; ++ final int rad1MinZ = (sectionZ - 1) << SECTION_SHIFT; ++ final int rad1MaxX = ((sectionX + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1); ++ final int rad1MaxZ = ((sectionZ + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1); ++ ++ // set up encode offset first as we need to queue level changes _before_ ++ propagator.setupEncodeOffset(sectionX, sectionZ); ++ ++ final int coordinateOffset = propagator.coordinateOffset; ++ ++ final ReentrantAreaLock.Node ticketNode = ticketLock == null ? null : ticketLock.lock(rad1MinX, rad1MinZ, rad1MaxX, rad1MaxZ); ++ final boolean ret; ++ try { ++ // first, check if this update was stolen ++ if (section != this.sections.get(CoordinateUtils.getChunkKey(sectionX, sectionZ))) { ++ // occurs when a stolen update deletes this section ++ // it is possible that another update is scheduled, but that one will have the correct section ++ if (node != null) { ++ this.updateQueue.remove(node); ++ } ++ return false; ++ } ++ ++ final int oldSourceSize = section.sources.size(); ++ ++ // process pending sources ++ for (final Iterator<Short2ByteMap.Entry> iterator = section.queuedSources.short2ByteEntrySet().fastIterator(); iterator.hasNext();) { ++ final Short2ByteMap.Entry entry = iterator.next(); ++ final int pos = (int)entry.getShortKey(); ++ final int posX = (pos & (SECTION_SIZE - 1)) | (sectionX << SECTION_SHIFT); ++ final int posZ = ((pos >> SECTION_SHIFT) & (SECTION_SIZE - 1)) | (sectionZ << SECTION_SHIFT); ++ final int newSource = (int)entry.getByteValue(); ++ ++ final short currentEncoded = section.levels[pos]; ++ final int currLevel = currentEncoded & 0xFF; ++ final int prevSource = (currentEncoded >>> 8) & 0xFF; ++ ++ if (prevSource == newSource) { ++ // nothing changed ++ continue; ++ } ++ ++ if ((prevSource < currLevel && newSource <= currLevel) || newSource == currLevel) { ++ // just update the source, don't need to propagate change ++ section.levels[pos] = (short)(currLevel | (newSource << 8)); ++ // level is unchanged, don't add to changed positions ++ } else { ++ // set current level and current source to new source ++ section.levels[pos] = (short)(newSource | (newSource << 8)); ++ // must add to updated positions in case this is final ++ propagator.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)newSource); ++ if (newSource != 0) { ++ // queue increase with new source level ++ propagator.appendToIncreaseQueue( ++ ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) | ++ ((newSource & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) | ++ (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS)) ++ ); ++ } ++ // queue decrease with previous level ++ if (newSource < currLevel) { ++ propagator.appendToDecreaseQueue( ++ ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) | ++ ((currLevel & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) | ++ (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS)) ++ ); ++ } ++ } ++ ++ if (newSource == 0) { ++ // prevSource != newSource, so we are removing this source ++ section.sources.remove((short)pos); ++ } else if (prevSource == 0) { ++ // prevSource != newSource, so we are adding this source ++ section.sources.add((short)pos); ++ } ++ } ++ ++ section.queuedSources.clear(); ++ ++ final int newSourceSize = section.sources.size(); ++ ++ if (oldSourceSize == 0 && newSourceSize != 0) { ++ // need to make sure the sections in 1 radius are initialised ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ final int offX = dx + sectionX; ++ final int offZ = dz + sectionZ; ++ final long coordinate = CoordinateUtils.getChunkKey(offX, offZ); ++ final Section neighbour = this.sections.computeIfAbsent(coordinate, (final long keyInMap) -> { ++ return new Section(CoordinateUtils.getChunkX(keyInMap), CoordinateUtils.getChunkZ(keyInMap)); ++ }); ++ ++ // increase ref count ++ ++neighbour.oneRadNeighboursWithSources; ++ if (neighbour.oneRadNeighboursWithSources <= 0 || neighbour.oneRadNeighboursWithSources > 8) { ++ throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources)); ++ } ++ } ++ } ++ } ++ ++ if (propagator.hasUpdates()) { ++ propagator.setupCaches(this, sectionX, sectionZ, 1); ++ propagator.performDecrease(); ++ // don't need try-finally, as any exception will cause the propagator to not be returned ++ propagator.destroyCaches(); ++ } ++ ++ if (newSourceSize == 0) { ++ final boolean decrementRef = oldSourceSize != 0; ++ // check for section de-init ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ final int offX = dx + sectionX; ++ final int offZ = dz + sectionZ; ++ final long coordinate = CoordinateUtils.getChunkKey(offX, offZ); ++ final Section neighbour = this.sections.get(coordinate); ++ ++ if (neighbour == null) { ++ if (oldSourceSize == 0 && (dx | dz) != 0) { ++ // since we don't have sources, this section is allowed to be null ++ continue; ++ } ++ throw new IllegalStateException("??"); ++ } ++ ++ if (decrementRef && (dx | dz) != 0) { ++ // decrease ref count, but only for neighbours ++ --neighbour.oneRadNeighboursWithSources; ++ } ++ ++ // we need to check the current section for de-init as well ++ if (neighbour.oneRadNeighboursWithSources == 0) { ++ if (neighbour.queuedSources.isEmpty() && neighbour.sources.isEmpty()) { ++ // need to de-init ++ this.sections.remove(coordinate); ++ } // else: neighbour is queued for an update, and it will de-init itself ++ } else if (neighbour.oneRadNeighboursWithSources < 0 || neighbour.oneRadNeighboursWithSources > 8) { ++ throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources)); ++ } ++ } ++ } ++ } ++ ++ ++ ret = !propagator.updatedPositions.isEmpty(); ++ ++ if (ret) { ++ this.processLevelUpdates(propagator.updatedPositions); ++ ++ if (!propagator.updatedPositions.isEmpty()) { ++ // now we can actually update the ticket levels in the chunk holders ++ final int maxScheduleRadius = getMaxSchedulingRadius(); ++ ++ // allow the chunkholders to process ticket level updates without needing to acquire the schedule lock every time ++ final ReentrantAreaLock.Node schedulingNode = schedulingLock.lock( ++ rad1MinX - maxScheduleRadius, rad1MinZ - maxScheduleRadius, ++ rad1MaxX + maxScheduleRadius, rad1MaxZ + maxScheduleRadius ++ ); ++ try { ++ this.processSchedulingUpdates(propagator.updatedPositions, scheduledTasks, changedFullStatus); ++ } finally { ++ schedulingLock.unlock(schedulingNode); ++ } ++ } ++ ++ propagator.updatedPositions.clear(); ++ } ++ } finally { ++ if (ticketLock != null) { ++ ticketLock.unlock(ticketNode); ++ } ++ } ++ ++ // finished ++ if (node != null) { ++ this.updateQueue.remove(node); ++ } ++ ++ return ret; ++ } ++ ++ public boolean performUpdates(final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock, ++ final List<ChunkProgressionTask> scheduledTasks, final List<NewChunkHolder> changedFullStatus) { ++ if (this.updateQueue.isEmpty()) { ++ return false; ++ } ++ ++ final long maxOrder = this.updateQueue.getLastOrder(); ++ ++ boolean updated = false; ++ Propagator propagator = null; ++ ++ for (;;) { ++ final UpdateQueue.UpdateQueueNode toUpdate = this.updateQueue.acquireNextOrWait(maxOrder); ++ if (toUpdate == null) { ++ if (!this.updateQueue.hasRemainingUpdates(maxOrder)) { ++ if (propagator != null) { ++ Propagator.returnPropagator(propagator); ++ } ++ return updated; ++ } ++ ++ continue; ++ } ++ ++ if (propagator == null) { ++ propagator = Propagator.acquirePropagator(); ++ } ++ ++ updated |= this.performUpdate(toUpdate.section, toUpdate, propagator, ticketLock, schedulingLock, scheduledTasks, changedFullStatus); ++ } ++ } ++ ++ // Similar implementation of concurrent FIFO queue (See MTQ in ConcurrentUtil) which has an additional node pointer ++ // for the last update node being handled ++ private static final class UpdateQueue { ++ ++ private volatile UpdateQueueNode head; ++ private volatile UpdateQueueNode tail; ++ ++ private static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "head", UpdateQueueNode.class); ++ private static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "tail", UpdateQueueNode.class); ++ ++ /* head */ ++ ++ private final void setHeadPlain(final UpdateQueueNode newHead) { ++ HEAD_HANDLE.set(this, newHead); ++ } ++ ++ private final void setHeadOpaque(final UpdateQueueNode newHead) { ++ HEAD_HANDLE.setOpaque(this, newHead); ++ } ++ ++ private final UpdateQueueNode getHeadPlain() { ++ return (UpdateQueueNode)HEAD_HANDLE.get(this); ++ } ++ ++ private final UpdateQueueNode getHeadOpaque() { ++ return (UpdateQueueNode)HEAD_HANDLE.getOpaque(this); ++ } ++ ++ private final UpdateQueueNode getHeadAcquire() { ++ return (UpdateQueueNode)HEAD_HANDLE.getAcquire(this); ++ } ++ ++ /* tail */ ++ ++ private final void setTailPlain(final UpdateQueueNode newTail) { ++ TAIL_HANDLE.set(this, newTail); ++ } ++ ++ private final void setTailOpaque(final UpdateQueueNode newTail) { ++ TAIL_HANDLE.setOpaque(this, newTail); ++ } ++ ++ private final UpdateQueueNode getTailPlain() { ++ return (UpdateQueueNode)TAIL_HANDLE.get(this); ++ } ++ ++ private final UpdateQueueNode getTailOpaque() { ++ return (UpdateQueueNode)TAIL_HANDLE.getOpaque(this); ++ } ++ ++ public UpdateQueue() { ++ final UpdateQueueNode dummy = new UpdateQueueNode(null, null); ++ dummy.order = -1L; ++ dummy.preventAdds(); ++ ++ this.setHeadPlain(dummy); ++ this.setTailPlain(dummy); ++ } ++ ++ public boolean isEmpty() { ++ return this.peek() == null; ++ } ++ ++ public boolean hasRemainingUpdates(final long maxUpdate) { ++ final UpdateQueueNode node = this.peek(); ++ return node != null && node.order <= maxUpdate; ++ } ++ ++ public long getLastOrder() { ++ for (UpdateQueueNode tail = this.getTailOpaque(), curr = tail;;) { ++ final UpdateQueueNode next = curr.getNextVolatile(); ++ if (next == null) { ++ // try to update stale tail ++ if (this.getTailOpaque() == tail && curr != tail) { ++ this.setTailOpaque(curr); ++ } ++ return curr.order; ++ } ++ curr = next; ++ } ++ } ++ ++ private static void await(final UpdateQueueNode node) { ++ final Thread currThread = Thread.currentThread(); ++ // we do not use add-blocking because we use the nullability of the section to block ++ // remove() does not begin to poll from the wait queue until the section is null'd, ++ // and so provided we check the nullability before parking there is no ordering of these operations ++ // such that remove() finishes polling from the wait queue while section is not null ++ node.add(currThread); ++ ++ // wait until completed ++ while (node.getSectionVolatile() != null) { ++ LockSupport.park(); ++ } ++ } ++ ++ public UpdateQueueNode acquireNextOrWait(final long maxOrder) { ++ final List<UpdateQueueNode> blocking = new ArrayList<>(); ++ ++ node_search: ++ for (UpdateQueueNode curr = this.peek(); curr != null && curr.order <= maxOrder; curr = curr.getNextVolatile()) { ++ if (curr.getSectionVolatile() == null) { ++ continue; ++ } ++ ++ if (curr.getUpdatingVolatile()) { ++ blocking.add(curr); ++ continue; ++ } ++ ++ for (int i = 0, len = blocking.size(); i < len; ++i) { ++ final UpdateQueueNode node = blocking.get(i); ++ ++ if (node.intersects(curr)) { ++ continue node_search; ++ } ++ } ++ ++ if (curr.getAndSetUpdatingVolatile(true)) { ++ blocking.add(curr); ++ continue; ++ } ++ ++ return curr; ++ } ++ ++ if (!blocking.isEmpty()) { ++ await(blocking.get(0)); ++ } ++ ++ return null; ++ } ++ ++ public UpdateQueueNode peek() { ++ for (UpdateQueueNode head = this.getHeadOpaque(), curr = head;;) { ++ final UpdateQueueNode next = curr.getNextVolatile(); ++ final Section element = curr.getSectionVolatile(); /* Likely in sync */ ++ ++ if (element != null) { ++ if (this.getHeadOpaque() == head && curr != head) { ++ this.setHeadOpaque(curr); ++ } ++ return curr; ++ } ++ ++ if (next == null) { ++ if (this.getHeadOpaque() == head && curr != head) { ++ this.setHeadOpaque(curr); ++ } ++ return null; ++ } ++ curr = next; ++ } ++ } ++ ++ public void remove(final UpdateQueueNode node) { ++ // mark as removed ++ node.setSectionVolatile(null); ++ ++ // use peek to advance head ++ this.peek(); ++ ++ // unpark any waiters / block the wait queue ++ Thread unpark; ++ while ((unpark = node.poll()) != null) { ++ LockSupport.unpark(unpark); ++ } ++ } ++ ++ public void append(final UpdateQueueNode node) { ++ int failures = 0; ++ ++ for (UpdateQueueNode currTail = this.getTailOpaque(), curr = currTail;;) { ++ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */ ++ /* It is likely due to a cache miss caused by another write to the next field */ ++ final UpdateQueueNode next = curr.getNextVolatile(); ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (next == null) { ++ node.order = curr.order + 1L; ++ final UpdateQueueNode compared = curr.compareExchangeNextVolatile(null, node); ++ ++ if (compared == null) { ++ /* Added */ ++ /* Avoid CASing on tail more than we need to */ ++ /* CAS to avoid setting an out-of-date tail */ ++ if (this.getTailOpaque() == currTail) { ++ this.setTailOpaque(node); ++ } ++ return; ++ } ++ ++ ++failures; ++ curr = compared; ++ continue; ++ } ++ ++ if (curr == currTail) { ++ /* Tail is likely not up-to-date */ ++ curr = next; ++ } else { ++ /* Try to update to tail */ ++ if (currTail == (currTail = this.getTailOpaque())) { ++ curr = next; ++ } else { ++ curr = currTail; ++ } ++ } ++ } ++ } ++ ++ // each node also represents a set of waiters, represented by the MTQ ++ // if the queue is add-blocked, then the update is complete ++ private static final class UpdateQueueNode extends MultiThreadedQueue<Thread> { ++ private final int sectionX; ++ private final int sectionZ; ++ ++ private long order; ++ private volatile Section section; ++ private volatile UpdateQueueNode next; ++ private volatile boolean updating; ++ ++ private static final VarHandle SECTION_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "section", Section.class); ++ private static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "next", UpdateQueueNode.class); ++ private static final VarHandle UPDATING_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "updating", boolean.class); ++ ++ public UpdateQueueNode(final Section section, final UpdateQueueNode next) { ++ if (section == null) { ++ this.sectionX = this.sectionZ = 0; ++ } else { ++ this.sectionX = section.sectionX; ++ this.sectionZ = section.sectionZ; ++ } ++ ++ SECTION_HANDLE.set(this, section); ++ NEXT_HANDLE.set(this, next); ++ } ++ ++ public boolean intersects(final UpdateQueueNode other) { ++ final int dist = Math.max(Math.abs(this.sectionX - other.sectionX), Math.abs(this.sectionZ - other.sectionZ)); ++ ++ // intersection radius is ticket update radius (1) + scheduling radius ++ return dist <= (1 + ((getMaxSchedulingRadius() + (SECTION_SIZE - 1)) >> SECTION_SHIFT)); ++ } ++ ++ /* section */ ++ ++ private final Section getSectionPlain() { ++ return (Section)SECTION_HANDLE.get(this); ++ } ++ ++ private final Section getSectionVolatile() { ++ return (Section)SECTION_HANDLE.getVolatile(this); ++ } ++ ++ private final void setSectionPlain(final Section update) { ++ SECTION_HANDLE.set(this, update); ++ } ++ ++ private final void setSectionOpaque(final Section update) { ++ SECTION_HANDLE.setOpaque(this, update); ++ } ++ ++ private final void setSectionVolatile(final Section update) { ++ SECTION_HANDLE.setVolatile(this, update); ++ } ++ ++ private final Section getAndSetSectionVolatile(final Section update) { ++ return (Section)SECTION_HANDLE.getAndSet(this, update); ++ } ++ ++ private final Section compareExchangeSectionVolatile(final Section expect, final Section update) { ++ return (Section)SECTION_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ /* next */ ++ ++ private final UpdateQueueNode getNextPlain() { ++ return (UpdateQueueNode)NEXT_HANDLE.get(this); ++ } ++ ++ private final UpdateQueueNode getNextOpaque() { ++ return (UpdateQueueNode)NEXT_HANDLE.getOpaque(this); ++ } ++ ++ private final UpdateQueueNode getNextAcquire() { ++ return (UpdateQueueNode)NEXT_HANDLE.getAcquire(this); ++ } ++ ++ private final UpdateQueueNode getNextVolatile() { ++ return (UpdateQueueNode)NEXT_HANDLE.getVolatile(this); ++ } ++ ++ private final void setNextPlain(final UpdateQueueNode next) { ++ NEXT_HANDLE.set(this, next); ++ } ++ ++ private final void setNextVolatile(final UpdateQueueNode next) { ++ NEXT_HANDLE.setVolatile(this, next); ++ } ++ ++ private final UpdateQueueNode compareExchangeNextVolatile(final UpdateQueueNode expect, final UpdateQueueNode set) { ++ return (UpdateQueueNode)NEXT_HANDLE.compareAndExchange(this, expect, set); ++ } ++ ++ /* updating */ ++ ++ private final boolean getUpdatingVolatile() { ++ return (boolean)UPDATING_HANDLE.getVolatile(this); ++ } ++ ++ private final boolean getAndSetUpdatingVolatile(final boolean value) { ++ return (boolean)UPDATING_HANDLE.getAndSet(this, value); ++ } ++ } ++ } ++ ++ private static final class Section { ++ ++ // upper 8 bits: sources, lower 8 bits: level ++ // if we REALLY wanted to get crazy, we could make the increase propagator use MethodHandles#byteArrayViewVarHandle ++ // to read and write the lower 8 bits of this array directly rather than reading, updating the bits, then writing back. ++ private final short[] levels = new short[SECTION_SIZE * SECTION_SIZE]; ++ // set of local positions that represent sources ++ private final ShortOpenHashSet sources = new ShortOpenHashSet(); ++ // map of local index to new source level ++ // the source level _cannot_ be updated in the backing storage immediately since the update ++ private static final byte NO_QUEUED_UPDATE = (byte)-1; ++ private final Short2ByteLinkedOpenHashMap queuedSources = new Short2ByteLinkedOpenHashMap(); ++ { ++ this.queuedSources.defaultReturnValue(NO_QUEUED_UPDATE); ++ } ++ private int oneRadNeighboursWithSources = 0; ++ ++ public final int sectionX; ++ public final int sectionZ; ++ ++ public Section(final int sectionX, final int sectionZ) { ++ this.sectionX = sectionX; ++ this.sectionZ = sectionZ; ++ } ++ ++ public boolean isZero() { ++ for (final short val : this.levels) { ++ if (val != 0) { ++ return false; ++ } ++ } ++ return true; ++ } ++ ++ @Override ++ public String toString() { ++ final StringBuilder ret = new StringBuilder(); ++ ++ for (int x = 0; x < SECTION_SIZE; ++x) { ++ ret.append("levels x=").append(x).append("\n"); ++ for (int z = 0; z < SECTION_SIZE; ++z) { ++ final short v = this.levels[x | (z << SECTION_SHIFT)]; ++ ret.append(v & 0xFF).append("."); ++ } ++ ret.append("\n"); ++ ret.append("sources x=").append(x).append("\n"); ++ for (int z = 0; z < SECTION_SIZE; ++z) { ++ final short v = this.levels[x | (z << SECTION_SHIFT)]; ++ ret.append((v >>> 8) & 0xFF).append("."); ++ } ++ ret.append("\n\n"); ++ } ++ ++ return ret.toString(); ++ } ++ } ++ ++ ++ private static final class Propagator { ++ ++ private static final ArrayDeque<Propagator> CACHED_PROPAGATORS = new ArrayDeque<>(); ++ private static final int MAX_PROPAGATORS = Runtime.getRuntime().availableProcessors() * 2; ++ ++ private static Propagator acquirePropagator() { ++ synchronized (CACHED_PROPAGATORS) { ++ final Propagator ret = CACHED_PROPAGATORS.pollFirst(); ++ if (ret != null) { ++ return ret; ++ } ++ } ++ return new Propagator(); ++ } ++ ++ private static void returnPropagator(final Propagator propagator) { ++ synchronized (CACHED_PROPAGATORS) { ++ if (CACHED_PROPAGATORS.size() < MAX_PROPAGATORS) { ++ CACHED_PROPAGATORS.add(propagator); ++ } ++ } ++ } ++ ++ private static final int SECTION_RADIUS = 2; ++ private static final int SECTION_CACHE_WIDTH = 2 * SECTION_RADIUS + 1; ++ // minimum number of bits to represent [0, SECTION_SIZE * SECTION_CACHE_WIDTH) ++ private static final int COORDINATE_BITS = 9; ++ private static final int COORDINATE_SIZE = 1 << COORDINATE_BITS; ++ static { ++ if ((SECTION_SIZE * SECTION_CACHE_WIDTH) > (1 << COORDINATE_BITS)) { ++ throw new IllegalStateException("Adjust COORDINATE_BITS"); ++ } ++ } ++ // index = x + (z * SECTION_CACHE_WIDTH) ++ // (this requires x >= 0 and z >= 0) ++ private final Section[] sections = new Section[SECTION_CACHE_WIDTH * SECTION_CACHE_WIDTH]; ++ ++ private int encodeOffsetX; ++ private int encodeOffsetZ; ++ ++ private int coordinateOffset; ++ ++ private int encodeSectionOffsetX; ++ private int encodeSectionOffsetZ; ++ ++ private int sectionIndexOffset; ++ ++ public final boolean hasUpdates() { ++ return this.decreaseQueueInitialLength != 0 || this.increaseQueueInitialLength != 0; ++ } ++ ++ private final void setupEncodeOffset(final int centerSectionX, final int centerSectionZ) { ++ final int maxCoordinate = (SECTION_RADIUS * SECTION_SIZE - 1); ++ // must have that encoded >= 0 ++ // coordinates can range from [-maxCoordinate + centerSection*SECTION_SIZE, maxCoordinate + centerSection*SECTION_SIZE] ++ // we want a range of [0, maxCoordinate*2] ++ // so, 0 = -maxCoordinate + centerSection*SECTION_SIZE + offset ++ this.encodeOffsetX = maxCoordinate - (centerSectionX << SECTION_SHIFT); ++ this.encodeOffsetZ = maxCoordinate - (centerSectionZ << SECTION_SHIFT); ++ ++ // encoded coordinates range from [0, SECTION_SIZE * SECTION_CACHE_WIDTH) ++ // coordinate index = (x + encodeOffsetX) + ((z + encodeOffsetZ) << COORDINATE_BITS) ++ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << COORDINATE_BITS); ++ ++ // need encoded values to be >= 0 ++ // so, 0 = (-SECTION_RADIUS + centerSectionX) + encodeOffset ++ this.encodeSectionOffsetX = SECTION_RADIUS - centerSectionX; ++ this.encodeSectionOffsetZ = SECTION_RADIUS - centerSectionZ; ++ ++ // section index = (secX + encodeSectionOffsetX) + ((secZ + encodeSectionOffsetZ) * SECTION_CACHE_WIDTH) ++ this.sectionIndexOffset = this.encodeSectionOffsetX + (this.encodeSectionOffsetZ * SECTION_CACHE_WIDTH); ++ } ++ ++ // must hold ticket lock for (centerSectionX,centerSectionZ) in radius rad ++ // must call setupEncodeOffset ++ private final void setupCaches(final ThreadedTicketLevelPropagator propagator, ++ final int centerSectionX, final int centerSectionZ, ++ final int rad) { ++ for (int dz = -rad; dz <= rad; ++dz) { ++ for (int dx = -rad; dx <= rad; ++dx) { ++ final int sectionX = centerSectionX + dx; ++ final int sectionZ = centerSectionZ + dz; ++ final long coordinate = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ final Section section = propagator.sections.get(coordinate); ++ ++ if (section == null) { ++ throw new IllegalStateException("Section at " + coordinate + " should not be null"); ++ } ++ ++ this.setSectionInCache(sectionX, sectionZ, section); ++ } ++ } ++ } ++ ++ private final void setSectionInCache(final int sectionX, final int sectionZ, final Section section) { ++ this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset] = section; ++ } ++ ++ private final Section getSection(final int sectionX, final int sectionZ) { ++ return this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset]; ++ } ++ ++ private final int getLevel(final int posX, final int posZ) { ++ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset]; ++ if (section != null) { ++ return (int)section.levels[(posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT)] & 0xFF; ++ } ++ ++ return 0; ++ } ++ ++ private final void setLevel(final int posX, final int posZ, final int to) { ++ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset]; ++ if (section != null) { ++ final int index = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); ++ final short level = section.levels[index]; ++ section.levels[index] = (short)((level & ~0xFF) | (to & 0xFF)); ++ this.updatedPositions.put(CoordinateUtils.getChunkKey(posX, posZ), (byte)to); ++ } ++ } ++ ++ private final void destroyCaches() { ++ Arrays.fill(this.sections, null); ++ } ++ ++ // contains: ++ // lower (COORDINATE_BITS(9) + COORDINATE_BITS(9) = 18) bits encoded position: (x | (z << COORDINATE_BITS)) ++ // next LEVEL_BITS (6) bits: propagated level [0, 63] ++ // propagation directions bitset (16 bits): ++ private static final long ALL_DIRECTIONS_BITSET = ( ++ // z = -1 ++ (1L << ((1 - 1) | ((1 - 1) << 2))) | ++ (1L << ((1 + 0) | ((1 - 1) << 2))) | ++ (1L << ((1 + 1) | ((1 - 1) << 2))) | ++ ++ // z = 0 ++ (1L << ((1 - 1) | ((1 + 0) << 2))) | ++ //(1L << ((1 + 0) | ((1 + 0) << 2))) | // exclude (0,0) ++ (1L << ((1 + 1) | ((1 + 0) << 2))) | ++ ++ // z = 1 ++ (1L << ((1 - 1) | ((1 + 1) << 2))) | ++ (1L << ((1 + 0) | ((1 + 1) << 2))) | ++ (1L << ((1 + 1) | ((1 + 1) << 2))) ++ ); ++ ++ private void ex(int bitset) { ++ for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) { ++ final int set = Integer.numberOfTrailingZeros(bitset); ++ final int tailingBit = (-bitset) & bitset; ++ // XOR to remove the trailing bit ++ bitset ^= tailingBit; ++ ++ // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits ++ // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the ++ // index of the set bit is the encoded value ++ // the encoded coordinate has 3 valid states: ++ // 0b00 (0) -> -1 ++ // 0b01 (1) -> 0 ++ // 0b10 (2) -> 1 ++ // the decode operation then is val - 1, and the encode operation is val + 1 ++ final int xOff = (set & 3) - 1; ++ final int zOff = ((set >>> 2) & 3) - 1; ++ System.out.println("Encoded: (" + xOff + "," + zOff + ")"); ++ } ++ } ++ ++ private void ch(long bs, int shift) { ++ int bitset = (int)(bs >>> shift); ++ for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) { ++ final int set = Integer.numberOfTrailingZeros(bitset); ++ final int tailingBit = (-bitset) & bitset; ++ // XOR to remove the trailing bit ++ bitset ^= tailingBit; ++ ++ // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits ++ // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the ++ // index of the set bit is the encoded value ++ // the encoded coordinate has 3 valid states: ++ // 0b00 (0) -> -1 ++ // 0b01 (1) -> 0 ++ // 0b10 (2) -> 1 ++ // the decode operation then is val - 1, and the encode operation is val + 1 ++ final int xOff = (set & 3) - 1; ++ final int zOff = ((set >>> 2) & 3) - 1; ++ if (Math.abs(xOff) > 1 || Math.abs(zOff) > 1 || (xOff | zOff) == 0) { ++ throw new IllegalStateException(); ++ } ++ } ++ } ++ ++ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading ++ // updates for sources ++ private static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 1; ++ // whether the propagation needs to check if its current level is equal to the expected level ++ // used only in increase propagation ++ private static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 0; ++ ++ private long[] increaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2]; ++ private int increaseQueueInitialLength; ++ private long[] decreaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2]; ++ private int decreaseQueueInitialLength; ++ ++ private final Long2ByteLinkedOpenHashMap updatedPositions = new Long2ByteLinkedOpenHashMap(); ++ ++ private final long[] resizeIncreaseQueue() { ++ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2); ++ } ++ ++ private final long[] resizeDecreaseQueue() { ++ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2); ++ } ++ ++ private final void appendToIncreaseQueue(final long value) { ++ final int idx = this.increaseQueueInitialLength++; ++ long[] queue = this.increaseQueue; ++ if (idx >= queue.length) { ++ queue = this.resizeIncreaseQueue(); ++ queue[idx] = value; ++ return; ++ } else { ++ queue[idx] = value; ++ return; ++ } ++ } ++ ++ private final void appendToDecreaseQueue(final long value) { ++ final int idx = this.decreaseQueueInitialLength++; ++ long[] queue = this.decreaseQueue; ++ if (idx >= queue.length) { ++ queue = this.resizeDecreaseQueue(); ++ queue[idx] = value; ++ return; ++ } else { ++ queue[idx] = value; ++ return; ++ } ++ } ++ ++ private final void performIncrease() { ++ long[] queue = this.increaseQueue; ++ int queueReadIndex = 0; ++ int queueLength = this.increaseQueueInitialLength; ++ this.increaseQueueInitialLength = 0; ++ final int decodeOffsetX = -this.encodeOffsetX; ++ final int decodeOffsetZ = -this.encodeOffsetZ; ++ final int encodeOffset = this.coordinateOffset; ++ final int sectionOffset = this.sectionIndexOffset; ++ ++ final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions; ++ ++ while (queueReadIndex < queueLength) { ++ final long queueValue = queue[queueReadIndex++]; ++ ++ final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX; ++ final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ; ++ final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1); ++ // note: the above code requires coordinate bits * 2 < 32 ++ // bitset is 16 bits ++ int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1); ++ ++ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) { ++ if (this.getLevel(posX, posZ) != propagatedLevel) { ++ // not at the level we expect, so something changed. ++ continue; ++ } ++ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) { ++ // these are used to restore sources after a propagation decrease ++ this.setLevel(posX, posZ, propagatedLevel); ++ } ++ ++ // this bitset represents the values that we have not propagated to ++ // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases ++ // significantly reducing the total number of ops ++ // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need ++ // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead ++ // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits) ++ // to make things easy, we use positions [0, 4] in the bitset, with current position being 2 ++ // index = x | (z << 3) ++ ++ // to start, we eliminate everything 1 radius from the current position as the previous propagator ++ // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius ++ // but the rest not propagated are already handled ++ long currentPropagation = ~( ++ // z = -1 ++ (1L << ((2 - 1) | ((2 - 1) << 3))) | ++ (1L << ((2 + 0) | ((2 - 1) << 3))) | ++ (1L << ((2 + 1) | ((2 - 1) << 3))) | ++ ++ // z = 0 ++ (1L << ((2 - 1) | ((2 + 0) << 3))) | ++ (1L << ((2 + 0) | ((2 + 0) << 3))) | ++ (1L << ((2 + 1) | ((2 + 0) << 3))) | ++ ++ // z = 1 ++ (1L << ((2 - 1) | ((2 + 1) << 3))) | ++ (1L << ((2 + 0) | ((2 + 1) << 3))) | ++ (1L << ((2 + 1) | ((2 + 1) << 3))) ++ ); ++ ++ final int toPropagate = propagatedLevel - 1; ++ ++ // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting ++ // the bits, the cpu loop predictor should perfectly predict the loop. ++ for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) { ++ final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset); ++ final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset; ++ propagateDirectionBitset ^= tailingBit; ++ ++ // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset ++ // it has been split to save some cycles via parallelism ++ final int pDecodeX = (set & 3); ++ final int pDecodeZ = ((set >>> 2) & 3); ++ ++ // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX ++ final int offX = (posX - 1) + pDecodeX; ++ final int offZ = (posZ - 1) + pDecodeZ; ++ ++ final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset; ++ final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); ++ ++ // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset ++ // bitset idx = x | (z << 3) ++ ++ // read three bits, so we need 7L ++ // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1 ++ // nstartidx1 = x rel -1 for z rel -1 ++ // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3) ++ // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3) ++ // = pDecodeX | (pDecodeZ << 3) = start ++ final int start = pDecodeX | (pDecodeZ << 3); ++ final long bitsetLine1 = currentPropagation & (7L << (start)); ++ ++ // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset) ++ final long bitsetLine2 = currentPropagation & (7L << (start + 8)); ++ ++ // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset) ++ final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8))); ++ ++ // remove ("take") lines from bitset ++ currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3); ++ ++ // now try to propagate ++ final Section section = this.sections[sectionIndex]; ++ ++ // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag ++ final short currentStoredLevel = section.levels[localIndex]; ++ final int currentLevel = currentStoredLevel & 0xFF; ++ ++ if (currentLevel >= toPropagate) { ++ continue; // already at the level we want ++ } ++ ++ // update level ++ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF) | (toPropagate & 0xFF)); ++ updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)toPropagate); ++ ++ // queue next ++ if (toPropagate > 1) { ++ // now combine into one bitset to pass to child ++ // the child bitset is 4x4, so we just shift each line by 4 ++ // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value ++ final long childPropagation = ++ ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1 ++ ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0 ++ ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1 ++ ++ // don't queue update if toPropagate cannot propagate anything to neighbours ++ // (for increase, propagating 0 to neighbours is useless) ++ if (queueLength >= queue.length) { ++ queue = this.resizeIncreaseQueue(); ++ } ++ queue[queueLength++] = ++ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | ++ ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | ++ childPropagation; //(ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); ++ continue; ++ } ++ continue; ++ } ++ } ++ } ++ ++ private final void performDecrease() { ++ long[] queue = this.decreaseQueue; ++ long[] increaseQueue = this.increaseQueue; ++ int queueReadIndex = 0; ++ int queueLength = this.decreaseQueueInitialLength; ++ this.decreaseQueueInitialLength = 0; ++ int increaseQueueLength = this.increaseQueueInitialLength; ++ final int decodeOffsetX = -this.encodeOffsetX; ++ final int decodeOffsetZ = -this.encodeOffsetZ; ++ final int encodeOffset = this.coordinateOffset; ++ final int sectionOffset = this.sectionIndexOffset; ++ ++ final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions; ++ ++ while (queueReadIndex < queueLength) { ++ final long queueValue = queue[queueReadIndex++]; ++ ++ final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX; ++ final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ; ++ final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1); ++ // note: the above code requires coordinate bits * 2 < 32 ++ // bitset is 16 bits ++ int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1); ++ ++ // this bitset represents the values that we have not propagated to ++ // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases ++ // significantly reducing the total number of ops ++ // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need ++ // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead ++ // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits) ++ // to make things easy, we use positions [0, 4] in the bitset, with current position being 2 ++ // index = x | (z << 3) ++ ++ // to start, we eliminate everything 1 radius from the current position as the previous propagator ++ // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius ++ // but the rest not propagated are already handled ++ long currentPropagation = ~( ++ // z = -1 ++ (1L << ((2 - 1) | ((2 - 1) << 3))) | ++ (1L << ((2 + 0) | ((2 - 1) << 3))) | ++ (1L << ((2 + 1) | ((2 - 1) << 3))) | ++ ++ // z = 0 ++ (1L << ((2 - 1) | ((2 + 0) << 3))) | ++ (1L << ((2 + 0) | ((2 + 0) << 3))) | ++ (1L << ((2 + 1) | ((2 + 0) << 3))) | ++ ++ // z = 1 ++ (1L << ((2 - 1) | ((2 + 1) << 3))) | ++ (1L << ((2 + 0) | ((2 + 1) << 3))) | ++ (1L << ((2 + 1) | ((2 + 1) << 3))) ++ ); ++ ++ final int toPropagate = propagatedLevel - 1; ++ ++ // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting ++ // the bits, the cpu loop predictor should perfectly predict the loop. ++ for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) { ++ final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset); ++ final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset; ++ propagateDirectionBitset ^= tailingBit; ++ ++ ++ // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset ++ // it has been split to save some cycles via parallelism ++ final int pDecodeX = (set & 3); ++ final int pDecodeZ = ((set >>> 2) & 3); ++ ++ // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX ++ final int offX = (posX - 1) + pDecodeX; ++ final int offZ = (posZ - 1) + pDecodeZ; ++ ++ final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset; ++ final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); ++ ++ // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset ++ // bitset idx = x | (z << 3) ++ ++ // read three bits, so we need 7L ++ // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1 ++ // nstartidx1 = x rel -1 for z rel -1 ++ // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3) ++ // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3) ++ // = pDecodeX | (pDecodeZ << 3) = start ++ final int start = pDecodeX | (pDecodeZ << 3); ++ final long bitsetLine1 = currentPropagation & (7L << (start)); ++ ++ // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset) ++ final long bitsetLine2 = currentPropagation & (7L << (start + 8)); ++ ++ // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset) ++ final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8))); ++ ++ // now try to propagate ++ final Section section = this.sections[sectionIndex]; ++ ++ // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag ++ final short currentStoredLevel = section.levels[localIndex]; ++ final int currentLevel = currentStoredLevel & 0xFF; ++ final int sourceLevel = (currentStoredLevel >>> 8) & 0xFF; ++ ++ if (currentLevel == 0) { ++ continue; // already at the level we want ++ } ++ ++ if (currentLevel > toPropagate) { ++ // it looks like another source propagated here, so re-propagate it ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | ++ ((currentLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | ++ (FLAG_RECHECK_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS))); ++ continue; ++ } ++ ++ // remove ("take") lines from bitset ++ // can't do this during decrease, TODO WHY? ++ //currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3); ++ ++ // update level ++ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF)); ++ updatedPositions.putAndMoveToLast(CoordinateUtils.getChunkKey(offX, offZ), (byte)0); ++ ++ if (sourceLevel != 0) { ++ // re-propagate source ++ // note: do not set recheck level, or else the propagation will fail ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | ++ ((sourceLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | ++ (FLAG_WRITE_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS))); ++ } ++ ++ // queue next ++ // note: targetLevel > 0 here, since toPropagate >= currentLevel and currentLevel > 0 ++ // now combine into one bitset to pass to child ++ // the child bitset is 4x4, so we just shift each line by 4 ++ // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value ++ final long childPropagation = ++ ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1 ++ ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0 ++ ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1 ++ ++ // don't queue update if toPropagate cannot propagate anything to neighbours ++ // (for increase, propagating 0 to neighbours is useless) ++ if (queueLength >= queue.length) { ++ queue = this.resizeDecreaseQueue(); ++ } ++ queue[queueLength++] = ++ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | ++ ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | ++ (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); //childPropagation; ++ continue; ++ } ++ } ++ ++ // propagate sources we clobbered ++ this.increaseQueueInitialLength = increaseQueueLength; ++ this.performIncrease(); ++ } ++ } ++ ++ /* ++ private static final java.util.Random random = new java.util.Random(4L); ++ private static final List<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void>> walkers = ++ new java.util.ArrayList<>(); ++ static final int PLAYERS = 0; ++ static final int RAD_BLOCKS = 10000; ++ static final int RAD = RAD_BLOCKS >> 4; ++ static final int RAD_BIG_BLOCKS = 100_000; ++ static final int RAD_BIG = RAD_BIG_BLOCKS >> 4; ++ static final int VD = 4; ++ static final int BIG_PLAYERS = 50; ++ static final double WALK_CHANCE = 0.10; ++ static final double TP_CHANCE = 0.01; ++ static final int TP_BACK_PLAYERS = 200; ++ static final double TP_BACK_CHANCE = 0.25; ++ static final double TP_STEAL_CHANCE = 0.25; ++ private static final List<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void>> tpBack = ++ new java.util.ArrayList<>(); ++ ++ public static void main(final String[] args) { ++ final ReentrantAreaLock ticketLock = new ReentrantAreaLock(SECTION_SHIFT); ++ final ReentrantAreaLock schedulingLock = new ReentrantAreaLock(SECTION_SHIFT); ++ final Long2ByteLinkedOpenHashMap levelMap = new Long2ByteLinkedOpenHashMap(); ++ final Long2ByteLinkedOpenHashMap refMap = new Long2ByteLinkedOpenHashMap(); ++ final io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D ref = new io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D((final long coordinate, final byte oldLevel, final byte newLevel) -> { ++ if (newLevel == 0) { ++ refMap.remove(coordinate); ++ } else { ++ refMap.put(coordinate, newLevel); ++ } ++ }); ++ final ThreadedTicketLevelPropagator propagator = new ThreadedTicketLevelPropagator() { ++ @Override ++ protected void processLevelUpdates(Long2ByteLinkedOpenHashMap updates) { ++ for (final long key : updates.keySet()) { ++ final byte val = updates.get(key); ++ if (val == 0) { ++ levelMap.remove(key); ++ } else { ++ levelMap.put(key, val); ++ } ++ } ++ } ++ ++ @Override ++ protected void processSchedulingUpdates(Long2ByteLinkedOpenHashMap updates, List<ChunkProgressionTask> scheduledTasks, List<NewChunkHolder> changedFullStatus) {} ++ }; ++ ++ for (;;) { ++ if (walkers.isEmpty() && tpBack.isEmpty()) { ++ for (int i = 0; i < PLAYERS; ++i) { ++ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; ++ int posX = random.nextInt(-rad, rad + 1); ++ int posZ = random.nextInt(-rad, rad + 1); ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { ++ @Override ++ protected void addCallback(Void parameter, int chunkX, int chunkZ) { ++ int src = 45 - 31 + 1; ++ ref.setSource(chunkX, chunkZ, src); ++ propagator.setSource(chunkX, chunkZ, src); ++ } ++ ++ @Override ++ protected void removeCallback(Void parameter, int chunkX, int chunkZ) { ++ ref.removeSource(chunkX, chunkZ); ++ propagator.removeSource(chunkX, chunkZ); ++ } ++ }; ++ ++ map.add(posX, posZ, VD); ++ ++ walkers.add(map); ++ } ++ for (int i = 0; i < TP_BACK_PLAYERS; ++i) { ++ int rad = RAD_BIG; ++ int posX = random.nextInt(-rad, rad + 1); ++ int posZ = random.nextInt(-rad, rad + 1); ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { ++ @Override ++ protected void addCallback(Void parameter, int chunkX, int chunkZ) { ++ int src = 45 - 31 + 1; ++ ref.setSource(chunkX, chunkZ, src); ++ propagator.setSource(chunkX, chunkZ, src); ++ } ++ ++ @Override ++ protected void removeCallback(Void parameter, int chunkX, int chunkZ) { ++ ref.removeSource(chunkX, chunkZ); ++ propagator.removeSource(chunkX, chunkZ); ++ } ++ }; ++ ++ map.add(posX, posZ, random.nextInt(1, 63)); ++ ++ tpBack.add(map); ++ } ++ } else { ++ for (int i = 0; i < PLAYERS; ++i) { ++ if (random.nextDouble() > WALK_CHANCE) { ++ continue; ++ } ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = walkers.get(i); ++ ++ int updateX = random.nextInt(-1, 2); ++ int updateZ = random.nextInt(-1, 2); ++ ++ map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD); ++ } ++ ++ for (int i = 0; i < PLAYERS; ++i) { ++ if (random.nextDouble() > TP_CHANCE) { ++ continue; ++ } ++ ++ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; ++ int posX = random.nextInt(-rad, rad + 1); ++ int posZ = random.nextInt(-rad, rad + 1); ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = walkers.get(i); ++ ++ map.update(posX, posZ, VD); ++ } ++ ++ for (int i = 0; i < TP_BACK_PLAYERS; ++i) { ++ if (random.nextDouble() > TP_BACK_CHANCE) { ++ continue; ++ } ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = tpBack.get(i); ++ ++ map.update(-map.lastChunkX, -map.lastChunkZ, random.nextInt(1, 63)); ++ ++ if (random.nextDouble() > TP_STEAL_CHANCE) { ++ propagator.performUpdate( ++ map.lastChunkX >> SECTION_SHIFT, map.lastChunkZ >> SECTION_SHIFT, schedulingLock, null, null ++ ); ++ propagator.performUpdate( ++ (-map.lastChunkX >> SECTION_SHIFT), (-map.lastChunkZ >> SECTION_SHIFT), schedulingLock, null, null ++ ); ++ } ++ } ++ } ++ ++ ref.propagateUpdates(); ++ propagator.performUpdates(ticketLock, schedulingLock, null, null); ++ ++ if (!refMap.equals(levelMap)) { ++ throw new IllegalStateException("Error!"); ++ } ++ } ++ } ++ */ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373ba811ba8b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java +@@ -0,0 +1,668 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++ ++import java.util.ArrayList; ++import java.util.Comparator; ++import java.util.List; ++import java.util.PriorityQueue; ++ ++public class RadiusAwarePrioritisedExecutor { ++ ++ private static final Comparator<DependencyNode> DEPENDENCY_NODE_COMPARATOR = (final DependencyNode t1, final DependencyNode t2) -> { ++ return Long.compare(t1.id, t2.id); ++ }; ++ ++ private final DependencyTree[] queues = new DependencyTree[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES]; ++ private static final int NO_TASKS_QUEUED = -1; ++ private int selectedQueue = NO_TASKS_QUEUED; ++ private boolean canQueueTasks = true; ++ ++ public RadiusAwarePrioritisedExecutor(final PrioritisedExecutor executor, final int maxToSchedule) { ++ for (int i = 0; i < this.queues.length; ++i) { ++ this.queues[i] = new DependencyTree(this, executor, maxToSchedule, i); ++ } ++ } ++ ++ private boolean canQueueTasks() { ++ return this.canQueueTasks; ++ } ++ ++ private List<PrioritisedExecutor.PrioritisedTask> treeFinished() { ++ this.canQueueTasks = true; ++ for (int priority = 0; priority < this.queues.length; ++priority) { ++ final DependencyTree queue = this.queues[priority]; ++ if (queue.hasWaitingTasks()) { ++ final List<PrioritisedExecutor.PrioritisedTask> ret = queue.tryPushTasks(); ++ ++ if (ret == null || ret.isEmpty()) { ++ // this happens when the tasks in the wait queue were purged ++ // in this case, the queue was actually empty, we just had to purge it ++ // if we set the selected queue without scheduling any tasks, the queue will never be unselected ++ // as that requires a scheduled task completing... ++ continue; ++ } ++ ++ this.selectedQueue = priority; ++ return ret; ++ } ++ } ++ ++ this.selectedQueue = NO_TASKS_QUEUED; ++ ++ return null; ++ } ++ ++ private List<PrioritisedExecutor.PrioritisedTask> queue(final Task task, final PrioritisedExecutor.Priority priority) { ++ final int priorityId = priority.priority; ++ final DependencyTree queue = this.queues[priorityId]; ++ ++ final DependencyNode node = new DependencyNode(task, queue); ++ ++ if (task.dependencyNode != null) { ++ throw new IllegalStateException(); ++ } ++ task.dependencyNode = node; ++ ++ queue.pushNode(node); ++ ++ if (this.selectedQueue == NO_TASKS_QUEUED) { ++ this.canQueueTasks = true; ++ this.selectedQueue = priorityId; ++ return queue.tryPushTasks(); ++ } ++ ++ if (!this.canQueueTasks) { ++ return null; ++ } ++ ++ if (PrioritisedExecutor.Priority.isHigherPriority(priorityId, this.selectedQueue)) { ++ // prevent the lower priority tree from queueing more tasks ++ this.canQueueTasks = false; ++ return null; ++ } ++ ++ // priorityId != selectedQueue: lower priority, don't care - treeFinished will pick it up ++ return priorityId == this.selectedQueue ? queue.tryPushTasks() : null; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius, ++ final Runnable run, final PrioritisedExecutor.Priority priority) { ++ if (radius < 0) { ++ throw new IllegalArgumentException("Radius must be > 0: " + radius); ++ } ++ return new Task(this, chunkX, chunkZ, radius, run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius, ++ final Runnable run) { ++ return this.createTask(chunkX, chunkZ, radius, run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius, ++ final Runnable run, final PrioritisedExecutor.Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run, priority); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius, ++ final Runnable run) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return new Task(this, 0, 0, -1, run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run) { ++ return this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, priority); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ // all accesses must be synchronised by the radius aware object ++ private static final class DependencyTree { ++ ++ private final RadiusAwarePrioritisedExecutor scheduler; ++ private final PrioritisedExecutor executor; ++ private final int maxToSchedule; ++ private final int treeIndex; ++ ++ private int currentlyExecuting; ++ private long idGenerator; ++ ++ private final PriorityQueue<DependencyNode> awaiting = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); ++ ++ private final PriorityQueue<DependencyNode> infiniteRadius = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); ++ private boolean isInfiniteRadiusScheduled; ++ ++ private final Long2ReferenceOpenHashMap<DependencyNode> nodeByPosition = new Long2ReferenceOpenHashMap<>(); ++ ++ public DependencyTree(final RadiusAwarePrioritisedExecutor scheduler, final PrioritisedExecutor executor, ++ final int maxToSchedule, final int treeIndex) { ++ this.scheduler = scheduler; ++ this.executor = executor; ++ this.maxToSchedule = maxToSchedule; ++ this.treeIndex = treeIndex; ++ } ++ ++ public boolean hasWaitingTasks() { ++ return !this.awaiting.isEmpty() || !this.infiniteRadius.isEmpty(); ++ } ++ ++ private long nextId() { ++ return this.idGenerator++; ++ } ++ ++ private boolean isExecutingAnyTasks() { ++ return this.currentlyExecuting != 0; ++ } ++ ++ private void pushNode(final DependencyNode node) { ++ if (!node.task.isFiniteRadius()) { ++ this.infiniteRadius.add(node); ++ return; ++ } ++ ++ // set up dependency for node ++ final Task task = node.task; ++ ++ final int centerX = task.chunkX; ++ final int centerZ = task.chunkZ; ++ final int radius = task.radius; ++ ++ final int minX = centerX - radius; ++ final int maxX = centerX + radius; ++ ++ final int minZ = centerZ - radius; ++ final int maxZ = centerZ + radius; ++ ++ ReferenceOpenHashSet<DependencyNode> parents = null; ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ final DependencyNode dependency = this.nodeByPosition.put(CoordinateUtils.getChunkKey(currX, currZ), node); ++ if (dependency != null) { ++ if (parents == null) { ++ parents = new ReferenceOpenHashSet<>(); ++ } ++ if (parents.add(dependency)) { ++ // added a dependency, so we need to add as a child to the dependency ++ if (dependency.children == null) { ++ dependency.children = new ArrayList<>(); ++ } ++ dependency.children.add(node); ++ } ++ } ++ } ++ } ++ ++ if (parents == null) { ++ // no dependencies, add straight to awaiting ++ this.awaiting.add(node); ++ } else { ++ node.parents = parents.size(); ++ // we will be added to awaiting once we have no parents ++ } ++ } ++ ++ // called only when a node is returned after being executed ++ private List<PrioritisedExecutor.PrioritisedTask> returnNode(final DependencyNode node) { ++ final Task task = node.task; ++ ++ // now that the task is completed, we can push its children to the awaiting queue ++ this.pushChildren(node); ++ ++ if (task.isFiniteRadius()) { ++ // remove from dependency map ++ this.removeNodeFromMap(node); ++ } else { ++ // mark as no longer executing infinite radius ++ if (!this.isInfiniteRadiusScheduled) { ++ throw new IllegalStateException(); ++ } ++ this.isInfiniteRadiusScheduled = false; ++ } ++ ++ // decrement executing count, we are done executing this task ++ --this.currentlyExecuting; ++ ++ if (this.currentlyExecuting == 0) { ++ return this.scheduler.treeFinished(); ++ } ++ ++ return this.scheduler.canQueueTasks() ? this.tryPushTasks() : null; ++ } ++ ++ private List<PrioritisedExecutor.PrioritisedTask> tryPushTasks() { ++ // tasks are not queued, but only created here - we do hold the lock for the map ++ List<PrioritisedExecutor.PrioritisedTask> ret = null; ++ PrioritisedExecutor.PrioritisedTask pushedTask; ++ while ((pushedTask = this.tryPushTask()) != null) { ++ if (ret == null) { ++ ret = new ArrayList<>(); ++ } ++ ret.add(pushedTask); ++ } ++ ++ return ret; ++ } ++ ++ private void removeNodeFromMap(final DependencyNode node) { ++ final Task task = node.task; ++ ++ final int centerX = task.chunkX; ++ final int centerZ = task.chunkZ; ++ final int radius = task.radius; ++ ++ final int minX = centerX - radius; ++ final int maxX = centerX + radius; ++ ++ final int minZ = centerZ - radius; ++ final int maxZ = centerZ + radius; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ this.nodeByPosition.remove(CoordinateUtils.getChunkKey(currX, currZ), node); ++ } ++ } ++ } ++ ++ private void pushChildren(final DependencyNode node) { ++ // add all the children that we can into awaiting ++ final List<DependencyNode> children = node.children; ++ if (children != null) { ++ for (int i = 0, len = children.size(); i < len; ++i) { ++ final DependencyNode child = children.get(i); ++ int newParents = --child.parents; ++ if (newParents == 0) { ++ // no more dependents, we can push to awaiting ++ // even if the child is purged, we need to push it so that its children will be pushed ++ this.awaiting.add(child); ++ } else if (newParents < 0) { ++ throw new IllegalStateException(); ++ } ++ } ++ } ++ } ++ ++ private DependencyNode pollAwaiting() { ++ final DependencyNode ret = this.awaiting.poll(); ++ if (ret == null) { ++ return ret; ++ } ++ ++ if (ret.parents != 0) { ++ throw new IllegalStateException(); ++ } ++ ++ if (ret.purged) { ++ // need to manually remove from state here ++ this.pushChildren(ret); ++ this.removeNodeFromMap(ret); ++ } // else: delay children push until the task has finished ++ ++ return ret; ++ } ++ ++ private DependencyNode pollInfinite() { ++ return this.infiniteRadius.poll(); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask tryPushTask() { ++ if (this.currentlyExecuting >= this.maxToSchedule || this.isInfiniteRadiusScheduled) { ++ return null; ++ } ++ ++ DependencyNode firstInfinite; ++ while ((firstInfinite = this.infiniteRadius.peek()) != null && firstInfinite.purged) { ++ this.pollInfinite(); ++ } ++ ++ DependencyNode firstAwaiting; ++ while ((firstAwaiting = this.awaiting.peek()) != null && firstAwaiting.purged) { ++ this.pollAwaiting(); ++ } ++ ++ if (firstInfinite == null && firstAwaiting == null) { ++ return null; ++ } ++ ++ // firstAwaiting compared to firstInfinite ++ final int compare; ++ ++ if (firstAwaiting == null) { ++ // we choose first infinite, or infinite < awaiting ++ compare = 1; ++ } else if (firstInfinite == null) { ++ // we choose first awaiting, or awaiting < infinite ++ compare = -1; ++ } else { ++ compare = DEPENDENCY_NODE_COMPARATOR.compare(firstAwaiting, firstInfinite); ++ } ++ ++ if (compare >= 0) { ++ if (this.currentlyExecuting != 0) { ++ // don't queue infinite task while other tasks are executing in parallel ++ return null; ++ } ++ ++this.currentlyExecuting; ++ this.pollInfinite(); ++ this.isInfiniteRadiusScheduled = true; ++ return firstInfinite.task.pushTask(this.executor); ++ } else { ++ ++this.currentlyExecuting; ++ this.pollAwaiting(); ++ return firstAwaiting.task.pushTask(this.executor); ++ } ++ } ++ } ++ ++ private static final class DependencyNode { ++ ++ private final Task task; ++ private final DependencyTree tree; ++ ++ // dependency tree fields ++ // (must hold lock on the scheduler to use) ++ // null is the same as empty, we just use it so that we don't allocate the set unless we need to ++ private List<DependencyNode> children; ++ // 0 indicates that this task is considered "awaiting" ++ private int parents; ++ // false -> scheduled and not cancelled ++ // true -> scheduled but cancelled ++ private boolean purged; ++ private final long id; ++ ++ public DependencyNode(final Task task, final DependencyTree tree) { ++ this.task = task; ++ this.id = tree.nextId(); ++ this.tree = tree; ++ } ++ } ++ ++ private static final class Task implements PrioritisedExecutor.PrioritisedTask, Runnable { ++ ++ // task specific fields ++ private final RadiusAwarePrioritisedExecutor scheduler; ++ private final int chunkX; ++ private final int chunkZ; ++ private final int radius; ++ private Runnable run; ++ private PrioritisedExecutor.Priority priority; ++ ++ private DependencyNode dependencyNode; ++ private PrioritisedExecutor.PrioritisedTask queuedTask; ++ ++ private Task(final RadiusAwarePrioritisedExecutor scheduler, final int chunkX, final int chunkZ, final int radius, ++ final Runnable run, final PrioritisedExecutor.Priority priority) { ++ this.scheduler = scheduler; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.radius = radius; ++ this.run = run; ++ this.priority = priority; ++ } ++ ++ private boolean isFiniteRadius() { ++ return this.radius >= 0; ++ } ++ ++ private PrioritisedExecutor.PrioritisedTask pushTask(final PrioritisedExecutor executor) { ++ return this.queuedTask = executor.createTask(this, this.priority); ++ } ++ ++ private void executeTask() { ++ final Runnable run = this.run; ++ this.run = null; ++ run.run(); ++ } ++ ++ private static void scheduleTasks(final List<PrioritisedExecutor.PrioritisedTask> toSchedule) { ++ if (toSchedule != null) { ++ for (int i = 0, len = toSchedule.size(); i < len; ++i) { ++ toSchedule.get(i).queue(); ++ } ++ } ++ } ++ ++ private void returnNode() { ++ final List<PrioritisedExecutor.PrioritisedTask> toSchedule; ++ synchronized (this.scheduler) { ++ final DependencyNode node = this.dependencyNode; ++ this.dependencyNode = null; ++ toSchedule = node.tree.returnNode(node); ++ } ++ ++ scheduleTasks(toSchedule); ++ } ++ ++ @Override ++ public void run() { ++ final Runnable run = this.run; ++ this.run = null; ++ try { ++ run.run(); ++ } finally { ++ this.returnNode(); ++ } ++ } ++ ++ @Override ++ public boolean queue() { ++ final List<PrioritisedExecutor.PrioritisedTask> toSchedule; ++ synchronized (this.scheduler) { ++ if (this.queuedTask != null || this.dependencyNode != null || this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ toSchedule = this.scheduler.queue(this, this.priority); ++ } ++ ++ scheduleTasks(toSchedule); ++ return true; ++ } ++ ++ @Override ++ public boolean cancel() { ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this.scheduler) { ++ if ((task = this.queuedTask) == null) { ++ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ this.priority = PrioritisedExecutor.Priority.COMPLETING; ++ if (this.dependencyNode != null) { ++ this.dependencyNode.purged = true; ++ this.dependencyNode = null; ++ } ++ ++ return true; ++ } ++ } ++ ++ if (task.cancel()) { ++ // must manually return the node ++ this.run = null; ++ this.returnNode(); ++ return true; ++ } ++ return false; ++ } ++ ++ @Override ++ public boolean execute() { ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this.scheduler) { ++ if ((task = this.queuedTask) == null) { ++ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ this.priority = PrioritisedExecutor.Priority.COMPLETING; ++ if (this.dependencyNode != null) { ++ this.dependencyNode.purged = true; ++ this.dependencyNode = null; ++ } ++ // fall through to execution logic ++ } ++ } ++ ++ if (task != null) { ++ // will run the return node logic automatically ++ return task.execute(); ++ } else { ++ // don't run node removal/insertion logic, we aren't actually removed from the dependency tree ++ this.executeTask(); ++ return true; ++ } ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this.scheduler) { ++ if ((task = this.queuedTask) == null) { ++ return this.priority; ++ } ++ } ++ ++ return task.getPriority(); ++ } ++ ++ @Override ++ public boolean setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ final PrioritisedExecutor.PrioritisedTask task; ++ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null; ++ synchronized (this.scheduler) { ++ if ((task = this.queuedTask) == null) { ++ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (this.priority == priority) { ++ return true; ++ } ++ ++ this.priority = priority; ++ if (this.dependencyNode != null) { ++ // need to re-insert node ++ this.dependencyNode.purged = true; ++ this.dependencyNode = null; ++ toSchedule = this.scheduler.queue(this, priority); ++ } ++ } ++ } ++ ++ if (task != null) { ++ return task.setPriority(priority); ++ } ++ ++ scheduleTasks(toSchedule); ++ ++ return true; ++ } ++ ++ @Override ++ public boolean raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ final PrioritisedExecutor.PrioritisedTask task; ++ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null; ++ synchronized (this.scheduler) { ++ if ((task = this.queuedTask) == null) { ++ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (this.priority.isHigherOrEqualPriority(priority)) { ++ return true; ++ } ++ ++ this.priority = priority; ++ if (this.dependencyNode != null) { ++ // need to re-insert node ++ this.dependencyNode.purged = true; ++ this.dependencyNode = null; ++ toSchedule = this.scheduler.queue(this, priority); ++ } ++ } ++ } ++ ++ if (task != null) { ++ return task.raisePriority(priority); ++ } ++ ++ scheduleTasks(toSchedule); ++ ++ return true; ++ } ++ ++ @Override ++ public boolean lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ final PrioritisedExecutor.PrioritisedTask task; ++ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null; ++ synchronized (this.scheduler) { ++ if ((task = this.queuedTask) == null) { ++ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (this.priority.isLowerOrEqualPriority(priority)) { ++ return true; ++ } ++ ++ this.priority = priority; ++ if (this.dependencyNode != null) { ++ // need to re-insert node ++ this.dependencyNode.purged = true; ++ this.dependencyNode = null; ++ toSchedule = this.scheduler.queue(this, priority); ++ } ++ } ++ } ++ ++ if (task != null) { ++ return task.lowerPriority(priority); ++ } ++ ++ scheduleTasks(toSchedule); ++ ++ return true; ++ } ++ } ++} +\ No newline at end of file +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..49774d42f35eeeac5e2b334cce40e6dcca6d01ed +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java +@@ -0,0 +1,139 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiManager; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ImposterProtoChunk; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.status.ChunkStatusTasks; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; ++import java.lang.invoke.VarHandle; ++ ++public final class ChunkFullTask extends ChunkProgressionTask implements Runnable { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkFullTask.class); ++ ++ private final NewChunkHolder chunkHolder; ++ private final ChunkAccess fromChunk; ++ private final PrioritisedExecutor.PrioritisedTask convertToFullTask; ++ ++ public ChunkFullTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, ++ final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ); ++ this.chunkHolder = chunkHolder; ++ this.fromChunk = fromChunk; ++ this.convertToFullTask = scheduler.createChunkTask(chunkX, chunkZ, this, priority); ++ } ++ ++ @Override ++ public ChunkStatus getTargetStatus() { ++ return ChunkStatus.FULL; ++ } ++ ++ @Override ++ public void run() { ++ // See Vanilla ChunkPyramid#LOADING_PYRAMID.FULL for what this function should be doing ++ final LevelChunk chunk; ++ try { ++ // moved from the load from nbt stage into here ++ final PoiChunk poiChunk = this.chunkHolder.getPoiChunk(); ++ if (poiChunk == null) { ++ LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString()); ++ } else { ++ poiChunk.load(); ++ ((ChunkSystemPoiManager)this.world.getPoiManager()).moonrise$checkConsistency(this.fromChunk); ++ } ++ ++ if (this.fromChunk instanceof ImposterProtoChunk wrappedFull) { ++ chunk = wrappedFull.getWrapped(); ++ } else { ++ final ServerLevel world = this.world; ++ final ProtoChunk protoChunk = (ProtoChunk)this.fromChunk; ++ chunk = new LevelChunk(this.world, protoChunk, (final LevelChunk unused) -> { ++ ChunkStatusTasks.postLoadProtoChunk(world, protoChunk.getEntities(), protoChunk.getPos()); // Paper - pass chunk pos ++ }); ++ this.chunkHolder.replaceProtoChunk(new ImposterProtoChunk(chunk, false)); ++ } ++ ++ final NewChunkHolder chunkHolder = this.chunkHolder; ++ ++ chunk.setFullStatus(chunkHolder::getChunkStatus); ++ chunk.runPostLoad(); ++ // Unlike Vanilla, we load the entity chunk here, as we load the NBT in empty status (unlike Vanilla) ++ // This brings entity addition back in line with older versions of the game ++ // Since we load the NBT in the empty status, this will never block for I/O ++ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getOrCreateEntityChunk(this.chunkX, this.chunkZ, false); ++ ++ // we don't need the entitiesInLevel, not sure why it's there ++ chunk.setLoaded(true); ++ chunk.registerAllBlockEntitiesAfterLevelLoad(); ++ chunk.registerTickContainerInLevel(this.world); ++ } catch (final Throwable throwable) { ++ this.complete(null, throwable); ++ return; ++ } ++ this.complete(chunk, null); ++ } ++ ++ protected volatile boolean scheduled; ++ protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkFullTask.class, "scheduled", boolean.class); ++ ++ @Override ++ public boolean isScheduled() { ++ return this.scheduled; ++ } ++ ++ @Override ++ public void schedule() { ++ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkFullTask)this, true)) { ++ throw new IllegalStateException("Cannot double call schedule()"); ++ } ++ this.convertToFullTask.queue(); ++ } ++ ++ @Override ++ public void cancel() { ++ if (this.convertToFullTask.cancel()) { ++ this.complete(null, null); ++ } ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.convertToFullTask.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.convertToFullTask.lowerPriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.convertToFullTask.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.convertToFullTask.raisePriority(priority); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7c2e6752228fac175c4aa97fa3d817b8a938922f +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java +@@ -0,0 +1,181 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.PriorityHolder; ++import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine; ++import ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface; ++import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import org.apache.logging.log4j.LogManager; ++import org.apache.logging.log4j.Logger; ++import java.util.function.BooleanSupplier; ++ ++public final class ChunkLightTask extends ChunkProgressionTask { ++ ++ private static final Logger LOGGER = LogManager.getLogger(); ++ ++ private final ChunkAccess fromChunk; ++ ++ private final LightTaskPriorityHolder priorityHolder; ++ ++ public ChunkLightTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, ++ final ChunkAccess chunk, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ); ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.priorityHolder = new LightTaskPriorityHolder(priority, this); ++ this.fromChunk = chunk; ++ } ++ ++ @Override ++ public boolean isScheduled() { ++ return this.priorityHolder.isScheduled(); ++ } ++ ++ @Override ++ public ChunkStatus getTargetStatus() { ++ return ChunkStatus.LIGHT; ++ } ++ ++ @Override ++ public void schedule() { ++ this.priorityHolder.schedule(); ++ } ++ ++ @Override ++ public void cancel() { ++ this.priorityHolder.cancel(); ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.priorityHolder.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ this.priorityHolder.raisePriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ this.priorityHolder.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ this.priorityHolder.raisePriority(priority); ++ } ++ ++ private static final class LightTaskPriorityHolder extends PriorityHolder { ++ ++ private final ChunkLightTask task; ++ ++ private LightTaskPriorityHolder(final PrioritisedExecutor.Priority priority, final ChunkLightTask task) { ++ super(priority); ++ this.task = task; ++ } ++ ++ @Override ++ protected void cancelScheduled() { ++ final ChunkLightTask task = this.task; ++ task.complete(null, null); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.Priority getScheduledPriority() { ++ final ChunkLightTask task = this.task; ++ return ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine().getServerLightQueue().getPriority(task.chunkX, task.chunkZ); ++ } ++ ++ @Override ++ protected void scheduleTask(final PrioritisedExecutor.Priority priority) { ++ final ChunkLightTask task = this.task; ++ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); ++ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); ++ lightQueue.queueChunkLightTask(new ChunkPos(task.chunkX, task.chunkZ), new LightTask(starLightInterface, task), priority); ++ lightQueue.setPriority(task.chunkX, task.chunkZ, priority); ++ } ++ ++ @Override ++ protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final ChunkLightTask task = this.task; ++ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); ++ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); ++ lightQueue.lowerPriority(task.chunkX, task.chunkZ, priority); ++ } ++ ++ @Override ++ protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final ChunkLightTask task = this.task; ++ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); ++ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); ++ lightQueue.setPriority(task.chunkX, task.chunkZ, priority); ++ } ++ ++ @Override ++ protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final ChunkLightTask task = this.task; ++ final StarLightInterface starLightInterface = ((StarLightLightingProvider)task.world.getChunkSource().getLightEngine()).starlight$getLightEngine(); ++ final StarLightInterface.ServerLightQueue lightQueue = starLightInterface.getServerLightQueue(); ++ lightQueue.raisePriority(task.chunkX, task.chunkZ, priority); ++ } ++ } ++ ++ private static final class LightTask implements BooleanSupplier { ++ ++ private final StarLightInterface lightEngine; ++ private final ChunkLightTask task; ++ ++ public LightTask(final StarLightInterface lightEngine, final ChunkLightTask task) { ++ this.lightEngine = lightEngine; ++ this.task = task; ++ } ++ ++ @Override ++ public boolean getAsBoolean() { ++ final ChunkLightTask task = this.task; ++ // executed on light thread ++ if (!task.priorityHolder.markExecuting()) { ++ // cancelled ++ return false; ++ } ++ ++ try { ++ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(task.fromChunk); ++ ++ if (task.fromChunk.isLightCorrect() && task.fromChunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) { ++ this.lightEngine.forceLoadInChunk(task.fromChunk, emptySections); ++ this.lightEngine.checkChunkEdges(task.chunkX, task.chunkZ); ++ } else { ++ task.fromChunk.setLightCorrect(false); ++ this.lightEngine.lightChunk(task.fromChunk, emptySections); ++ task.fromChunk.setLightCorrect(true); ++ } ++ // we need to advance status ++ if (task.fromChunk instanceof ProtoChunk chunk && chunk.getPersistedStatus() == ChunkStatus.LIGHT.getParent()) { ++ chunk.setPersistedStatus(ChunkStatus.LIGHT); ++ } ++ } catch (final Throwable thr) { ++ LOGGER.fatal( ++ "Failed to light chunk " + task.fromChunk.getPos().toString() ++ + " in world '" + WorldUtil.getWorldName(this.lightEngine.getWorld()) + "'", thr ++ ); ++ ++ task.complete(null, thr); ++ ++ return true; ++ } ++ ++ task.complete(task.fromChunk, null); ++ return true; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13091425f8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java +@@ -0,0 +1,487 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemConverters; ++import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystemFeatures; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.poi.PoiChunk; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import net.minecraft.world.level.chunk.UpgradeData; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.storage.ChunkSerializer; ++import net.minecraft.world.level.levelgen.blending.BlendingData; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; ++import java.lang.invoke.VarHandle; ++import java.util.Map; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.function.Consumer; ++ ++public final class ChunkLoadTask extends ChunkProgressionTask { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkLoadTask.class); ++ ++ private final NewChunkHolder chunkHolder; ++ private final ChunkDataLoadTask loadTask; ++ ++ private volatile boolean cancelled; ++ private NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; ++ private NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; ++ private GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> loadResult; ++ private final AtomicInteger taskCountToComplete = new AtomicInteger(3); // one for poi, one for entity, and one for chunk data ++ ++ public ChunkLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, ++ final NewChunkHolder chunkHolder, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ); ++ this.chunkHolder = chunkHolder; ++ this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority); ++ this.loadTask.addCallback((final GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> result) -> { ++ ChunkLoadTask.this.loadResult = result; // must be before getAndDecrement ++ ChunkLoadTask.this.tryCompleteLoad(); ++ }); ++ } ++ ++ private void tryCompleteLoad() { ++ final int count = this.taskCountToComplete.decrementAndGet(); ++ if (count == 0) { ++ final GenericDataLoadTask.TaskResult<ChunkAccess, Throwable> result = this.cancelled ? null : this.loadResult; // only after the getAndDecrement ++ ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right()); ++ } else if (count < 0) { ++ throw new IllegalStateException("Called tryCompleteLoad() too many times"); ++ } ++ } ++ ++ @Override ++ public ChunkStatus getTargetStatus() { ++ return ChunkStatus.EMPTY; ++ } ++ ++ private boolean scheduled; ++ ++ @Override ++ public boolean isScheduled() { ++ return this.scheduled; ++ } ++ ++ @Override ++ public void schedule() { ++ final NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; ++ final NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; ++ ++ final Consumer<GenericDataLoadTask.TaskResult<?, ?>> scheduleLoadTask = (final GenericDataLoadTask.TaskResult<?, ?> result) -> { ++ ChunkLoadTask.this.tryCompleteLoad(); ++ }; ++ ++ // NOTE: it is IMPOSSIBLE for getOrLoadEntityData/getOrLoadPoiData to complete synchronously, because ++ // they must schedule a task to off main or to on main to complete ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ if (this.scheduled) { ++ throw new IllegalStateException("schedule() called twice"); ++ } ++ this.scheduled = true; ++ if (this.cancelled) { ++ return; ++ } ++ if (!this.chunkHolder.isEntityChunkNBTLoaded()) { ++ entityLoadTask = this.chunkHolder.getOrLoadEntityData((Consumer)scheduleLoadTask); ++ } else { ++ entityLoadTask = null; ++ this.tryCompleteLoad(); ++ } ++ ++ if (!this.chunkHolder.isPoiChunkLoaded()) { ++ poiLoadTask = this.chunkHolder.getOrLoadPoiData((Consumer)scheduleLoadTask); ++ } else { ++ poiLoadTask = null; ++ this.tryCompleteLoad(); ++ } ++ ++ this.entityLoadTask = entityLoadTask; ++ this.poiLoadTask = poiLoadTask; ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ ++ if (entityLoadTask != null) { ++ entityLoadTask.schedule(); ++ } ++ ++ if (poiLoadTask != null) { ++ poiLoadTask.schedule(); ++ } ++ ++ this.loadTask.schedule(false); ++ } ++ ++ @Override ++ public void cancel() { ++ // must be before load task access, so we can synchronise with the writes to the fields ++ final boolean scheduled; ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ // must read field here, as it may be written later conucrrently - ++ // we need to know if we scheduled _before_ cancellation ++ scheduled = this.scheduled; ++ this.cancelled = true; ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ ++ /* ++ Note: The entityLoadTask/poiLoadTask do not complete when cancelled, ++ so we need to manually try to complete in those cases ++ It is also important to note that we set the cancelled field first, just in case ++ the chunk load task attempts to complete with a non-null value ++ */ ++ ++ if (scheduled) { ++ // since we scheduled, we need to cancel the tasks ++ if (this.entityLoadTask != null) { ++ if (this.entityLoadTask.cancel()) { ++ this.tryCompleteLoad(); ++ } ++ } ++ if (this.poiLoadTask != null) { ++ if (this.poiLoadTask.cancel()) { ++ this.tryCompleteLoad(); ++ } ++ } ++ } else { ++ // since nothing was scheduled, we need to decrement the task count here ourselves ++ ++ // for entity load task ++ this.tryCompleteLoad(); ++ ++ // for poi load task ++ this.tryCompleteLoad(); ++ } ++ this.loadTask.cancel(); ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.loadTask.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); ++ if (entityLoad != null) { ++ entityLoad.lowerPriority(priority); ++ } ++ ++ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); ++ ++ if (poiLoad != null) { ++ poiLoad.lowerPriority(priority); ++ } ++ ++ this.loadTask.lowerPriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); ++ if (entityLoad != null) { ++ entityLoad.setPriority(priority); ++ } ++ ++ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); ++ ++ if (poiLoad != null) { ++ poiLoad.setPriority(priority); ++ } ++ ++ this.loadTask.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); ++ if (entityLoad != null) { ++ entityLoad.raisePriority(priority); ++ } ++ ++ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); ++ ++ if (poiLoad != null) { ++ poiLoad.raisePriority(priority); ++ } ++ ++ this.loadTask.raisePriority(priority); ++ } ++ ++ protected static abstract class CallbackDataLoadTask<OnMain,FinalCompletion> extends GenericDataLoadTask<OnMain,FinalCompletion> { ++ ++ private TaskResult<FinalCompletion, Throwable> result; ++ private final MultiThreadedQueue<Consumer<TaskResult<FinalCompletion, Throwable>>> waiters = new MultiThreadedQueue<>(); ++ ++ protected volatile boolean completed; ++ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(CallbackDataLoadTask.class, "completed", boolean.class); ++ ++ protected CallbackDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final RegionFileIOThread.RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, type, priority); ++ } ++ ++ public void addCallback(final Consumer<TaskResult<FinalCompletion, Throwable>> consumer) { ++ if (!this.waiters.add(consumer)) { ++ try { ++ consumer.accept(this.result); ++ } catch (final Throwable throwable) { ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Consumer", ChunkTaskScheduler.stringIfNull(consumer), ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.result.right()), ++ "CallbackDataLoadTask impl", this.getClass().getName() ++ ), throwable); ++ } ++ } ++ } ++ ++ @Override ++ protected void onComplete(final TaskResult<FinalCompletion, Throwable> result) { ++ if ((boolean)COMPLETED_HANDLE.getAndSet((CallbackDataLoadTask)this, (boolean)true)) { ++ throw new IllegalStateException("Already completed"); ++ } ++ this.result = result; ++ Consumer<TaskResult<FinalCompletion, Throwable>> consumer; ++ while ((consumer = this.waiters.pollOrBlockAdds()) != null) { ++ try { ++ consumer.accept(result); ++ } catch (final Throwable throwable) { ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Consumer", ChunkTaskScheduler.stringIfNull(consumer), ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(result.right()), ++ "CallbackDataLoadTask impl", this.getClass().getName() ++ ), throwable); ++ return; ++ } ++ } ++ } ++ } ++ ++ private static final class ChunkDataLoadTask extends CallbackDataLoadTask<CompoundTag, ChunkAccess> { ++ private ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.CHUNK_DATA, priority); ++ } ++ ++ @Override ++ protected boolean hasOffMain() { ++ return true; ++ } ++ ++ @Override ++ protected boolean hasOnMain() { ++ return true; ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return this.scheduler.loadExecutor.createTask(run, priority); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return this.scheduler.createChunkTask(this.chunkX, this.chunkZ, run, priority); ++ } ++ ++ @Override ++ protected TaskResult<ChunkAccess, Throwable> completeOnMainOffMain(final CompoundTag data, final Throwable throwable) { ++ if (throwable != null) { ++ return new TaskResult<>(null, throwable); ++ } ++ if (data == null) { ++ return new TaskResult<>(this.getEmptyChunk(), null); ++ } ++ ++ if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) { ++ return this.deserialize(data); ++ } ++ // need to deserialize on main thread ++ return null; ++ } ++ ++ private ProtoChunk getEmptyChunk() { ++ return new ProtoChunk( ++ new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world, ++ this.world.registryAccess().registryOrThrow(Registries.BIOME), (BlendingData)null ++ ); ++ } ++ ++ @Override ++ protected TaskResult<CompoundTag, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) { ++ if (throwable != null) { ++ LOGGER.error("Failed to load chunk data for task: " + this.toString() + ", chunk data will be lost", throwable); ++ return new TaskResult<>(null, null); ++ } ++ ++ if (data == null) { ++ return new TaskResult<>(null, null); ++ } ++ ++ try { ++ // run converters ++ final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data, new net.minecraft.world.level.ChunkPos(this.chunkX, this.chunkZ)); ++ ++ return new TaskResult<>(converted, null); ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2); ++ return new TaskResult<>(null, null); ++ } ++ } ++ ++ private TaskResult<ChunkAccess, Throwable> deserialize(final CompoundTag data) { ++ try { ++ final ChunkAccess deserialized = ChunkSerializer.read( ++ this.world, this.world.getPoiManager(), this.world.getChunkSource().chunkMap.storageInfo(), new ChunkPos(this.chunkX, this.chunkZ), data ++ ); ++ return new TaskResult<>(deserialized, null); ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2); ++ return new TaskResult<>(this.getEmptyChunk(), null); ++ } ++ } ++ ++ @Override ++ protected TaskResult<ChunkAccess, Throwable> runOnMain(final CompoundTag data, final Throwable throwable) { ++ // data != null && throwable == null ++ if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) { ++ throw new UnsupportedOperationException(); ++ } ++ return this.deserialize(data); ++ } ++ } ++ ++ public static final class PoiDataLoadTask extends CallbackDataLoadTask<PoiChunk, PoiChunk> { ++ ++ public PoiDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.POI_DATA, priority); ++ } ++ ++ @Override ++ protected boolean hasOffMain() { ++ return true; ++ } ++ ++ @Override ++ protected boolean hasOnMain() { ++ return false; ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return this.scheduler.loadExecutor.createTask(run, priority); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult<PoiChunk, Throwable> completeOnMainOffMain(final PoiChunk data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult<PoiChunk, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) { ++ if (throwable != null) { ++ LOGGER.error("Failed to load poi data for task: " + this.toString() + ", poi data will be lost", throwable); ++ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); ++ } ++ ++ if (data == null || data.isEmpty()) { ++ // nothing to do ++ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); ++ } ++ ++ try { ++ // run converters ++ final CompoundTag converted = ChunkSystemConverters.convertPoiCompoundTag(data, this.world); ++ ++ // now we need to parse it ++ return new TaskResult<>(PoiChunk.parse(this.world, this.chunkX, this.chunkZ, converted), null); ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed to run parse poi data for task: " + this.toString() + ", poi data will be lost", thr2); ++ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); ++ } ++ } ++ ++ @Override ++ protected TaskResult<PoiChunk, Throwable> runOnMain(final PoiChunk data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ } ++ ++ public static final class EntityDataLoadTask extends CallbackDataLoadTask<CompoundTag, CompoundTag> { ++ ++ public EntityDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, priority); ++ } ++ ++ @Override ++ protected boolean hasOffMain() { ++ return true; ++ } ++ ++ @Override ++ protected boolean hasOnMain() { ++ return false; ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return this.scheduler.loadExecutor.createTask(run, priority); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult<CompoundTag, Throwable> completeOnMainOffMain(final CompoundTag data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult<CompoundTag, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) { ++ if (throwable != null) { ++ LOGGER.error("Failed to load entity data for task: " + this.toString() + ", entity data will be lost", throwable); ++ return new TaskResult<>(null, null); ++ } ++ ++ if (data == null || data.isEmpty()) { ++ // nothing to do ++ return new TaskResult<>(null, null); ++ } ++ ++ try { ++ return new TaskResult<>(ChunkSystemConverters.convertEntityChunkCompoundTag(data, this.world), null); ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed to run converters for entity data for task: " + this.toString() + ", entity data will be lost", thr2); ++ return new TaskResult<>(null, thr2); ++ } ++ } ++ ++ @Override ++ protected TaskResult<CompoundTag, Throwable> runOnMain(final CompoundTag data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..70e900b0f9c131900bf8b3f3ecbfbd5df5361205 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java +@@ -0,0 +1,101 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import java.lang.invoke.VarHandle; ++import java.util.Map; ++import java.util.function.BiConsumer; ++ ++public abstract class ChunkProgressionTask { ++ ++ private final MultiThreadedQueue<BiConsumer<ChunkAccess, Throwable>> waiters = new MultiThreadedQueue<>(); ++ private ChunkAccess completedChunk; ++ private Throwable completedThrowable; ++ ++ protected final ChunkTaskScheduler scheduler; ++ protected final ServerLevel world; ++ protected final int chunkX; ++ protected final int chunkZ; ++ ++ protected volatile boolean completed; ++ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(ChunkProgressionTask.class, "completed", boolean.class); ++ ++ protected ChunkProgressionTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ) { ++ this.scheduler = scheduler; ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ } ++ ++ // Used only for debug json ++ public abstract boolean isScheduled(); ++ ++ // Note: It is the responsibility of the task to set the chunk's status once it has completed ++ public abstract ChunkStatus getTargetStatus(); ++ ++ /* Only executed once */ ++ /* Implementations must be prepared to handle cases where cancel() is called before schedule() */ ++ public abstract void schedule(); ++ ++ /* May be called multiple times */ ++ public abstract void cancel(); ++ ++ public abstract PrioritisedExecutor.Priority getPriority(); ++ ++ /* Schedule lock is always held for the priority update calls */ ++ ++ public abstract void lowerPriority(final PrioritisedExecutor.Priority priority); ++ ++ public abstract void setPriority(final PrioritisedExecutor.Priority priority); ++ ++ public abstract void raisePriority(final PrioritisedExecutor.Priority priority); ++ ++ public final void onComplete(final BiConsumer<ChunkAccess, Throwable> onComplete) { ++ if (!this.waiters.add(onComplete)) { ++ try { ++ onComplete.accept(this.completedChunk, this.completedThrowable); ++ } catch (final Throwable throwable) { ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Consumer", ChunkTaskScheduler.stringIfNull(onComplete), ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.completedThrowable) ++ ), throwable); ++ } ++ } ++ } ++ ++ protected final void complete(final ChunkAccess chunk, final Throwable throwable) { ++ try { ++ this.complete0(chunk, throwable); ++ } catch (final Throwable thr2) { ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable) ++ ), thr2); ++ } ++ } ++ ++ private void complete0(final ChunkAccess chunk, final Throwable throwable) { ++ if ((boolean)COMPLETED_HANDLE.getAndSet((ChunkProgressionTask)this, (boolean)true)) { ++ throw new IllegalStateException("Already completed"); ++ } ++ this.completedChunk = chunk; ++ this.completedThrowable = throwable; ++ ++ BiConsumer<ChunkAccess, Throwable> consumer; ++ while ((consumer = this.waiters.pollOrBlockAdds()) != null) { ++ consumer.accept(chunk, throwable); ++ } ++ } ++ ++ @Override ++ public String toString() { ++ return "ChunkProgressionTask{class: " + this.getClass().getName() + ", for world: " + WorldUtil.getWorldName(this.world) + ++ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() + ++ ", status: " + this.getTargetStatus().toString() + ", scheduled: " + this.isScheduled() + "}"; ++ } ++} +\ No newline at end of file +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2c17d5589f15f1155be08be670d29acbe954a8fa +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java +@@ -0,0 +1,217 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.GenerationChunkHolder; ++import net.minecraft.server.level.ServerChunkCache; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.StaticCache2D; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import net.minecraft.world.level.chunk.status.ChunkPyramid; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.status.WorldGenContext; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; ++import java.lang.invoke.VarHandle; ++import java.util.List; ++import java.util.Map; ++import java.util.concurrent.CompletableFuture; ++ ++public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask implements Runnable { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkUpgradeGenericStatusTask.class); ++ ++ private final ChunkAccess fromChunk; ++ private final ChunkStatus fromStatus; ++ private final ChunkStatus toStatus; ++ private final StaticCache2D<GenerationChunkHolder> neighbours; ++ ++ private final PrioritisedExecutor.PrioritisedTask generateTask; ++ ++ public ChunkUpgradeGenericStatusTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final ChunkAccess chunk, final StaticCache2D<GenerationChunkHolder> neighbours, ++ final ChunkStatus toStatus, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ); ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.fromChunk = chunk; ++ this.fromStatus = chunk.getPersistedStatus(); ++ this.toStatus = toStatus; ++ this.neighbours = neighbours; ++ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isParallelCapable()) { ++ this.generateTask = this.scheduler.parallelGenExecutor.createTask(this, priority); ++ } else { ++ final int writeRadius = ((ChunkSystemChunkStatus)this.toStatus).moonrise$getWriteRadius(); ++ if (writeRadius < 0) { ++ this.generateTask = this.scheduler.radiusAwareScheduler.createInfiniteRadiusTask(this, priority); ++ } else { ++ this.generateTask = this.scheduler.radiusAwareScheduler.createTask(chunkX, chunkZ, writeRadius, this, priority); ++ } ++ } ++ } ++ ++ @Override ++ public ChunkStatus getTargetStatus() { ++ return this.toStatus; ++ } ++ ++ private boolean isEmptyTask() { ++ // must use fromStatus here to avoid any race condition with run() overwriting the status ++ final boolean generation = !this.fromStatus.isOrAfter(this.toStatus); ++ return (generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) || (!generation && ((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus()); ++ } ++ ++ @Override ++ public void run() { ++ final ChunkAccess chunk = this.fromChunk; ++ ++ final ServerChunkCache serverChunkCache = this.world.getChunkSource(); ++ final ChunkMap chunkMap = serverChunkCache.chunkMap; ++ ++ final CompletableFuture<ChunkAccess> completeFuture; ++ ++ final boolean generation; ++ boolean completing = false; ++ ++ // note: should optimise the case where the chunk does not need to execute the status, because ++ // schedule() calls this synchronously if it will run through that path ++ ++ final WorldGenContext ctx = chunkMap.worldGenContext; ++ try { ++ generation = !chunk.getPersistedStatus().isOrAfter(this.toStatus); ++ if (generation) { ++ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyGenStatus()) { ++ if (chunk instanceof ProtoChunk) { ++ ((ProtoChunk)chunk).setPersistedStatus(this.toStatus); ++ } ++ completing = true; ++ this.complete(chunk, null); ++ return; ++ } ++ completeFuture = ChunkPyramid.GENERATION_PYRAMID.getStepTo(this.toStatus).apply(ctx, this.neighbours, this.fromChunk) ++ .whenComplete((final ChunkAccess either, final Throwable throwable) -> { ++ if (either instanceof ProtoChunk proto) { ++ proto.setPersistedStatus(ChunkUpgradeGenericStatusTask.this.toStatus); ++ } ++ } ++ ); ++ } else { ++ if (((ChunkSystemChunkStatus)this.toStatus).moonrise$isEmptyLoadStatus()) { ++ completing = true; ++ this.complete(chunk, null); ++ return; ++ } ++ completeFuture = ChunkPyramid.LOADING_PYRAMID.getStepTo(this.toStatus).apply(ctx, this.neighbours, this.fromChunk); ++ } ++ } catch (final Throwable throwable) { ++ if (!completing) { ++ this.complete(null, throwable); ++ return; ++ } ++ ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Target status", ChunkTaskScheduler.stringIfNull(this.toStatus), ++ "From status", ChunkTaskScheduler.stringIfNull(this.fromStatus), ++ "Generation task", this ++ ), throwable); ++ ++ LOGGER.error( ++ "Failed to complete status for chunk: status:" + this.toStatus + ", chunk: (" + this.chunkX + ++ "," + this.chunkZ + "), world: " + WorldUtil.getWorldName(this.world), ++ throwable ++ ); ++ ++ return; ++ } ++ ++ if (!completeFuture.isDone() && !((ChunkSystemChunkStatus)this.toStatus).moonrise$getWarnedAboutNoImmediateComplete().getAndSet(true)) { ++ LOGGER.warn("Future status not complete after scheduling: " + this.toStatus.toString() + ", generate: " + generation); ++ } ++ ++ final ChunkAccess newChunk; ++ ++ try { ++ newChunk = completeFuture.join(); ++ } catch (final Throwable throwable) { ++ this.complete(null, throwable); ++ return; ++ } ++ ++ if (newChunk == null) { ++ this.complete(null, ++ new IllegalStateException( ++ "Chunk for status: " + ChunkUpgradeGenericStatusTask.this.toStatus.toString() ++ + ", generation: " + generation + " should not be null! Future: " + completeFuture ++ ).fillInStackTrace() ++ ); ++ return; ++ } ++ ++ this.complete(newChunk, null); ++ } ++ ++ private volatile boolean scheduled; ++ private static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkUpgradeGenericStatusTask.class, "scheduled", boolean.class); ++ ++ @Override ++ public boolean isScheduled() { ++ return this.scheduled; ++ } ++ ++ @Override ++ public void schedule() { ++ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkUpgradeGenericStatusTask)this, true)) { ++ throw new IllegalStateException("Cannot double call schedule()"); ++ } ++ if (this.isEmptyTask()) { ++ if (this.generateTask.cancel()) { ++ this.run(); ++ } ++ } else { ++ this.generateTask.queue(); ++ } ++ } ++ ++ @Override ++ public void cancel() { ++ if (this.generateTask.cancel()) { ++ this.complete(null, null); ++ } ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.generateTask.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.generateTask.lowerPriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.generateTask.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.generateTask.raisePriority(priority); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be314b876c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java +@@ -0,0 +1,673 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task; ++ ++import ca.spottedleaf.concurrentutil.completable.Completable; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ServerLevel; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; ++import java.lang.invoke.VarHandle; ++import java.util.Map; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.function.BiConsumer; ++ ++public abstract class GenericDataLoadTask<OnMain,FinalCompletion> { ++ ++ private static final Logger LOGGER = LoggerFactory.getLogger(GenericDataLoadTask.class); ++ ++ protected static final CompoundTag CANCELLED_DATA = new CompoundTag(); ++ ++ // reference count is the upper 32 bits ++ protected final AtomicLong stageAndReferenceCount = new AtomicLong(STAGE_NOT_STARTED); ++ ++ protected static final long STAGE_MASK = 0xFFFFFFFFL; ++ protected static final long STAGE_CANCELLED = 0xFFFFFFFFL; ++ protected static final long STAGE_NOT_STARTED = 0L; ++ protected static final long STAGE_LOADING = 1L; ++ protected static final long STAGE_PROCESSING = 2L; ++ protected static final long STAGE_COMPLETED = 3L; ++ ++ // for loading data off disk ++ protected final LoadDataFromDiskTask loadDataFromDiskTask; ++ // processing off-main ++ protected final PrioritisedExecutor.PrioritisedTask processOffMain; ++ // processing on-main ++ protected final PrioritisedExecutor.PrioritisedTask processOnMain; ++ ++ protected final ChunkTaskScheduler scheduler; ++ protected final ServerLevel world; ++ protected final int chunkX; ++ protected final int chunkZ; ++ protected final RegionFileIOThread.RegionFileType type; ++ ++ public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final RegionFileIOThread.RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ this.scheduler = scheduler; ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.type = type; ++ ++ final ProcessOnMainTask mainTask; ++ if (this.hasOnMain()) { ++ mainTask = new ProcessOnMainTask(); ++ this.processOnMain = this.createOnMain(mainTask, priority); ++ } else { ++ mainTask = null; ++ this.processOnMain = null; ++ } ++ ++ final ProcessOffMainTask offMainTask; ++ if (this.hasOffMain()) { ++ offMainTask = new ProcessOffMainTask(mainTask); ++ this.processOffMain = this.createOffMain(offMainTask, priority); ++ } else { ++ offMainTask = null; ++ this.processOffMain = null; ++ } ++ ++ if (this.processOffMain == null && this.processOnMain == null) { ++ throw new IllegalStateException("Illegal class implementation: " + this.getClass().getName() + ", should be able to schedule at least one task!"); ++ } ++ ++ this.loadDataFromDiskTask = new LoadDataFromDiskTask(world, chunkX, chunkZ, type, new DataLoadCallback(offMainTask, mainTask), priority); ++ } ++ ++ public static final record TaskResult<L, R>(L left, R right) {} ++ ++ protected abstract boolean hasOffMain(); ++ ++ protected abstract boolean hasOnMain(); ++ ++ protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority); ++ ++ protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority); ++ ++ protected abstract TaskResult<OnMain, Throwable> runOffMain(final CompoundTag data, final Throwable throwable); ++ ++ protected abstract TaskResult<FinalCompletion, Throwable> runOnMain(final OnMain data, final Throwable throwable); ++ ++ protected abstract void onComplete(final TaskResult<FinalCompletion,Throwable> result); ++ ++ protected abstract TaskResult<FinalCompletion, Throwable> completeOnMainOffMain(final OnMain data, final Throwable throwable); ++ ++ @Override ++ public String toString() { ++ return "GenericDataLoadTask{class: " + this.getClass().getName() + ", world: " + WorldUtil.getWorldName(this.world) + ++ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() + ++ ", type: " + this.type.toString() + "}"; ++ } ++ ++ public PrioritisedExecutor.Priority getPriority() { ++ if (this.processOnMain != null) { ++ return this.processOnMain.getPriority(); ++ } else { ++ return this.processOffMain.getPriority(); ++ } ++ } ++ ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ // can't lower I/O tasks, we don't know what they affect ++ if (this.processOffMain != null) { ++ this.processOffMain.lowerPriority(priority); ++ } ++ if (this.processOnMain != null) { ++ this.processOnMain.lowerPriority(priority); ++ } ++ } ++ ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ // can't lower I/O tasks, we don't know what they affect ++ this.loadDataFromDiskTask.raisePriority(priority); ++ if (this.processOffMain != null) { ++ this.processOffMain.setPriority(priority); ++ } ++ if (this.processOnMain != null) { ++ this.processOnMain.setPriority(priority); ++ } ++ } ++ ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ // can't lower I/O tasks, we don't know what they affect ++ this.loadDataFromDiskTask.raisePriority(priority); ++ if (this.processOffMain != null) { ++ this.processOffMain.raisePriority(priority); ++ } ++ if (this.processOnMain != null) { ++ this.processOnMain.raisePriority(priority); ++ } ++ } ++ ++ // returns whether scheduleNow() needs to be called ++ public boolean schedule(final boolean delay) { ++ if (this.stageAndReferenceCount.get() != STAGE_NOT_STARTED || ++ !this.stageAndReferenceCount.compareAndSet(STAGE_NOT_STARTED, (1L << 32) | STAGE_LOADING)) { ++ // try and increment reference count ++ int failures = 0; ++ for (long curr = this.stageAndReferenceCount.get();;) { ++ if ((curr & STAGE_MASK) == STAGE_CANCELLED || (curr & STAGE_MASK) == STAGE_COMPLETED) { ++ // cancelled or completed, nothing to do here ++ return false; ++ } ++ ++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, curr + (1L << 32)))) { ++ // successful ++ return false; ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ if (!delay) { ++ this.scheduleNow(); ++ return false; ++ } ++ return true; ++ } ++ ++ public void scheduleNow() { ++ this.loadDataFromDiskTask.schedule(); // will schedule the rest ++ } ++ ++ // assumes the current stage cannot be completed ++ // returns false if cancelled, returns true if can proceed ++ private boolean advanceStage(final long expect, final long to) { ++ int failures = 0; ++ for (long curr = this.stageAndReferenceCount.get();;) { ++ if ((curr & STAGE_MASK) != expect) { ++ // must be cancelled ++ return false; ++ } ++ ++ final long newVal = (curr & ~STAGE_MASK) | to; ++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { ++ return true; ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public boolean cancel() { ++ int failures = 0; ++ for (long curr = this.stageAndReferenceCount.get();;) { ++ if ((curr & STAGE_MASK) == STAGE_COMPLETED || (curr & STAGE_MASK) == STAGE_CANCELLED) { ++ return false; ++ } ++ ++ if ((curr & STAGE_MASK) == STAGE_NOT_STARTED || (curr & ~STAGE_MASK) == (1L << 32)) { ++ // no other references, so we can cancel ++ final long newVal = STAGE_CANCELLED; ++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { ++ this.loadDataFromDiskTask.cancel(); ++ if (this.processOffMain != null) { ++ this.processOffMain.cancel(); ++ } ++ if (this.processOnMain != null) { ++ this.processOnMain.cancel(); ++ } ++ this.onComplete(null); ++ return true; ++ } ++ } else { ++ if ((curr & ~STAGE_MASK) == (0L << 32)) { ++ throw new IllegalStateException("Reference count cannot be zero here"); ++ } ++ // just decrease the reference count ++ final long newVal = curr - (1L << 32); ++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { ++ return false; ++ } ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ private final class DataLoadCallback implements BiConsumer<CompoundTag, Throwable> { ++ ++ private final ProcessOffMainTask offMainTask; ++ private final ProcessOnMainTask onMainTask; ++ ++ public DataLoadCallback(final ProcessOffMainTask offMainTask, final ProcessOnMainTask onMainTask) { ++ this.offMainTask = offMainTask; ++ this.onMainTask = onMainTask; ++ } ++ ++ @Override ++ public void accept(final CompoundTag compoundTag, final Throwable throwable) { ++ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) { ++ // don't try to schedule further ++ return; ++ } ++ ++ try { ++ if (compoundTag == CANCELLED_DATA) { ++ // cancelled, except this isn't possible ++ LOGGER.error("Data callback says cancelled, but stage does not?"); ++ return; ++ } ++ ++ // get off of the regionfile callback ASAP, no clue what locks are held right now... ++ if (GenericDataLoadTask.this.processOffMain != null) { ++ this.offMainTask.data = compoundTag; ++ this.offMainTask.throwable = throwable; ++ GenericDataLoadTask.this.processOffMain.queue(); ++ return; ++ } else { ++ // no off-main task, so go straight to main ++ this.onMainTask.data = (OnMain)compoundTag; ++ this.onMainTask.throwable = throwable; ++ GenericDataLoadTask.this.processOnMain.queue(); ++ } ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed I/O callback for task: " + GenericDataLoadTask.this.toString(), thr2); ++ GenericDataLoadTask.this.scheduler.unrecoverableChunkSystemFailure( ++ GenericDataLoadTask.this.chunkX, GenericDataLoadTask.this.chunkZ, Map.of( ++ "Callback throwable", ChunkTaskScheduler.stringIfNull(throwable) ++ ), thr2 ++ ); ++ } ++ } ++ } ++ ++ private final class ProcessOffMainTask implements Runnable { ++ ++ private CompoundTag data; ++ private Throwable throwable; ++ private final ProcessOnMainTask schedule; ++ ++ public ProcessOffMainTask(final ProcessOnMainTask schedule) { ++ this.schedule = schedule; ++ } ++ ++ @Override ++ public void run() { ++ if (!GenericDataLoadTask.this.advanceStage(STAGE_LOADING, this.schedule == null ? STAGE_COMPLETED : STAGE_PROCESSING)) { ++ // cancelled ++ return; ++ } ++ final TaskResult<OnMain, Throwable> newData = GenericDataLoadTask.this.runOffMain(this.data, this.throwable); ++ ++ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) { ++ // don't try to schedule further ++ return; ++ } ++ ++ if (this.schedule != null) { ++ final TaskResult<FinalCompletion, Throwable> syncComplete = GenericDataLoadTask.this.completeOnMainOffMain(newData.left, newData.right); ++ ++ if (syncComplete != null) { ++ if (GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) { ++ GenericDataLoadTask.this.onComplete(syncComplete); ++ } // else: cancelled ++ return; ++ } ++ ++ this.schedule.data = newData.left; ++ this.schedule.throwable = newData.right; ++ ++ GenericDataLoadTask.this.processOnMain.queue(); ++ } else { ++ GenericDataLoadTask.this.onComplete((TaskResult<FinalCompletion, Throwable>)newData); ++ } ++ } ++ } ++ ++ private final class ProcessOnMainTask implements Runnable { ++ ++ private OnMain data; ++ private Throwable throwable; ++ ++ @Override ++ public void run() { ++ if (!GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) { ++ // cancelled ++ return; ++ } ++ final TaskResult<FinalCompletion, Throwable> result = GenericDataLoadTask.this.runOnMain(this.data, this.throwable); ++ ++ GenericDataLoadTask.this.onComplete(result); ++ } ++ } ++ ++ protected static final class LoadDataFromDiskTask { ++ ++ private volatile int priority; ++ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(LoadDataFromDiskTask.class, "priority", int.class); ++ ++ private static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 0; ++ private static final int PRIORITY_LOAD_SCHEDULED = Integer.MIN_VALUE >>> 1; ++ private static final int PRIORITY_UNLOAD_SCHEDULED = Integer.MIN_VALUE >>> 2; ++ ++ private static final int PRIORITY_FLAGS = ~Character.MAX_VALUE; ++ ++ private final int getPriorityVolatile() { ++ return (int)PRIORITY_HANDLE.getVolatile((LoadDataFromDiskTask)this); ++ } ++ ++ private final int compareAndExchangePriorityVolatile(final int expect, final int update) { ++ return (int)PRIORITY_HANDLE.compareAndExchange((LoadDataFromDiskTask)this, (int)expect, (int)update); ++ } ++ ++ private final int getAndOrPriorityVolatile(final int val) { ++ return (int)PRIORITY_HANDLE.getAndBitwiseOr((LoadDataFromDiskTask)this, (int)val); ++ } ++ ++ private final void setPriorityPlain(final int val) { ++ PRIORITY_HANDLE.set((LoadDataFromDiskTask)this, (int)val); ++ } ++ ++ private final ServerLevel world; ++ private final int chunkX; ++ private final int chunkZ; ++ ++ private final RegionFileIOThread.RegionFileType type; ++ private Cancellable dataLoadTask; ++ private Cancellable dataUnloadCancellable; ++ private DelayedPrioritisedTask dataUnloadTask; ++ ++ private final BiConsumer<CompoundTag, Throwable> onComplete; ++ private final AtomicBoolean scheduled = new AtomicBoolean(); ++ ++ // onComplete should be caller sensitive, it may complete synchronously with schedule() - which does ++ // hold a priority lock. ++ public LoadDataFromDiskTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileIOThread.RegionFileType type, ++ final BiConsumer<CompoundTag, Throwable> onComplete, ++ final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.type = type; ++ this.onComplete = onComplete; ++ this.setPriorityPlain(priority.priority); ++ } ++ ++ private void complete(final CompoundTag data, final Throwable throwable) { ++ try { ++ this.onComplete.accept(data, throwable); ++ } catch (final Throwable thr2) { ++ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable), ++ "Regionfile type", ChunkTaskScheduler.stringIfNull(this.type) ++ ), thr2); ++ } ++ } ++ ++ private boolean markExecuting() { ++ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0; ++ } ++ ++ private boolean isMarkedExecuted() { ++ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0; ++ } ++ ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ return; ++ } ++ ++ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { ++ RegionFileIOThread.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); ++ return; ++ } ++ ++ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { ++ if (this.dataUnloadTask != null) { ++ this.dataUnloadTask.lowerPriority(priority); ++ } ++ // no return - we need to propagate priority ++ } ++ ++ if (!priority.isHigherPriority(curr & ~PRIORITY_FLAGS)) { ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ return; ++ } ++ ++ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { ++ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); ++ return; ++ } ++ ++ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { ++ if (this.dataUnloadTask != null) { ++ this.dataUnloadTask.setPriority(priority); ++ } ++ // no return - we need to propagate priority ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ return; ++ } ++ ++ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { ++ RegionFileIOThread.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority); ++ return; ++ } ++ ++ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { ++ if (this.dataUnloadTask != null) { ++ this.dataUnloadTask.raisePriority(priority); ++ } ++ // no return - we need to propagate priority ++ } ++ ++ if (!priority.isLowerPriority(curr & ~PRIORITY_FLAGS)) { ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public void cancel() { ++ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed already ++ return; ++ } ++ ++ // OK if we miss the field read, the task cannot complete if the cancelled bit is set and ++ // the write to dataLoadTask will check for the cancelled bit ++ if (this.dataUnloadCancellable != null) { ++ this.dataUnloadCancellable.cancel(); ++ } ++ ++ if (this.dataLoadTask != null) { ++ this.dataLoadTask.cancel(); ++ } ++ ++ this.complete(CANCELLED_DATA, null); ++ } ++ ++ public void schedule() { ++ if (this.scheduled.getAndSet(true)) { ++ throw new IllegalStateException("schedule() called twice"); ++ } ++ int priority = this.getPriorityVolatile(); ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled ++ return; ++ } ++ ++ final BiConsumer<CompoundTag, Throwable> consumer = (final CompoundTag data, final Throwable thr) -> { ++ // because cancelScheduled() cannot actually stop this task from executing in every case, we need ++ // to mark complete here to ensure we do not double complete ++ if (LoadDataFromDiskTask.this.markExecuting()) { ++ LoadDataFromDiskTask.this.complete(data, thr); ++ } // else: cancelled ++ }; ++ ++ final PrioritisedExecutor.Priority initialPriority = PrioritisedExecutor.Priority.getPriority(priority); ++ boolean scheduledUnload = false; ++ ++ final NewChunkHolder holder = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ); ++ if (holder != null) { ++ final BiConsumer<CompoundTag, Throwable> unloadConsumer = (final CompoundTag data, final Throwable thr) -> { ++ if (data != null) { ++ consumer.accept(data, null); ++ } else { ++ // need to schedule task ++ LoadDataFromDiskTask.this.schedule(false, consumer, PrioritisedExecutor.Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS)); ++ } ++ }; ++ Cancellable unloadCancellable = null; ++ CompoundTag syncComplete = null; ++ final NewChunkHolder.UnloadTask unloadTask = holder.getUnloadTask(this.type); // can be null if no task exists ++ final Completable<CompoundTag> unloadCompletable = unloadTask == null ? null : unloadTask.completable(); ++ if (unloadCompletable != null) { ++ unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer); ++ if (unloadCancellable == null) { ++ syncComplete = unloadCompletable.getResult(); ++ } ++ } ++ ++ if (syncComplete != null) { ++ consumer.accept(syncComplete, null); ++ return; ++ } ++ ++ if (unloadCancellable != null) { ++ scheduledUnload = true; ++ this.dataUnloadCancellable = unloadCancellable; ++ this.dataUnloadTask = unloadTask.task(); ++ } ++ } ++ ++ this.schedule(scheduledUnload, consumer, initialPriority); ++ } ++ ++ private void schedule(final boolean scheduledUnload, final BiConsumer<CompoundTag, Throwable> consumer, final PrioritisedExecutor.Priority initialPriority) { ++ int priority = this.getPriorityVolatile(); ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled ++ return; ++ } ++ ++ if (!scheduledUnload) { ++ this.dataLoadTask = RegionFileIOThread.loadDataAsync( ++ this.world, this.chunkX, this.chunkZ, this.type, consumer, ++ initialPriority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), initialPriority ++ ); ++ } ++ ++ int failures = 0; ++ for (;;) { ++ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | (scheduledUnload ? PRIORITY_UNLOAD_SCHEDULED : PRIORITY_LOAD_SCHEDULED)))) { ++ return; ++ } ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ if (this.dataUnloadCancellable != null) { ++ this.dataUnloadCancellable.cancel(); ++ } ++ ++ if (this.dataLoadTask != null) { ++ this.dataLoadTask.cancel(); ++ } ++ return; ++ } ++ ++ if (scheduledUnload) { ++ if (this.dataUnloadTask != null) { ++ this.dataUnloadTask.setPriority(PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS)); ++ } ++ } else { ++ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS)); ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cb6af3712bf9f6f6b8f7a459c309c75dabe83a50 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/server/ChunkSystemMinecraftServer.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.server; ++ ++public interface ChunkSystemMinecraftServer { ++ ++ public void moonrise$setChunkSystemCrash(final Throwable throwable); ++ ++ public void moonrise$executeMidTickTasks(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ea759ce6f10f2a5a4e107ab7528030fe931ba223 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/status/ChunkSystemChunkStep.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.status; ++ ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++ ++public interface ChunkSystemChunkStep { ++ ++ public ChunkStatus moonrise$getRequiredStatusAtRadius(final int radius); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java +new file mode 100644 +index 0000000000000000000000000000000000000000..129a35ff2db5b3bb6736810fc180796ce55e1875 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.storage; ++ ++import net.minecraft.world.level.chunk.storage.RegionFileStorage; ++ ++public interface ChunkSystemChunkStorage { ++ ++ public RegionFileStorage moonrise$getRegionStorage(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java +new file mode 100644 +index 0000000000000000000000000000000000000000..786e6ad17cd6216ef0aadaa7cf10044a0c19c933 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.ticket; ++ ++public interface ChunkSystemTicket<T> { ++ ++ public long moonrise$getRemoveDelay(); ++ ++ public void moonrise$setRemoveDelay(final long removeDelay); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2add7fd15a2210286aeb9af5024263333340d34c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticks/ChunkSystemLevelChunkTicks.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.ticks; ++ ++public interface ChunkSystemLevelChunkTicks { ++ ++ public boolean moonrise$isDirty(final long tick); ++ ++ public void moonrise$clearDirty(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ce3bb903c9ccb7efa0f004cf79b291dcb1cb7a23 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ChunkSystemSortedArraySet.java +@@ -0,0 +1,15 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.util; ++ ++import net.minecraft.util.SortedArraySet; ++ ++public interface ChunkSystemSortedArraySet<T> { ++ ++ public SortedArraySet<T> moonrise$copy(); ++ ++ public Object[] moonrise$copyBackingArray(); ++ ++ public T moonrise$replace(final T object); ++ ++ public T moonrise$removeAndGet(final T object); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3a9a564edfdb99e006e4816cb8821bd1e9ecff43 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java +@@ -0,0 +1,320 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.util; ++ ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import it.unimi.dsi.fastutil.HashCommon; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import it.unimi.dsi.fastutil.longs.LongIterator; ++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; ++import it.unimi.dsi.fastutil.longs.LongOpenHashSet; ++import java.util.Arrays; ++import java.util.Objects; ++ ++public final class ParallelSearchRadiusIteration { ++ ++ // expected that this list returns for a given radius, the set of chunks ordered ++ // by manhattan distance ++ private static final long[][] SEARCH_RADIUS_ITERATION_LIST = new long[64+2+1][]; ++ static { ++ for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) { ++ // a BFS around -x, -z, +x, +z will give increasing manhatten distance ++ SEARCH_RADIUS_ITERATION_LIST[i] = generateBFSOrder(i); ++ } ++ } ++ ++ public static long[] getSearchIteration(final int radius) { ++ return SEARCH_RADIUS_ITERATION_LIST[radius]; ++ } ++ ++ private static class CustomLongArray extends LongArrayList { ++ ++ public CustomLongArray() { ++ super(); ++ } ++ ++ public CustomLongArray(final int expected) { ++ super(expected); ++ } ++ ++ public boolean addAll(final CustomLongArray list) { ++ this.addElements(this.size, list.a, 0, list.size); ++ return list.size != 0; ++ } ++ ++ public void addUnchecked(final long value) { ++ this.a[this.size++] = value; ++ } ++ ++ public void forceSize(final int to) { ++ this.size = to; ++ } ++ ++ @Override ++ public int hashCode() { ++ long h = 1L; ++ ++ Objects.checkFromToIndex(0, this.size, this.a.length); ++ ++ for (int i = 0; i < this.size; ++i) { ++ h = HashCommon.mix(h + this.a[i]); ++ } ++ ++ return (int)h; ++ } ++ ++ @Override ++ public boolean equals(final Object o) { ++ if (o == this) { ++ return true; ++ } ++ ++ if (!(o instanceof CustomLongArray other)) { ++ return false; ++ } ++ ++ return this.size == other.size && Arrays.equals(this.a, 0, this.size, other.a, 0, this.size); ++ } ++ } ++ ++ private static int getDistanceSize(final int radius, final int max) { ++ if (radius == 0) { ++ return 1; ++ } ++ final int diff = radius - max; ++ if (diff <= 0) { ++ return 4*radius; ++ } ++ return 4*(max - Math.max(0, diff - 1)); ++ } ++ ++ private static int getQ1DistanceSize(final int radius, final int max) { ++ if (radius == 0) { ++ return 1; ++ } ++ final int diff = radius - max; ++ if (diff <= 0) { ++ return radius+1; ++ } ++ return max - diff + 1; ++ } ++ ++ private static final class BasicFIFOLQueue { ++ ++ private final long[] values; ++ private int head, tail; ++ ++ public BasicFIFOLQueue(final int cap) { ++ if (cap <= 1) { ++ throw new IllegalArgumentException(); ++ } ++ this.values = new long[cap]; ++ } ++ ++ public boolean isEmpty() { ++ return this.head == this.tail; ++ } ++ ++ public long removeFirst() { ++ final long ret = this.values[this.head]; ++ ++ if (this.head == this.tail) { ++ throw new IllegalStateException(); ++ } ++ ++ ++this.head; ++ if (this.head == this.values.length) { ++ this.head = 0; ++ } ++ ++ return ret; ++ } ++ ++ public void addLast(final long value) { ++ this.values[this.tail++] = value; ++ ++ if (this.tail == this.head) { ++ throw new IllegalStateException(); ++ } ++ ++ if (this.tail == this.values.length) { ++ this.tail = 0; ++ } ++ } ++ } ++ ++ private static CustomLongArray[] makeQ1BFS(final int radius) { ++ final CustomLongArray[] ret = new CustomLongArray[2 * radius + 1]; ++ final BasicFIFOLQueue queue = new BasicFIFOLQueue(Math.max(1, 4 * radius) + 1); ++ final LongOpenHashSet seen = new LongOpenHashSet((radius + 1) * (radius + 1)); ++ ++ seen.add(CoordinateUtils.getChunkKey(0, 0)); ++ queue.addLast(CoordinateUtils.getChunkKey(0, 0)); ++ while (!queue.isEmpty()) { ++ final long chunk = queue.removeFirst(); ++ final int chunkX = CoordinateUtils.getChunkX(chunk); ++ final int chunkZ = CoordinateUtils.getChunkZ(chunk); ++ ++ final int index = Math.abs(chunkX) + Math.abs(chunkZ); ++ final CustomLongArray list = ret[index]; ++ if (list != null) { ++ list.addUnchecked(chunk); ++ } else { ++ (ret[index] = new CustomLongArray(getQ1DistanceSize(index, radius))).addUnchecked(chunk); ++ } ++ ++ for (int i = 0; i < 4; ++i) { ++ // 0 -> -1, 0 ++ // 1 -> 0, -1 ++ // 2 -> 1, 0 ++ // 3 -> 0, 1 ++ ++ final int signInv = -(i >>> 1); // 2/3 -> -(1), 0/1 -> -(0) ++ // note: -n = (~n) + 1 ++ // (n ^ signInv) - signInv = signInv == 0 ? ((n ^ 0) - 0 = n) : ((n ^ -1) - (-1) = ~n + 1) ++ ++ final int axis = i & 1; // 0/2 -> 0, 1/3 -> 1 ++ final int dx = ((axis - 1) ^ signInv) - signInv; // 0 -> -1, 1 -> 0 ++ final int dz = (-axis ^ signInv) - signInv; // 0 -> 0, 1 -> -1 ++ ++ final int neighbourX = chunkX + dx; ++ final int neighbourZ = chunkZ + dz; ++ final long neighbour = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); ++ ++ if ((neighbourX | neighbourZ) < 0 || Math.max(Math.abs(neighbourX), Math.abs(neighbourZ)) > radius) { ++ // don't enqueue out of range ++ continue; ++ } ++ ++ if (!seen.add(neighbour)) { ++ continue; ++ } ++ ++ queue.addLast(neighbour); ++ } ++ } ++ ++ return ret; ++ } ++ ++ // doesn't appear worth optimising this function now, even though it's 70% of the call ++ private static CustomLongArray spread(final CustomLongArray input, final int size) { ++ final LongLinkedOpenHashSet notAdded = new LongLinkedOpenHashSet(input); ++ final CustomLongArray added = new CustomLongArray(size); ++ ++ while (!notAdded.isEmpty()) { ++ if (added.isEmpty()) { ++ added.addUnchecked(notAdded.removeLastLong()); ++ continue; ++ } ++ ++ long maxChunk = -1L; ++ int maxDist = 0; ++ ++ // select the chunk from the not yet added set that has the largest minimum distance from ++ // the current set of added chunks ++ ++ for (final LongIterator iterator = notAdded.iterator(); iterator.hasNext();) { ++ final long chunkKey = iterator.nextLong(); ++ final int chunkX = CoordinateUtils.getChunkX(chunkKey); ++ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey); ++ ++ int minDist = Integer.MAX_VALUE; ++ ++ final int len = added.size(); ++ final long[] addedArr = added.elements(); ++ Objects.checkFromToIndex(0, len, addedArr.length); ++ for (int i = 0; i < len; ++i) { ++ final long addedKey = addedArr[i]; ++ final int addedX = CoordinateUtils.getChunkX(addedKey); ++ final int addedZ = CoordinateUtils.getChunkZ(addedKey); ++ ++ // here we use square distance because chunk generation uses neighbours in a square radius ++ final int dist = Math.max(Math.abs(addedX - chunkX), Math.abs(addedZ - chunkZ)); ++ ++ minDist = Math.min(dist, minDist); ++ } ++ ++ if (minDist > maxDist) { ++ maxDist = minDist; ++ maxChunk = chunkKey; ++ } ++ } ++ ++ // move the selected chunk from the not added set to the added set ++ ++ if (!notAdded.remove(maxChunk)) { ++ throw new IllegalStateException(); ++ } ++ ++ added.addUnchecked(maxChunk); ++ } ++ ++ return added; ++ } ++ ++ private static void expandQuadrants(final CustomLongArray input, final int size) { ++ final int len = input.size(); ++ final long[] array = input.elements(); ++ ++ int writeIndex = size - 1; ++ for (int i = len - 1; i >= 0; --i) { ++ final long key = array[i]; ++ final int chunkX = CoordinateUtils.getChunkX(key); ++ final int chunkZ = CoordinateUtils.getChunkZ(key); ++ ++ if ((chunkX | chunkZ) < 0 || (i != 0 && chunkX == 0 && chunkZ == 0)) { ++ throw new IllegalStateException(); ++ } ++ ++ // Q4 ++ if (chunkZ != 0) { ++ array[writeIndex--] = CoordinateUtils.getChunkKey(chunkX, -chunkZ); ++ } ++ // Q3 ++ if (chunkX != 0 && chunkZ != 0) { ++ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, -chunkZ); ++ } ++ // Q2 ++ if (chunkX != 0) { ++ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, chunkZ); ++ } ++ ++ array[writeIndex--] = key; ++ } ++ ++ input.forceSize(size); ++ ++ if (writeIndex != -1) { ++ throw new IllegalStateException(); ++ } ++ } ++ ++ private static long[] generateBFSOrder(final int radius) { ++ // by using only the first quadrant, we can reduce the total element size by 4 when spreading ++ final CustomLongArray[] byDistance = makeQ1BFS(radius); ++ ++ // to increase generation parallelism, we want to space the chunks out so that they are not nearby when generating ++ // this also means we are minimising locality ++ // but, we need to maintain sorted order by manhatten distance ++ ++ // per manhatten distance we transform the chunk list so that each element is maximally spaced out from each other ++ for (int i = 0, len = byDistance.length; i < len; ++i) { ++ final CustomLongArray points = byDistance[i]; ++ final int expectedSize = getDistanceSize(i, radius); ++ ++ final CustomLongArray spread = spread(points, expectedSize); ++ // add in Q2, Q3, Q4 ++ expandQuadrants(spread, expectedSize); ++ ++ byDistance[i] = spread; ++ } ++ ++ // now, rebuild the list so that it still maintains manhatten distance order ++ final CustomLongArray ret = new CustomLongArray((2 * radius + 1) * (2 * radius + 1)); ++ ++ for (final CustomLongArray dist : byDistance) { ++ ret.addAll(dist); ++ } ++ ++ return ret.elements(); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ea6b6ed27b212719feb31610faac974899688839 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java +@@ -0,0 +1,12 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.world; ++ ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.phys.AABB; ++import java.util.List; ++import java.util.function.Predicate; ++ ++public interface ChunkSystemEntityGetter { ++ ++ public List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4b9e2fa963c14f65f15407c1814c543c2999ea32 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemServerChunkCache.java +@@ -0,0 +1,11 @@ ++package ca.spottedleaf.moonrise.patches.chunk_system.world; ++ ++import net.minecraft.world.level.chunk.LevelChunk; ++ ++public interface ChunkSystemServerChunkCache { ++ ++ public void moonrise$setFullChunk(final int chunkX, final int chunkZ, final LevelChunk chunk); ++ ++ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a27385126 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +@@ -0,0 +1,1853 @@ ++package ca.spottedleaf.moonrise.patches.collisions; ++ ++public final class CollisionUtil { ++ ++ public static final double COLLISION_EPSILON = 1.0E-7; ++ public static final it.unimi.dsi.fastutil.doubles.DoubleArrayList ZERO_ONE = it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(new double[] { 0.0, 1.0 }); ++ ++ public static boolean isSpecialCollidingBlock(final net.minecraft.world.level.block.state.BlockBehaviour.BlockStateBase block) { ++ return block.hasLargeCollisionShape() || block.getBlock() == net.minecraft.world.level.block.Blocks.MOVING_PISTON; ++ } ++ ++ public static boolean isEmpty(final net.minecraft.world.phys.AABB aabb) { ++ return (aabb.maxX - aabb.minX) < COLLISION_EPSILON || (aabb.maxY - aabb.minY) < COLLISION_EPSILON || (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON; ++ } ++ ++ public static boolean isEmpty(final double minX, final double minY, final double minZ, ++ final double maxX, final double maxY, final double maxZ) { ++ return (maxX - minX) < COLLISION_EPSILON || (maxY - minY) < COLLISION_EPSILON || (maxZ - minZ) < COLLISION_EPSILON; ++ } ++ ++ public static net.minecraft.world.phys.AABB getBoxForChunk(final int chunkX, final int chunkZ) { ++ double x = (double)(chunkX << 4); ++ double z = (double)(chunkZ << 4); ++ // use a bounding box bigger than the chunk to prevent entities from entering it on move ++ return new net.minecraft.world.phys.AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON, ++ x + (16.0 + 3*COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*COLLISION_EPSILON)); ++ } ++ ++ /* ++ A couple of rules for VoxelShape collisions: ++ Two shapes only intersect if they are actually more than EPSILON units into each other. This also applies to movement ++ checks. ++ If the two shapes strictly collide, then the return value of a collide call will return a value in the opposite ++ direction of the source move. However, this value will not be greater in magnitude than EPSILON. Collision code ++ will automatically round it to 0. ++ */ ++ ++ public static boolean voxelShapeIntersect(final double minX1, final double minY1, final double minZ1, final double maxX1, ++ final double maxY1, final double maxZ1, final double minX2, final double minY2, ++ final double minZ2, final double maxX2, final double maxY2, final double maxZ2) { ++ return (minX1 - maxX2) < -COLLISION_EPSILON && (maxX1 - minX2) > COLLISION_EPSILON && ++ (minY1 - maxY2) < -COLLISION_EPSILON && (maxY1 - minY2) > COLLISION_EPSILON && ++ (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON; ++ } ++ ++ public static boolean voxelShapeIntersect(final net.minecraft.world.phys.AABB box, final double minX, final double minY, final double minZ, ++ final double maxX, final double maxY, final double maxZ) { ++ return (box.minX - maxX) < -COLLISION_EPSILON && (box.maxX - minX) > COLLISION_EPSILON && ++ (box.minY - maxY) < -COLLISION_EPSILON && (box.maxY - minY) > COLLISION_EPSILON && ++ (box.minZ - maxZ) < -COLLISION_EPSILON && (box.maxZ - minZ) > COLLISION_EPSILON; ++ } ++ ++ public static boolean voxelShapeIntersect(final net.minecraft.world.phys.AABB box1, final net.minecraft.world.phys.AABB box2) { ++ return (box1.minX - box2.maxX) < -COLLISION_EPSILON && (box1.maxX - box2.minX) > COLLISION_EPSILON && ++ (box1.minY - box2.maxY) < -COLLISION_EPSILON && (box1.maxY - box2.minY) > COLLISION_EPSILON && ++ (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON; ++ } ++ ++ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON ++ public static double collideX(final net.minecraft.world.phys.AABB target, final net.minecraft.world.phys.AABB source, final double source_move) { ++ if ((source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON && ++ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { ++ if (source_move >= 0.0) { ++ final double max_move = target.minX - source.maxX; // < 0.0 if no strict collision ++ if (max_move < -COLLISION_EPSILON) { ++ return source_move; ++ } ++ return Math.min(max_move, source_move); ++ } else { ++ final double max_move = target.maxX - source.minX; // > 0.0 if no strict collision ++ if (max_move > COLLISION_EPSILON) { ++ return source_move; ++ } ++ return Math.max(max_move, source_move); ++ } ++ } ++ return source_move; ++ } ++ ++ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON ++ public static double collideY(final net.minecraft.world.phys.AABB target, final net.minecraft.world.phys.AABB source, final double source_move) { ++ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && ++ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { ++ if (source_move >= 0.0) { ++ final double max_move = target.minY - source.maxY; // < 0.0 if no strict collision ++ if (max_move < -COLLISION_EPSILON) { ++ return source_move; ++ } ++ return Math.min(max_move, source_move); ++ } else { ++ final double max_move = target.maxY - source.minY; // > 0.0 if no strict collision ++ if (max_move > COLLISION_EPSILON) { ++ return source_move; ++ } ++ return Math.max(max_move, source_move); ++ } ++ } ++ return source_move; ++ } ++ ++ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON ++ public static double collideZ(final net.minecraft.world.phys.AABB target, final net.minecraft.world.phys.AABB source, final double source_move) { ++ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && ++ (source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON) { ++ if (source_move >= 0.0) { ++ final double max_move = target.minZ - source.maxZ; // < 0.0 if no strict collision ++ if (max_move < -COLLISION_EPSILON) { ++ return source_move; ++ } ++ return Math.min(max_move, source_move); ++ } else { ++ final double max_move = target.maxZ - source.minZ; // > 0.0 if no strict collision ++ if (max_move > COLLISION_EPSILON) { ++ return source_move; ++ } ++ return Math.max(max_move, source_move); ++ } ++ } ++ return source_move; ++ } ++ ++ // startIndex and endIndex inclusive ++ // assumes indices are in range of array ++ private static int findFloor(final double[] values, final double value, int startIndex, int endIndex) { ++ do { ++ final int middle = (startIndex + endIndex) >>> 1; ++ final double middleVal = values[middle]; ++ ++ if (value < middleVal) { ++ endIndex = middle - 1; ++ } else { ++ startIndex = middle + 1; ++ } ++ } while (startIndex <= endIndex); ++ ++ return startIndex - 1; ++ } ++ ++ public static boolean voxelShapeIntersectNoEmpty(final net.minecraft.world.phys.shapes.VoxelShape voxel, final net.minecraft.world.phys.AABB aabb) { ++ if (voxel.isEmpty()) { ++ return false; ++ } ++ ++ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true ++ ++ // offsets that should be applied to coords ++ final double off_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetX(); ++ final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetY(); ++ final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetZ(); ++ ++ final double[] coords_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ(); ++ ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getCachedVoxelData(); ++ ++ // note: size = coords.length - 1 ++ final int size_x = cached_shape_data.sizeX(); ++ final int size_y = cached_shape_data.sizeY(); ++ final int size_z = cached_shape_data.sizeZ(); ++ ++ // note: voxel bitset with set index (x, y, z) indicates that ++ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) ++ // is collidable. this is the fundamental principle of operation for the voxel collision operation ++ ++ // note: we should be offsetting coords, but we can also just subtract from source as well - which is ++ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) ++ // note: for intersection, one we find the floor of the min we can use that as the start index ++ // for the next check as source max >= source min ++ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, ++ // as this implies that coords[coords.length - 1] < source min ++ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max ++ ++ final int floor_min_x = Math.max( ++ 0, ++ findFloor(coords_x, (aabb.minX - off_x) + COLLISION_EPSILON, 0, size_x) ++ ); ++ if (floor_min_x >= size_x) { ++ // cannot intersect ++ return false; ++ } ++ ++ final int ceil_max_x = Math.min( ++ size_x, ++ findFloor(coords_x, (aabb.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1 ++ ); ++ if (floor_min_x >= ceil_max_x) { ++ // cannot intersect ++ return false; ++ } ++ ++ final int floor_min_y = Math.max( ++ 0, ++ findFloor(coords_y, (aabb.minY - off_y) + COLLISION_EPSILON, 0, size_y) ++ ); ++ if (floor_min_y >= size_y) { ++ // cannot intersect ++ return false; ++ } ++ ++ final int ceil_max_y = Math.min( ++ size_y, ++ findFloor(coords_y, (aabb.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1 ++ ); ++ if (floor_min_y >= ceil_max_y) { ++ // cannot intersect ++ return false; ++ } ++ ++ final int floor_min_z = Math.max( ++ 0, ++ findFloor(coords_z, (aabb.minZ - off_z) + COLLISION_EPSILON, 0, size_z) ++ ); ++ if (floor_min_z >= size_z) { ++ // cannot intersect ++ return false; ++ } ++ ++ final int ceil_max_z = Math.min( ++ size_z, ++ findFloor(coords_z, (aabb.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1 ++ ); ++ if (floor_min_z >= ceil_max_z) { ++ // cannot intersect ++ return false; ++ } ++ ++ final long[] bitset = cached_shape_data.voxelSet(); ++ ++ // check bitset to check if any shapes in range are full ++ ++ final int mul_x = size_y*size_z; ++ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { ++ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { ++ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { ++ final int index = curr_z + curr_y*size_z + curr_x*mul_x; ++ // note: JLS states long shift operators ANDS shift by 63 ++ if ((bitset[index >>> 6] & (1L << index)) != 0L) { ++ return true; ++ } ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ // assume !target.isEmpty() && abs(source_move) >= COLLISION_EPSILON ++ public static double collideX(final net.minecraft.world.phys.shapes.VoxelShape target, final net.minecraft.world.phys.AABB source, final double source_move) { ++ final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); ++ if (single_aabb != null) { ++ return collideX(single_aabb, source, source_move); ++ } ++ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true ++ ++ // offsets that should be applied to coords ++ final double off_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetX(); ++ final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetY(); ++ final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetZ(); ++ ++ final double[] coords_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); ++ ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getCachedVoxelData(); ++ ++ // note: size = coords.length - 1 ++ final int size_x = cached_shape_data.sizeX(); ++ final int size_y = cached_shape_data.sizeY(); ++ final int size_z = cached_shape_data.sizeZ(); ++ ++ // note: voxel bitset with set index (x, y, z) indicates that ++ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) ++ // is collidable. this is the fundamental principle of operation for the voxel collision operation ++ ++ ++ // note: we should be offsetting coords, but we can also just subtract from source as well - which is ++ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) ++ // note: for intersection, one we find the floor of the min we can use that as the start index ++ // for the next check as source max >= source min ++ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, ++ // as this implies that coords[coords.length - 1] < source min ++ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max ++ ++ final int floor_min_y = Math.max( ++ 0, ++ findFloor(coords_y, (source.minY - off_y) + COLLISION_EPSILON, 0, size_y) ++ ); ++ if (floor_min_y >= size_y) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int ceil_max_y = Math.min( ++ size_y, ++ findFloor(coords_y, (source.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1 ++ ); ++ if (floor_min_y >= ceil_max_y) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int floor_min_z = Math.max( ++ 0, ++ findFloor(coords_z, (source.minZ - off_z) + COLLISION_EPSILON, 0, size_z) ++ ); ++ if (floor_min_z >= size_z) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int ceil_max_z = Math.min( ++ size_z, ++ findFloor(coords_z, (source.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1 ++ ); ++ if (floor_min_z >= ceil_max_z) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ // index = z + y*size_z + x*(size_z*size_y) ++ ++ final long[] bitset = cached_shape_data.voxelSet(); ++ ++ if (source_move > 0.0) { ++ final double source_max = source.maxX - off_x; ++ final int ceil_max_x = findFloor( ++ coords_x, source_max - COLLISION_EPSILON, 0, size_x ++ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max ++ ++ // note: only the order of the first loop matters ++ ++ // note: we cannot collide with the face at index size on the collision axis for forward movement ++ ++ final int mul_x = size_y*size_z; ++ for (int curr_x = ceil_max_x; curr_x < size_x; ++curr_x) { ++ double max_dist = coords_x[curr_x] - source_max; ++ if (max_dist >= source_move) { ++ // if we reach here, then we will never have a case where ++ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] ++ // thus, we can return immediately ++ ++ // this optimization is important since this loop is bounded by size, and _not_ by ++ // a calculated max index based off of source_move - so it would be possible to check ++ // the whole intersected shape for collisions when we didn't need to! ++ return source_move; ++ } ++ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON ++ max_dist = Math.min(max_dist, source_move); ++ } ++ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { ++ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { ++ final int index = curr_z + curr_y*size_z + curr_x*mul_x; ++ // note: JLS states long shift operators ANDS shift by 63 ++ if ((bitset[index >>> 6] & (1L << index)) != 0L) { ++ return max_dist; ++ } ++ } ++ } ++ } ++ ++ return source_move; ++ } else { ++ final double source_min = source.minX - off_x; ++ final int floor_min_x = findFloor( ++ coords_x, source_min + COLLISION_EPSILON, 0, size_x ++ ); ++ ++ // note: only the order of the first loop matters ++ ++ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement ++ ++ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the ++ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] ++ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid ++ final int mul_x = size_y*size_z; ++ for (int curr_x = floor_min_x - 1; curr_x >= 0; --curr_x) { ++ double max_dist = coords_x[curr_x + 1] - source_min; ++ if (max_dist <= source_move) { ++ // if we reach here, then we will never have a case where ++ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] ++ // thus, we can return immediately ++ ++ // this optimization is important since this loop is possibly bounded by size, and _not_ by ++ // a calculated max index based off of source_move - so it would be possible to check ++ // the whole intersected shape for collisions when we didn't need to! ++ return source_move; ++ } ++ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON ++ max_dist = Math.max(max_dist, source_move); ++ } ++ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { ++ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { ++ final int index = curr_z + curr_y*size_z + curr_x*mul_x; ++ // note: JLS states long shift operators ANDS shift by 63 ++ if ((bitset[index >>> 6] & (1L << index)) != 0L) { ++ return max_dist; ++ } ++ } ++ } ++ } ++ ++ return source_move; ++ } ++ } ++ ++ public static double collideY(final net.minecraft.world.phys.shapes.VoxelShape target, final net.minecraft.world.phys.AABB source, final double source_move) { ++ final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); ++ if (single_aabb != null) { ++ return collideY(single_aabb, source, source_move); ++ } ++ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true ++ ++ // offsets that should be applied to coords ++ final double off_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetX(); ++ final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetY(); ++ final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetZ(); ++ ++ final double[] coords_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); ++ ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getCachedVoxelData(); ++ ++ // note: size = coords.length - 1 ++ final int size_x = cached_shape_data.sizeX(); ++ final int size_y = cached_shape_data.sizeY(); ++ final int size_z = cached_shape_data.sizeZ(); ++ ++ // note: voxel bitset with set index (x, y, z) indicates that ++ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) ++ // is collidable. this is the fundamental principle of operation for the voxel collision operation ++ ++ ++ // note: we should be offsetting coords, but we can also just subtract from source as well - which is ++ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) ++ // note: for intersection, one we find the floor of the min we can use that as the start index ++ // for the next check as source max >= source min ++ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, ++ // as this implies that coords[coords.length - 1] < source min ++ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max ++ ++ final int floor_min_x = Math.max( ++ 0, ++ findFloor(coords_x, (source.minX - off_x) + COLLISION_EPSILON, 0, size_x) ++ ); ++ if (floor_min_x >= size_x) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int ceil_max_x = Math.min( ++ size_x, ++ findFloor(coords_x, (source.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1 ++ ); ++ if (floor_min_x >= ceil_max_x) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int floor_min_z = Math.max( ++ 0, ++ findFloor(coords_z, (source.minZ - off_z) + COLLISION_EPSILON, 0, size_z) ++ ); ++ if (floor_min_z >= size_z) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int ceil_max_z = Math.min( ++ size_z, ++ findFloor(coords_z, (source.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1 ++ ); ++ if (floor_min_z >= ceil_max_z) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ // index = z + y*size_z + x*(size_z*size_y) ++ ++ final long[] bitset = cached_shape_data.voxelSet(); ++ ++ if (source_move > 0.0) { ++ final double source_max = source.maxY - off_y; ++ final int ceil_max_y = findFloor( ++ coords_y, source_max - COLLISION_EPSILON, 0, size_y ++ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max ++ ++ // note: only the order of the first loop matters ++ ++ // note: we cannot collide with the face at index size on the collision axis for forward movement ++ ++ final int mul_x = size_y*size_z; ++ for (int curr_y = ceil_max_y; curr_y < size_y; ++curr_y) { ++ double max_dist = coords_y[curr_y] - source_max; ++ if (max_dist >= source_move) { ++ // if we reach here, then we will never have a case where ++ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] ++ // thus, we can return immediately ++ ++ // this optimization is important since this loop is bounded by size, and _not_ by ++ // a calculated max index based off of source_move - so it would be possible to check ++ // the whole intersected shape for collisions when we didn't need to! ++ return source_move; ++ } ++ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON ++ max_dist = Math.min(max_dist, source_move); ++ } ++ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { ++ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { ++ final int index = curr_z + curr_y*size_z + curr_x*mul_x; ++ // note: JLS states long shift operators ANDS shift by 63 ++ if ((bitset[index >>> 6] & (1L << index)) != 0L) { ++ return max_dist; ++ } ++ } ++ } ++ } ++ ++ return source_move; ++ } else { ++ final double source_min = source.minY - off_y; ++ final int floor_min_y = findFloor( ++ coords_y, source_min + COLLISION_EPSILON, 0, size_y ++ ); ++ ++ // note: only the order of the first loop matters ++ ++ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement ++ ++ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the ++ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] ++ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid ++ final int mul_x = size_y*size_z; ++ for (int curr_y = floor_min_y - 1; curr_y >= 0; --curr_y) { ++ double max_dist = coords_y[curr_y + 1] - source_min; ++ if (max_dist <= source_move) { ++ // if we reach here, then we will never have a case where ++ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] ++ // thus, we can return immediately ++ ++ // this optimization is important since this loop is possibly bounded by size, and _not_ by ++ // a calculated max index based off of source_move - so it would be possible to check ++ // the whole intersected shape for collisions when we didn't need to! ++ return source_move; ++ } ++ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON ++ max_dist = Math.max(max_dist, source_move); ++ } ++ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { ++ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { ++ final int index = curr_z + curr_y*size_z + curr_x*mul_x; ++ // note: JLS states long shift operators ANDS shift by 63 ++ if ((bitset[index >>> 6] & (1L << index)) != 0L) { ++ return max_dist; ++ } ++ } ++ } ++ } ++ ++ return source_move; ++ } ++ } ++ ++ public static double collideZ(final net.minecraft.world.phys.shapes.VoxelShape target, final net.minecraft.world.phys.AABB source, final double source_move) { ++ final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation(); ++ if (single_aabb != null) { ++ return collideZ(single_aabb, source, source_move); ++ } ++ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true ++ ++ // offsets that should be applied to coords ++ final double off_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetX(); ++ final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetY(); ++ final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetZ(); ++ ++ final double[] coords_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesZ(); ++ ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getCachedVoxelData(); ++ ++ // note: size = coords.length - 1 ++ final int size_x = cached_shape_data.sizeX(); ++ final int size_y = cached_shape_data.sizeY(); ++ final int size_z = cached_shape_data.sizeZ(); ++ ++ // note: voxel bitset with set index (x, y, z) indicates that ++ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) ++ // is collidable. this is the fundamental principle of operation for the voxel collision operation ++ ++ ++ // note: we should be offsetting coords, but we can also just subtract from source as well - which is ++ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) ++ // note: for intersection, one we find the floor of the min we can use that as the start index ++ // for the next check as source max >= source min ++ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, ++ // as this implies that coords[coords.length - 1] < source min ++ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max ++ ++ final int floor_min_x = Math.max( ++ 0, ++ findFloor(coords_x, (source.minX - off_x) + COLLISION_EPSILON, 0, size_x) ++ ); ++ if (floor_min_x >= size_x) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int ceil_max_x = Math.min( ++ size_x, ++ findFloor(coords_x, (source.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1 ++ ); ++ if (floor_min_x >= ceil_max_x) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int floor_min_y = Math.max( ++ 0, ++ findFloor(coords_y, (source.minY - off_y) + COLLISION_EPSILON, 0, size_y) ++ ); ++ if (floor_min_y >= size_y) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int ceil_max_y = Math.min( ++ size_y, ++ findFloor(coords_y, (source.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1 ++ ); ++ if (floor_min_y >= ceil_max_y) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ // index = z + y*size_z + x*(size_z*size_y) ++ ++ final long[] bitset = cached_shape_data.voxelSet(); ++ ++ if (source_move > 0.0) { ++ final double source_max = source.maxZ - off_z; ++ final int ceil_max_z = findFloor( ++ coords_z, source_max - COLLISION_EPSILON, 0, size_z ++ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max ++ ++ // note: only the order of the first loop matters ++ ++ // note: we cannot collide with the face at index size on the collision axis for forward movement ++ ++ final int mul_x = size_y*size_z; ++ for (int curr_z = ceil_max_z; curr_z < size_z; ++curr_z) { ++ double max_dist = coords_z[curr_z] - source_max; ++ if (max_dist >= source_move) { ++ // if we reach here, then we will never have a case where ++ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] ++ // thus, we can return immediately ++ ++ // this optimization is important since this loop is bounded by size, and _not_ by ++ // a calculated max index based off of source_move - so it would be possible to check ++ // the whole intersected shape for collisions when we didn't need to! ++ return source_move; ++ } ++ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON ++ max_dist = Math.min(max_dist, source_move); ++ } ++ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { ++ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { ++ final int index = curr_z + curr_y*size_z + curr_x*mul_x; ++ // note: JLS states long shift operators ANDS shift by 63 ++ if ((bitset[index >>> 6] & (1L << index)) != 0L) { ++ return max_dist; ++ } ++ } ++ } ++ } ++ ++ return source_move; ++ } else { ++ final double source_min = source.minZ - off_z; ++ final int floor_min_z = findFloor( ++ coords_z, source_min + COLLISION_EPSILON, 0, size_z ++ ); ++ ++ // note: only the order of the first loop matters ++ ++ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement ++ ++ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the ++ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] ++ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid ++ final int mul_x = size_y*size_z; ++ for (int curr_z = floor_min_z - 1; curr_z >= 0; --curr_z) { ++ double max_dist = coords_z[curr_z + 1] - source_min; ++ if (max_dist <= source_move) { ++ // if we reach here, then we will never have a case where ++ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] ++ // thus, we can return immediately ++ ++ // this optimization is important since this loop is possibly bounded by size, and _not_ by ++ // a calculated max index based off of source_move - so it would be possible to check ++ // the whole intersected shape for collisions when we didn't need to! ++ return source_move; ++ } ++ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON ++ max_dist = Math.max(max_dist, source_move); ++ } ++ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { ++ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { ++ final int index = curr_z + curr_y*size_z + curr_x*mul_x; ++ // note: JLS states long shift operators ANDS shift by 63 ++ if ((bitset[index >>> 6] & (1L << index)) != 0L) { ++ return max_dist; ++ } ++ } ++ } ++ } ++ ++ return source_move; ++ } ++ } ++ ++ // does not use epsilon ++ public static boolean strictlyContains(final net.minecraft.world.phys.shapes.VoxelShape voxel, final net.minecraft.world.phys.Vec3 point) { ++ return strictlyContains(voxel, point.x, point.y, point.z); ++ } ++ ++ // does not use epsilon ++ public static boolean strictlyContains(final net.minecraft.world.phys.shapes.VoxelShape voxel, double x, double y, double z) { ++ final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation(); ++ if (single_aabb != null) { ++ return single_aabb.contains(x, y, z); ++ } ++ ++ if (voxel.isEmpty()) { ++ // bitset is clear, no point in searching ++ return false; ++ } ++ ++ // offset input ++ x -= ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetX(); ++ y -= ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetY(); ++ z -= ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetZ(); ++ ++ final double[] coords_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesX(); ++ final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesY(); ++ final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ(); ++ ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getCachedVoxelData(); ++ ++ // note: size = coords.length - 1 ++ final int size_x = cached_shape_data.sizeX(); ++ final int size_y = cached_shape_data.sizeY(); ++ final int size_z = cached_shape_data.sizeZ(); ++ ++ // note: should mirror AABB#contains, which is that for any point X that X >= min and X < max. ++ // specifically, it cannot collide on the max bounds of the shape ++ ++ final int index_x = findFloor(coords_x, x, 0, size_x); ++ if (index_x < 0 || index_x >= size_x) { ++ return false; ++ } ++ ++ final int index_y = findFloor(coords_y, y, 0, size_y); ++ if (index_y < 0 || index_y >= size_y) { ++ return false; ++ } ++ ++ final int index_z = findFloor(coords_z, z, 0, size_z); ++ if (index_z < 0 || index_z >= size_z) { ++ return false; ++ } ++ ++ // index = z + y*size_z + x*(size_z*size_y) ++ ++ final int index = index_z + index_y*size_z + index_x*(size_z*size_y); ++ ++ final long[] bitset = cached_shape_data.voxelSet(); ++ ++ return (bitset[index >>> 6] & (1L << index)) != 0L; ++ } ++ ++ private static int makeBitset(final boolean ft, final boolean tf, final boolean tt) { ++ // idx ff -> 0 ++ // idx ft -> 1 ++ // idx tf -> 2 ++ // idx tt -> 3 ++ return ((ft ? 1 : 0) << 1) | ((tf ? 1 : 0) << 2) | ((tt ? 1 : 0) << 3); ++ } ++ ++ private static net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape merge(final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst, final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond, ++ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX, final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY, ++ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ, ++ final int booleanOp) { ++ final int sizeX = mergedX.voxels; ++ final int sizeY = mergedY.voxels; ++ final int sizeZ = mergedZ.voxels; ++ ++ final long[] s1Voxels = shapeDataFirst.voxelSet(); ++ final long[] s2Voxels = shapeDataSecond.voxelSet(); ++ ++ final int s1Mul1 = shapeDataFirst.sizeZ(); ++ final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY(); ++ ++ final int s2Mul1 = shapeDataSecond.sizeZ(); ++ final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY(); ++ ++ // note: indices may contain -1, but nothing > size ++ final net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape ret = new net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape(sizeX, sizeY, sizeZ); ++ ++ boolean empty = true; ++ ++ int mergedIdx = 0; ++ for (int idxX = 0; idxX < sizeX; ++idxX) { ++ final int s1x = mergedX.firstIndices[idxX]; ++ final int s2x = mergedX.secondIndices[idxX]; ++ boolean setX = false; ++ for (int idxY = 0; idxY < sizeY; ++idxY) { ++ final int s1y = mergedY.firstIndices[idxY]; ++ final int s2y = mergedY.secondIndices[idxY]; ++ boolean setY = false; ++ for (int idxZ = 0; idxZ < sizeZ; ++idxZ) { ++ final int s1z = mergedZ.firstIndices[idxZ]; ++ final int s2z = mergedZ.secondIndices[idxZ]; ++ ++ int idx; ++ ++ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L); ++ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L); ++ ++ // idx ff -> 0 ++ // idx ft -> 1 ++ // idx tf -> 2 ++ // idx tt -> 3 ++ ++ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0; ++ setY |= res; ++ setX |= res; ++ ++ if (res) { ++ empty = false; ++ // inline and optimize fill operation ++ ret.zMin = Math.min(ret.zMin, idxZ); ++ ret.zMax = Math.max(ret.zMax, idxZ + 1); ++ ret.storage.set(mergedIdx); ++ } ++ ++ ++mergedIdx; ++ } ++ if (setY) { ++ ret.yMin = Math.min(ret.yMin, idxY); ++ ret.yMax = Math.max(ret.yMax, idxY + 1); ++ } ++ } ++ if (setX) { ++ ret.xMin = Math.min(ret.xMin, idxX); ++ ret.xMax = Math.max(ret.xMax, idxX + 1); ++ } ++ } ++ ++ return empty ? null : ret; ++ } ++ ++ private static boolean isMergeEmpty(final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst, final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond, ++ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX, final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY, ++ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ, ++ final int booleanOp) { ++ final int sizeX = mergedX.voxels; ++ final int sizeY = mergedY.voxels; ++ final int sizeZ = mergedZ.voxels; ++ ++ final long[] s1Voxels = shapeDataFirst.voxelSet(); ++ final long[] s2Voxels = shapeDataSecond.voxelSet(); ++ ++ final int s1Mul1 = shapeDataFirst.sizeZ(); ++ final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY(); ++ ++ final int s2Mul1 = shapeDataSecond.sizeZ(); ++ final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY(); ++ ++ // note: indices may contain -1, but nothing > size ++ for (int idxX = 0; idxX < sizeX; ++idxX) { ++ final int s1x = mergedX.firstIndices[idxX]; ++ final int s2x = mergedX.secondIndices[idxX]; ++ for (int idxY = 0; idxY < sizeY; ++idxY) { ++ final int s1y = mergedY.firstIndices[idxY]; ++ final int s2y = mergedY.secondIndices[idxY]; ++ for (int idxZ = 0; idxZ < sizeZ; ++idxZ) { ++ final int s1z = mergedZ.firstIndices[idxZ]; ++ final int s2z = mergedZ.secondIndices[idxZ]; ++ ++ int idx; ++ ++ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L); ++ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L); ++ ++ // idx ff -> 0 ++ // idx ft -> 1 ++ // idx tf -> 2 ++ // idx tt -> 3 ++ ++ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0; ++ ++ if (res) { ++ return false; ++ } ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ public static net.minecraft.world.phys.shapes.VoxelShape joinOptimized(final net.minecraft.world.phys.shapes.VoxelShape first, final net.minecraft.world.phys.shapes.VoxelShape second, final net.minecraft.world.phys.shapes.BooleanOp operator) { ++ return joinUnoptimized(first, second, operator).optimize(); ++ } ++ ++ public static net.minecraft.world.phys.shapes.VoxelShape joinUnoptimized(final net.minecraft.world.phys.shapes.VoxelShape first, final net.minecraft.world.phys.shapes.VoxelShape second, final net.minecraft.world.phys.shapes.BooleanOp operator) { ++ final boolean ff = operator.apply(false, false); ++ if (ff) { ++ // technically, should be an infinite box but that's clearly an error ++ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true"); ++ } ++ ++ final boolean tt = operator.apply(true, true); ++ ++ if (first == second) { ++ return tt ? first : net.minecraft.world.phys.shapes.Shapes.empty(); ++ } ++ ++ final boolean ft = operator.apply(false, true); ++ final boolean tf = operator.apply(true, false); ++ ++ if (first.isEmpty()) { ++ return ft ? second : net.minecraft.world.phys.shapes.Shapes.empty(); ++ } ++ if (second.isEmpty()) { ++ return tf ? first : net.minecraft.world.phys.shapes.Shapes.empty(); ++ } ++ ++ if (!tt) { ++ // try to check for no intersection, since tt = false ++ final net.minecraft.world.phys.AABB aabbF = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation(); ++ final net.minecraft.world.phys.AABB aabbS = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation(); ++ ++ final boolean intersect; ++ ++ final boolean hasAABBF = aabbF != null; ++ final boolean hasAABBS = aabbS != null; ++ if (hasAABBF | hasAABBS) { ++ if (hasAABBF & hasAABBS) { ++ intersect = voxelShapeIntersect(aabbF, aabbS); ++ } else if (hasAABBF) { ++ intersect = voxelShapeIntersectNoEmpty(second, aabbF); ++ } else { ++ intersect = voxelShapeIntersectNoEmpty(first, aabbS); ++ } ++ } else { ++ // expect cached bounds ++ intersect = voxelShapeIntersect(first.bounds(), second.bounds()); ++ } ++ ++ if (!intersect) { ++ if (!tf & !ft) { ++ return net.minecraft.world.phys.shapes.Shapes.empty(); ++ } ++ if (!tf | !ft) { ++ return tf ? first : second; ++ } ++ } ++ } ++ ++ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge( ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetX(), ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetX(), ++ ft, tf ++ ); ++ if (mergedX == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) { ++ return net.minecraft.world.phys.shapes.Shapes.empty(); ++ } ++ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge( ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetY(), ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetY(), ++ ft, tf ++ ); ++ if (mergedY == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) { ++ return net.minecraft.world.phys.shapes.Shapes.empty(); ++ } ++ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge( ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetZ(), ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetZ(), ++ ft, tf ++ ); ++ if (mergedZ == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) { ++ return net.minecraft.world.phys.shapes.Shapes.empty(); ++ } ++ ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getCachedVoxelData(); ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getCachedVoxelData(); ++ ++ final net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape mergedShape = merge( ++ shapeDataFirst, shapeDataSecond, ++ mergedX, mergedY, mergedZ, ++ makeBitset(ft, tf, tt) ++ ); ++ ++ if (mergedShape == null) { ++ return net.minecraft.world.phys.shapes.Shapes.empty(); ++ } ++ ++ return new net.minecraft.world.phys.shapes.ArrayVoxelShape( ++ mergedShape, mergedX.wrapCoords(), mergedY.wrapCoords(), mergedZ.wrapCoords() ++ ); ++ } ++ ++ public static boolean isJoinNonEmpty(final net.minecraft.world.phys.shapes.VoxelShape first, final net.minecraft.world.phys.shapes.VoxelShape second, final net.minecraft.world.phys.shapes.BooleanOp operator) { ++ final boolean ff = operator.apply(false, false); ++ if (ff) { ++ // technically, should be an infinite box but that's clearly an error ++ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true"); ++ } ++ final boolean firstEmpty = first.isEmpty(); ++ final boolean secondEmpty = second.isEmpty(); ++ if (firstEmpty | secondEmpty) { ++ return operator.apply(!firstEmpty, !secondEmpty); ++ } ++ ++ final boolean tt = operator.apply(true, true); ++ ++ if (first == second) { ++ return tt; ++ } ++ ++ final boolean ft = operator.apply(false, true); ++ final boolean tf = operator.apply(true, false); ++ ++ // try to check intersection ++ final net.minecraft.world.phys.AABB aabbF = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation(); ++ final net.minecraft.world.phys.AABB aabbS = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation(); ++ ++ final boolean intersect; ++ ++ final boolean hasAABBF = aabbF != null; ++ final boolean hasAABBS = aabbS != null; ++ if (hasAABBF | hasAABBS) { ++ if (hasAABBF & hasAABBS) { ++ intersect = voxelShapeIntersect(aabbF, aabbS); ++ } else if (hasAABBF) { ++ intersect = voxelShapeIntersectNoEmpty(second, aabbF); ++ } else { ++ // hasAABBS -> true ++ intersect = voxelShapeIntersectNoEmpty(first, aabbS); ++ } ++ ++ if (!intersect) { ++ // is only non-empty if we take from first or second, as there is no overlap AND both shapes are non-empty ++ return tf | ft; ++ } else if (tt) { ++ // intersect = true && tt = true -> non-empty merged shape ++ return true; ++ } ++ } else { ++ // expect cached bounds ++ intersect = voxelShapeIntersect(first.bounds(), second.bounds()); ++ if (!intersect) { ++ // is only non-empty if we take from first or second, as there is no intersection ++ return tf | ft; ++ } ++ } ++ ++ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge( ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetX(), ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetX(), ++ ft, tf ++ ); ++ if (mergedX == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) { ++ return false; ++ } ++ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge( ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetY(), ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetY(), ++ ft, tf ++ ); ++ if (mergedY == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) { ++ return false; ++ } ++ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge( ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetZ(), ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetZ(), ++ ft, tf ++ ); ++ if (mergedZ == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) { ++ return false; ++ } ++ ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getCachedVoxelData(); ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getCachedVoxelData(); ++ ++ return !isMergeEmpty( ++ shapeDataFirst, shapeDataSecond, ++ mergedX, mergedY, mergedZ, ++ makeBitset(ft, tf, tt) ++ ); ++ } ++ ++ private static final class MergedVoxelCoordinateList { ++ ++ private static final int[][] SIMPLE_INDICES_CACHE = new int[64][]; ++ static { ++ for (int i = 0; i < SIMPLE_INDICES_CACHE.length; ++i) { ++ SIMPLE_INDICES_CACHE[i] = getIndices(i); ++ } ++ } ++ ++ private static final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList EMPTY = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList( ++ new double[] { 0.0 }, 0.0, new int[0], new int[0], 0 ++ ); ++ ++ private static int[] getIndices(final int length) { ++ final int[] ret = new int[length]; ++ ++ for (int i = 1; i < length; ++i) { ++ ret[i] = i; ++ } ++ ++ return ret; ++ } ++ ++ // indices above voxel size are always set to -1 ++ public final double[] coordinates; ++ public final double coordinateOffset; ++ public final int[] firstIndices; ++ public final int[] secondIndices; ++ public final int voxels; ++ ++ private MergedVoxelCoordinateList(final double[] coordinates, final double coordinateOffset, ++ final int[] firstIndices, final int[] secondIndices, final int voxels) { ++ this.coordinates = coordinates; ++ this.coordinateOffset = coordinateOffset; ++ this.firstIndices = firstIndices; ++ this.secondIndices = secondIndices; ++ this.voxels = voxels; ++ } ++ ++ public it.unimi.dsi.fastutil.doubles.DoubleList wrapCoords() { ++ if (this.coordinateOffset == 0.0) { ++ return it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(this.coordinates, this.voxels + 1); ++ } ++ return new net.minecraft.world.phys.shapes.OffsetDoubleList(it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset); ++ } ++ ++ // assume coordinates.length > 1 ++ public static ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList getForSingle(final double[] coordinates, final double offset) { ++ final int voxels = coordinates.length - 1; ++ final int[] indices = voxels < SIMPLE_INDICES_CACHE.length ? SIMPLE_INDICES_CACHE[voxels] : getIndices(voxels); ++ ++ return new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels); ++ } ++ ++ // assume coordinates.length > 1 ++ public static ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList merge(final double[] firstCoordinates, final double firstOffset, ++ final double[] secondCoordinates, final double secondOffset, ++ final boolean ft, final boolean tf) { ++ if (firstCoordinates == secondCoordinates && firstOffset == secondOffset) { ++ return getForSingle(firstCoordinates, firstOffset); ++ } ++ ++ final int firstCount = firstCoordinates.length; ++ final int secondCount = secondCoordinates.length; ++ ++ final int voxelsFirst = firstCount - 1; ++ final int voxelsSecond = secondCount - 1; ++ ++ final int maxCount = firstCount + secondCount; ++ ++ final double[] coordinates = new double[maxCount]; ++ final int[] firstIndices = new int[maxCount]; ++ final int[] secondIndices = new int[maxCount]; ++ ++ final boolean notTF = !tf; ++ final boolean notFT = !ft; ++ ++ int firstIndex = 0; ++ int secondIndex = 0; ++ int resultSize = 0; ++ ++ // note: operations on NaN are false ++ double last = Double.NaN; ++ ++ for (;;) { ++ final boolean noneLeftFirst = firstIndex >= firstCount; ++ final boolean noneLeftSecond = secondIndex >= secondCount; ++ ++ if ((noneLeftFirst & noneLeftSecond) | (noneLeftSecond & notTF) | (noneLeftFirst & notFT)) { ++ break; ++ } ++ ++ final boolean firstZero = firstIndex == 0; ++ final boolean secondZero = secondIndex == 0; ++ ++ final double select; ++ ++ if (noneLeftFirst) { ++ // noneLeftSecond -> false ++ // notFT -> false ++ select = secondCoordinates[secondIndex] + secondOffset; ++ ++secondIndex; ++ } else if (noneLeftSecond) { ++ // noneLeftFirst -> false ++ // notTF -> false ++ select = firstCoordinates[firstIndex] + firstOffset; ++ ++firstIndex; ++ } else { ++ // noneLeftFirst | noneLeftSecond -> false ++ // notTF -> ?? ++ // notFT -> ?? ++ final boolean breakFirst = notTF & secondZero; ++ final boolean breakSecond = notFT & firstZero; ++ ++ final double first = firstCoordinates[firstIndex] + firstOffset; ++ final double second = secondCoordinates[secondIndex] + secondOffset; ++ final boolean useFirst = first < (second + COLLISION_EPSILON); ++ final boolean cont = (useFirst & breakFirst) | (!useFirst & breakSecond); ++ ++ select = useFirst ? first : second; ++ firstIndex += useFirst ? 1 : 0; ++ secondIndex += 1 ^ (useFirst ? 1 : 0); ++ ++ if (cont) { ++ continue; ++ } ++ } ++ ++ int prevFirst = firstIndex - 1; ++ prevFirst = prevFirst >= voxelsFirst ? -1 : prevFirst; ++ int prevSecond = secondIndex - 1; ++ prevSecond = prevSecond >= voxelsSecond ? -1 : prevSecond; ++ ++ if (last >= (select - COLLISION_EPSILON)) { ++ // note: any operations on NaN is false ++ firstIndices[resultSize - 1] = prevFirst; ++ secondIndices[resultSize - 1] = prevSecond; ++ } else { ++ firstIndices[resultSize] = prevFirst; ++ secondIndices[resultSize] = prevSecond; ++ coordinates[resultSize] = select; ++ ++ ++resultSize; ++ last = select; ++ } ++ } ++ ++ return resultSize <= 1 ? EMPTY : new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1); ++ } ++ } ++ ++ public static boolean equals(final net.minecraft.world.phys.shapes.DiscreteVoxelShape shape1, final net.minecraft.world.phys.shapes.DiscreteVoxelShape shape2) { ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData1 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape)shape1).moonrise$getOrCreateCachedShapeData(); ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData2 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape)shape2).moonrise$getOrCreateCachedShapeData(); ++ ++ final boolean isEmpty1 = cachedShapeData1.isEmpty(); ++ final boolean isEmpty2 = cachedShapeData2.isEmpty(); ++ ++ if (isEmpty1 & isEmpty2) { ++ return true; ++ } else if (isEmpty1 ^ isEmpty2) { ++ return false; ++ } ++ ++ if (cachedShapeData1.hasSingleAABB() != cachedShapeData2.hasSingleAABB()) { ++ return false; ++ } ++ ++ if (cachedShapeData1.sizeX() != cachedShapeData2.sizeX()) { ++ return false; ++ } ++ if (cachedShapeData1.sizeY() != cachedShapeData2.sizeY()) { ++ return false; ++ } ++ if (cachedShapeData1.sizeZ() != cachedShapeData2.sizeZ()) { ++ return false; ++ } ++ ++ return java.util.Arrays.equals(cachedShapeData1.voxelSet(), cachedShapeData2.voxelSet()); ++ } ++ ++ // useful only for testing ++ public static boolean equals(final net.minecraft.world.phys.shapes.VoxelShape shape1, final net.minecraft.world.phys.shapes.VoxelShape shape2) { ++ if (!equals(shape1.shape, shape2.shape)) { ++ return false; ++ } ++ ++ return shape1.getCoords(net.minecraft.core.Direction.Axis.X).equals(shape2.getCoords(net.minecraft.core.Direction.Axis.X)) && ++ shape1.getCoords(net.minecraft.core.Direction.Axis.Y).equals(shape2.getCoords(net.minecraft.core.Direction.Axis.Y)) && ++ shape1.getCoords(net.minecraft.core.Direction.Axis.Z).equals(shape2.getCoords(net.minecraft.core.Direction.Axis.Z)); ++ } ++ ++ public static net.minecraft.world.phys.AABB offsetX(final net.minecraft.world.phys.AABB box, final double dx) { ++ return new net.minecraft.world.phys.AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); ++ } ++ ++ public static net.minecraft.world.phys.AABB offsetY(final net.minecraft.world.phys.AABB box, final double dy) { ++ return new net.minecraft.world.phys.AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ); ++ } ++ ++ public static net.minecraft.world.phys.AABB offsetZ(final net.minecraft.world.phys.AABB box, final double dz) { ++ return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz); ++ } ++ ++ public static net.minecraft.world.phys.AABB expandRight(final net.minecraft.world.phys.AABB box, final double dx) { // dx > 0.0 ++ return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); ++ } ++ ++ public static net.minecraft.world.phys.AABB expandLeft(final net.minecraft.world.phys.AABB box, final double dx) { // dx < 0.0 ++ return new net.minecraft.world.phys.AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); ++ } ++ ++ public static net.minecraft.world.phys.AABB expandUpwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy > 0.0 ++ return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ); ++ } ++ ++ public static net.minecraft.world.phys.AABB expandDownwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy < 0.0 ++ return new net.minecraft.world.phys.AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ); ++ } ++ ++ public static net.minecraft.world.phys.AABB expandForwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz > 0.0 ++ return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz); ++ } ++ ++ public static net.minecraft.world.phys.AABB expandBackwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz < 0.0 ++ return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ); ++ } ++ ++ public static net.minecraft.world.phys.AABB cutRight(final net.minecraft.world.phys.AABB box, final double dx) { // dx > 0.0 ++ return new net.minecraft.world.phys.AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ); ++ } ++ ++ public static net.minecraft.world.phys.AABB cutLeft(final net.minecraft.world.phys.AABB box, final double dx) { // dx < 0.0 ++ return new net.minecraft.world.phys.AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ); ++ } ++ ++ public static net.minecraft.world.phys.AABB cutUpwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy > 0.0 ++ return new net.minecraft.world.phys.AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ); ++ } ++ ++ public static net.minecraft.world.phys.AABB cutDownwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy < 0.0 ++ return new net.minecraft.world.phys.AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ); ++ } ++ ++ public static net.minecraft.world.phys.AABB cutForwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz > 0.0 ++ return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz); ++ } ++ ++ public static net.minecraft.world.phys.AABB cutBackwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz < 0.0 ++ return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ); ++ } ++ ++ public static double performAABBCollisionsX(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final net.minecraft.world.phys.AABB target = potentialCollisions.get(i); ++ value = collideX(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performAABBCollisionsY(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final net.minecraft.world.phys.AABB target = potentialCollisions.get(i); ++ value = collideY(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performAABBCollisionsZ(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final net.minecraft.world.phys.AABB target = potentialCollisions.get(i); ++ value = collideZ(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performVoxelCollisionsX(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final net.minecraft.world.phys.shapes.VoxelShape target = potentialCollisions.get(i); ++ value = collideX(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performVoxelCollisionsY(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final net.minecraft.world.phys.shapes.VoxelShape target = potentialCollisions.get(i); ++ value = collideY(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performVoxelCollisionsZ(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final net.minecraft.world.phys.shapes.VoxelShape target = potentialCollisions.get(i); ++ value = collideZ(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static net.minecraft.world.phys.Vec3 performVoxelCollisions(final net.minecraft.world.phys.Vec3 moveVector, net.minecraft.world.phys.AABB axisalignedbb, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) { ++ double x = moveVector.x; ++ double y = moveVector.y; ++ double z = moveVector.z; ++ ++ if (y != 0.0) { ++ y = performVoxelCollisionsY(axisalignedbb, y, potentialCollisions); ++ if (y != 0.0) { ++ axisalignedbb = offsetY(axisalignedbb, y); ++ } ++ } ++ ++ final boolean xSmaller = Math.abs(x) < Math.abs(z); ++ ++ if (xSmaller && z != 0.0) { ++ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); ++ if (z != 0.0) { ++ axisalignedbb = offsetZ(axisalignedbb, z); ++ } ++ } ++ ++ if (x != 0.0) { ++ x = performVoxelCollisionsX(axisalignedbb, x, potentialCollisions); ++ if (!xSmaller && x != 0.0) { ++ axisalignedbb = offsetX(axisalignedbb, x); ++ } ++ } ++ ++ if (!xSmaller && z != 0.0) { ++ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); ++ } ++ ++ return new net.minecraft.world.phys.Vec3(x, y, z); ++ } ++ ++ public static net.minecraft.world.phys.Vec3 performAABBCollisions(final net.minecraft.world.phys.Vec3 moveVector, net.minecraft.world.phys.AABB axisalignedbb, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) { ++ double x = moveVector.x; ++ double y = moveVector.y; ++ double z = moveVector.z; ++ ++ if (y != 0.0) { ++ y = performAABBCollisionsY(axisalignedbb, y, potentialCollisions); ++ if (y != 0.0) { ++ axisalignedbb = offsetY(axisalignedbb, y); ++ } ++ } ++ ++ final boolean xSmaller = Math.abs(x) < Math.abs(z); ++ ++ if (xSmaller && z != 0.0) { ++ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); ++ if (z != 0.0) { ++ axisalignedbb = offsetZ(axisalignedbb, z); ++ } ++ } ++ ++ if (x != 0.0) { ++ x = performAABBCollisionsX(axisalignedbb, x, potentialCollisions); ++ if (!xSmaller && x != 0.0) { ++ axisalignedbb = offsetX(axisalignedbb, x); ++ } ++ } ++ ++ if (!xSmaller && z != 0.0) { ++ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); ++ } ++ ++ return new net.minecraft.world.phys.Vec3(x, y, z); ++ } ++ ++ public static net.minecraft.world.phys.Vec3 performCollisions(final net.minecraft.world.phys.Vec3 moveVector, net.minecraft.world.phys.AABB axisalignedbb, ++ final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> voxels, ++ final java.util.List<net.minecraft.world.phys.AABB> aabbs) { ++ if (voxels.isEmpty()) { ++ // fast track only AABBs ++ return performAABBCollisions(moveVector, axisalignedbb, aabbs); ++ } ++ ++ double x = moveVector.x; ++ double y = moveVector.y; ++ double z = moveVector.z; ++ ++ if (y != 0.0) { ++ y = performAABBCollisionsY(axisalignedbb, y, aabbs); ++ y = performVoxelCollisionsY(axisalignedbb, y, voxels); ++ if (y != 0.0) { ++ axisalignedbb = offsetY(axisalignedbb, y); ++ } ++ } ++ ++ final boolean xSmaller = Math.abs(x) < Math.abs(z); ++ ++ if (xSmaller && z != 0.0) { ++ z = performAABBCollisionsZ(axisalignedbb, z, aabbs); ++ z = performVoxelCollisionsZ(axisalignedbb, z, voxels); ++ if (z != 0.0) { ++ axisalignedbb = offsetZ(axisalignedbb, z); ++ } ++ } ++ ++ if (x != 0.0) { ++ x = performAABBCollisionsX(axisalignedbb, x, aabbs); ++ x = performVoxelCollisionsX(axisalignedbb, x, voxels); ++ if (!xSmaller && x != 0.0) { ++ axisalignedbb = offsetX(axisalignedbb, x); ++ } ++ } ++ ++ if (!xSmaller && z != 0.0) { ++ z = performAABBCollisionsZ(axisalignedbb, z, aabbs); ++ z = performVoxelCollisionsZ(axisalignedbb, z, voxels); ++ } ++ ++ return new net.minecraft.world.phys.Vec3(x, y, z); ++ } ++ ++ public static boolean isCollidingWithBorder(final net.minecraft.world.level.border.WorldBorder worldborder, final net.minecraft.world.phys.AABB boundingBox) { ++ return isCollidingWithBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); ++ } ++ ++ public static boolean isCollidingWithBorder(final net.minecraft.world.level.border.WorldBorder worldborder, ++ final double boxMinX, final double boxMaxX, ++ final double boxMinZ, final double boxMaxZ) { ++ final double borderMinX = Math.floor(worldborder.getMinX()); // -X ++ final double borderMaxX = Math.ceil(worldborder.getMaxX()); // +X ++ ++ final double borderMinZ = Math.floor(worldborder.getMinZ()); // -Z ++ final double borderMaxZ = Math.ceil(worldborder.getMaxZ()); // +Z ++ ++ // inverted check for world border enclosing the specified box expanded by -EPSILON ++ return (borderMinX - boxMinX) > ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON || (borderMaxX - boxMaxX) < -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON || ++ (borderMinZ - boxMinZ) > ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON || (borderMaxZ - boxMaxZ) < -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON; ++ } ++ ++ /* Math.max/min specify that any NaN argument results in a NaN return, unlike these functions */ ++ private static double min(final double x, final double y) { ++ return x < y ? x : y; ++ } ++ ++ private static double max(final double x, final double y) { ++ return x > y ? x : y; ++ } ++ ++ public static final int COLLISION_FLAG_LOAD_CHUNKS = 1 << 0; ++ public static final int COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS = 1 << 1; ++ public static final int COLLISION_FLAG_CHECK_BORDER = 1 << 2; ++ public static final int COLLISION_FLAG_CHECK_ONLY = 1 << 3; ++ ++ public static boolean getCollisionsForBlocksOrWorldBorder(final net.minecraft.world.level.Level world, final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB aabb, ++ final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> intoVoxel, final java.util.List<net.minecraft.world.phys.AABB> intoAABB, ++ final int collisionFlags, final java.util.function.BiPredicate<net.minecraft.world.level.block.state.BlockState, net.minecraft.core.BlockPos> predicate) { ++ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; ++ boolean ret = false; ++ ++ if ((collisionFlags & COLLISION_FLAG_CHECK_BORDER) != 0) { ++ final net.minecraft.world.level.border.WorldBorder worldBorder = world.getWorldBorder(); ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) { ++ if (checkOnly) { ++ return true; ++ } else { ++ final net.minecraft.world.phys.shapes.VoxelShape borderShape = worldBorder.getCollisionShape(); ++ intoVoxel.add(borderShape); ++ ret = true; ++ } ++ } ++ } ++ ++ final int minSection = ((ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel)world).moonrise$getMinSection(); ++ ++ final int minBlockX = net.minecraft.util.Mth.floor(aabb.minX - COLLISION_EPSILON) - 1; ++ final int maxBlockX = net.minecraft.util.Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1; ++ ++ final int minBlockY = Math.max((minSection << 4) - 1, net.minecraft.util.Mth.floor(aabb.minY - COLLISION_EPSILON) - 1); ++ final int maxBlockY = Math.min((((ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel)world).moonrise$getMaxSection() << 4) + 16, net.minecraft.util.Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1); ++ ++ final int minBlockZ = net.minecraft.util.Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1; ++ final int maxBlockZ = net.minecraft.util.Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1; ++ ++ final net.minecraft.core.BlockPos.MutableBlockPos mutablePos = new net.minecraft.core.BlockPos.MutableBlockPos(); ++ final net.minecraft.world.phys.shapes.CollisionContext collisionShape = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(entity); ++ ++ // special cases: ++ if (minBlockY > maxBlockY) { ++ // no point in checking ++ return ret; ++ } ++ ++ final int minChunkX = minBlockX >> 4; ++ final int maxChunkX = maxBlockX >> 4; ++ ++ final int minChunkY = minBlockY >> 4; ++ final int maxChunkY = maxBlockY >> 4; ++ ++ final int minChunkZ = minBlockZ >> 4; ++ final int maxChunkZ = maxBlockZ >> 4; ++ ++ final boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0; ++ final net.minecraft.world.level.chunk.ChunkSource chunkSource = world.getChunkSource(); ++ ++ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { ++ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { ++ final net.minecraft.world.level.chunk.ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, loadChunks); ++ ++ if (chunk == null) { ++ if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) { ++ if (checkOnly) { ++ return true; ++ } else { ++ intoAABB.add(getBoxForChunk(currChunkX, currChunkZ)); ++ ret = true; ++ } ++ } ++ continue; ++ } ++ ++ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections(); ++ ++ // bound y ++ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { ++ final int sectionIdx = currChunkY - minSection; ++ if (sectionIdx < 0 || sectionIdx >= sections.length) { ++ continue; ++ } ++ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx]; ++ if (section == null || section.hasOnlyAir()) { ++ // empty ++ continue; ++ } ++ ++ final boolean hasSpecial = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getSpecialCollidingBlocks() != 0; ++ final int sectionAdjust = !hasSpecial ? 1 : 0; ++ ++ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states; ++ ++ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0; ++ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15; ++ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) + sectionAdjust : 0; ++ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) - sectionAdjust : 15; ++ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) + sectionAdjust : 0; ++ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) - sectionAdjust : 15; ++ ++ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { ++ final int blockY = currY | (currChunkY << 4); ++ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { ++ final int blockZ = currZ | (currChunkZ << 4); ++ for (int currX = minXIterate; currX <= maxXIterate; ++currX) { ++ final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8); ++ final int blockX = currX | (currChunkX << 4); ++ ++ final int edgeCount = hasSpecial ? ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + ++ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + ++ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0) : 0; ++ if (edgeCount == 3) { ++ continue; ++ } ++ ++ final net.minecraft.world.level.block.state.BlockState blockData = blocks.get(localBlockIndex); ++ ++ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$emptyCollisionShape()) { ++ continue; ++ } ++ ++ net.minecraft.world.phys.shapes.VoxelShape blockCollision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$getConstantCollisionShape(); ++ ++ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == net.minecraft.world.level.block.Blocks.MOVING_PISTON))) { ++ if (blockCollision == null) { ++ mutablePos.set(blockX, blockY, blockZ); ++ blockCollision = blockData.getCollisionShape(world, mutablePos, collisionShape); ++ } ++ ++ net.minecraft.world.phys.AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation(); ++ if (singleAABB != null) { ++ singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ); ++ if (!voxelShapeIntersect(aabb, singleAABB)) { ++ continue; ++ } ++ ++ if (predicate != null) { ++ mutablePos.set(blockX, blockY, blockZ); ++ if (!predicate.test(blockData, mutablePos)) { ++ continue; ++ } ++ } ++ ++ if (checkOnly) { ++ return true; ++ } else { ++ ret = true; ++ intoAABB.add(singleAABB); ++ continue; ++ } ++ } ++ ++ if (blockCollision.isEmpty()) { ++ continue; ++ } ++ ++ final net.minecraft.world.phys.shapes.VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ); ++ ++ if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) { ++ continue; ++ } ++ ++ if (predicate != null) { ++ mutablePos.set(blockX, blockY, blockZ); ++ if (!predicate.test(blockData, mutablePos)) { ++ continue; ++ } ++ } ++ ++ if (checkOnly) { ++ return true; ++ } else { ++ ret = true; ++ intoVoxel.add(blockCollisionOffset); ++ continue; ++ } ++ } ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public static boolean getEntityHardCollisions(final net.minecraft.world.level.Level world, final net.minecraft.world.entity.Entity entity, net.minecraft.world.phys.AABB aabb, ++ final java.util.List<net.minecraft.world.phys.AABB> into, final int collisionFlags, final java.util.function.Predicate<net.minecraft.world.entity.Entity> predicate) { ++ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; ++ ++ boolean ret = false; ++ ++ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with. ++ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems ++ // specifically with boat collisions. ++ aabb = aabb.inflate(-COLLISION_EPSILON, -COLLISION_EPSILON, -COLLISION_EPSILON); ++ final java.util.List<net.minecraft.world.entity.Entity> entities; ++ if (entity != null && ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$isHardColliding()) { ++ entities = world.getEntities(entity, aabb, predicate); ++ } else { ++ entities = ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter)world).moonrise$getHardCollidingEntities(entity, aabb, predicate); ++ } ++ ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ final net.minecraft.world.entity.Entity otherEntity = entities.get(i); ++ ++ if (otherEntity.isSpectator()) { ++ continue; ++ } ++ ++ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) { ++ if (checkOnly) { ++ return true; ++ } else { ++ into.add(otherEntity.getBoundingBox()); ++ ret = true; ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public static boolean getCollisions(final net.minecraft.world.level.Level world, final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB aabb, ++ final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> intoVoxel, final java.util.List<net.minecraft.world.phys.AABB> intoAABB, final int collisionFlags, ++ final java.util.function.BiPredicate<net.minecraft.world.level.block.state.BlockState, net.minecraft.core.BlockPos> blockPredicate, ++ final java.util.function.Predicate<net.minecraft.world.entity.Entity> entityPredicate) { ++ if ((collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0) { ++ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) ++ || getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); ++ } else { ++ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) ++ | getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); ++ } ++ } ++ ++ public static final class LazyEntityCollisionContext extends net.minecraft.world.phys.shapes.EntityCollisionContext { ++ ++ private net.minecraft.world.phys.shapes.CollisionContext delegate; ++ private boolean delegated; ++ ++ public LazyEntityCollisionContext(final net.minecraft.world.entity.Entity entity) { ++ super(false, 0.0, null, null, entity); ++ } ++ ++ public boolean isDelegated() { ++ final boolean delegated = this.delegated; ++ this.delegated = false; ++ return delegated; ++ } ++ ++ public net.minecraft.world.phys.shapes.CollisionContext getDelegate() { ++ this.delegated = true; ++ final net.minecraft.world.entity.Entity entity = this.getEntity(); ++ return this.delegate == null ? this.delegate = (entity == null ? net.minecraft.world.phys.shapes.CollisionContext.empty() : net.minecraft.world.phys.shapes.CollisionContext.of(entity)) : this.delegate; ++ } ++ ++ @Override ++ public boolean isDescending() { ++ return this.getDelegate().isDescending(); ++ } ++ ++ @Override ++ public boolean isAbove(final net.minecraft.world.phys.shapes.VoxelShape shape, final net.minecraft.core.BlockPos pos, final boolean defaultValue) { ++ return this.getDelegate().isAbove(shape, pos, defaultValue); ++ } ++ ++ @Override ++ public boolean isHoldingItem(final net.minecraft.world.item.Item item) { ++ return this.getDelegate().isHoldingItem(item); ++ } ++ ++ @Override ++ public boolean canStandOnFluid(final net.minecraft.world.level.material.FluidState state, final net.minecraft.world.level.material.FluidState fluidState) { ++ return this.getDelegate().canStandOnFluid(state, fluidState); ++ } ++ } ++ ++ private CollisionUtil() { ++ throw new RuntimeException(); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..eb7200657d5c7ac37ee93868ba43be0aefecac6d +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java +@@ -0,0 +1,23 @@ ++package ca.spottedleaf.moonrise.patches.collisions; ++ ++public final class ExplosionBlockCache { ++ ++ public final long key; ++ public final net.minecraft.core.BlockPos immutablePos; ++ public final net.minecraft.world.level.block.state.BlockState blockState; ++ public final net.minecraft.world.level.material.FluidState fluidState; ++ public final float resistance; ++ public final boolean outOfWorld; ++ public Boolean shouldExplode; // null -> not called yet ++ public net.minecraft.world.phys.shapes.VoxelShape cachedCollisionShape; ++ ++ public ExplosionBlockCache(final long key, final net.minecraft.core.BlockPos immutablePos, final net.minecraft.world.level.block.state.BlockState blockState, ++ final net.minecraft.world.level.material.FluidState fluidState, final float resistance, final boolean outOfWorld) { ++ this.key = key; ++ this.immutablePos = immutablePos; ++ this.blockState = blockState; ++ this.fluidState = fluidState; ++ this.resistance = resistance; ++ this.outOfWorld = outOfWorld; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java +new file mode 100644 +index 0000000000000000000000000000000000000000..02b29c563a298e06186de010de68a716bccba494 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java +@@ -0,0 +1,26 @@ ++package ca.spottedleaf.moonrise.patches.collisions.block; ++ ++public interface CollisionBlockState { ++ ++ // note: this does not consider canOcclude, it is only based on the cached collision shape (i.e hasCache()) ++ // and whether Shapes.faceShapeOccludes(EMPTY, cached shape) is true ++ public boolean moonrise$occludesFullBlock(); ++ ++ // whether the cached collision shape exists and is empty ++ public boolean moonrise$emptyCollisionShape(); ++ ++ // indicates that occludesFullBlock is cached for the collision shape ++ public boolean moonrise$hasCache(); ++ ++ // note: this is HashCommon#murmurHash3(incremental id); and since murmurHash3 has an inverse function the returned ++ // value is still unique ++ public int moonrise$uniqueId1(); ++ ++ // note: this is HashCommon#murmurHash3(incremental id); and since murmurHash3 has an inverse function the returned ++ // value is still unique ++ public int moonrise$uniqueId2(); ++ ++ public net.minecraft.world.phys.shapes.VoxelShape moonrise$getConstantCollisionShape(); ++ ++ public net.minecraft.world.phys.AABB moonrise$getConstantCollisionAABB(); ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5a6b16be4b8c0cc92d017bc592bc4818dba17da7 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java +@@ -0,0 +1,10 @@ ++package ca.spottedleaf.moonrise.patches.collisions.shape; ++ ++public record CachedShapeData( ++ int sizeX, int sizeY, int sizeZ, ++ long[] voxelSet, ++ int minFullX, int minFullY, int minFullZ, ++ int maxFullX, int maxFullY, int maxFullZ, ++ boolean isEmpty, boolean hasSingleAABB ++) { ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5fe1dad9dad368911aedbe6ba7fcd8f9b0189d32 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java +@@ -0,0 +1,35 @@ ++package ca.spottedleaf.moonrise.patches.collisions.shape; ++ ++public record CachedToAABBs( ++ java.util.List<net.minecraft.world.phys.AABB> aabbs, ++ boolean isOffset, ++ double offX, double offY, double offZ ++) { ++ ++ public ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs removeOffset() { ++ final java.util.List<net.minecraft.world.phys.AABB> toOffset = this.aabbs; ++ final double offX = this.offX; ++ final double offY = this.offY; ++ final double offZ = this.offZ; ++ ++ final java.util.List<net.minecraft.world.phys.AABB> ret = new java.util.ArrayList<>(toOffset.size()); ++ ++ for (int i = 0, len = toOffset.size(); i < len; ++i) { ++ ret.add(toOffset.get(i).move(offX, offY, offZ)); ++ } ++ ++ return new ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs(ret, false, 0.0, 0.0, 0.0); ++ } ++ ++ public static ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs offset(final ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cache, final double offX, final double offY, final double offZ) { ++ if (offX == 0.0 && offY == 0.0 && offZ == 0.0) { ++ return cache; ++ } ++ ++ final double resX = cache.offX + offX; ++ final double resY = cache.offY + offY; ++ final double resZ = cache.offZ + offZ; ++ ++ return new ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs(cache.aabbs, true, resX, resY, resZ); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a09efadea9b733840bbe69830dd8f2a303fe656f +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java +@@ -0,0 +1,7 @@ ++package ca.spottedleaf.moonrise.patches.collisions.shape; ++ ++public interface CollisionDiscreteVoxelShape { ++ ++ public ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getOrCreateCachedShapeData(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java +new file mode 100644 +index 0000000000000000000000000000000000000000..70371eb87c11a106e8513cdbc8d938dda088f745 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java +@@ -0,0 +1,36 @@ ++package ca.spottedleaf.moonrise.patches.collisions.shape; ++ ++public interface CollisionVoxelShape { ++ ++ public double moonrise$offsetX(); ++ ++ public double moonrise$offsetY(); ++ ++ public double moonrise$offsetZ(); ++ ++ public double[] moonrise$rootCoordinatesX(); ++ ++ public double[] moonrise$rootCoordinatesY(); ++ ++ public double[] moonrise$rootCoordinatesZ(); ++ ++ public ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getCachedVoxelData(); ++ ++ // rets null if not possible to represent this shape as one AABB ++ public net.minecraft.world.phys.AABB moonrise$getSingleAABBRepresentation(); ++ ++ // ONLY USE INTERNALLY, ONLY FOR INITIALISING IN CONSTRUCTOR: VOXELSHAPES ARE STATIC ++ public void moonrise$initCache(); ++ ++ // this returns empty if not clamped to 1.0 or 0.0 depending on direction ++ public net.minecraft.world.phys.shapes.VoxelShape moonrise$getFaceShapeClamped(final net.minecraft.core.Direction direction); ++ ++ public boolean moonrise$isFullBlock(); ++ ++ public boolean moonrise$occludesFullBlock(); ++ ++ public boolean moonrise$occludesFullBlockIfCached(); ++ ++ // uses a cache internally ++ public net.minecraft.world.phys.shapes.VoxelShape moonrise$orUnoptimized(final net.minecraft.world.phys.shapes.VoxelShape other); ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4217426d3eca5e5cd2bc37e509f84da1d6fed0b2 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java +@@ -0,0 +1,8 @@ ++package ca.spottedleaf.moonrise.patches.collisions.shape; ++ ++public record MergedORCache( ++ net.minecraft.world.phys.shapes.VoxelShape key, ++ net.minecraft.world.phys.shapes.VoxelShape result ++) { ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f62359e5d6aa9a9cdb015441dbdb6182dc302f02 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/CollisionDirection.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.collisions.util; ++ ++public interface CollisionDirection { ++ ++ // note: this is HashCommon#murmurHash3(some unique id) and since murmurHash3 has an inverse function the returned ++ // value is still unique ++ public int moonrise$uniqueId(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java +new file mode 100644 +index 0000000000000000000000000000000000000000..673103f160cbe577c6e05f998706af4e6850011b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java +@@ -0,0 +1,225 @@ ++package ca.spottedleaf.moonrise.patches.collisions.util; ++ ++import java.util.Iterator; ++import java.util.Optional; ++import java.util.Spliterator; ++import java.util.stream.Stream; ++ ++public final class EmptyStreamForMoveCall<T> implements java.util.stream.Stream<T> { ++ ++ public static final ca.spottedleaf.moonrise.patches.collisions.util.EmptyStreamForMoveCall INSTANCE = new ca.spottedleaf.moonrise.patches.collisions.util.EmptyStreamForMoveCall(); ++ ++ @Override ++ public boolean noneMatch(java.util.function.Predicate<? super T> predicate) { ++ return false; // important: ret false so the branch is never taken by mojang code ++ } ++ ++ @Override ++ public java.util.stream.Stream<T> filter(java.util.function.Predicate<? super T> predicate) { ++ return null; ++ } ++ ++ @Override ++ public <R> java.util.stream.Stream<R> map(java.util.function.Function<? super T, ? extends R> mapper) { ++ return null; ++ } ++ ++ @Override ++ public java.util.stream.IntStream mapToInt(java.util.function.ToIntFunction<? super T> mapper) { ++ return null; ++ } ++ ++ @Override ++ public java.util.stream.LongStream mapToLong(java.util.function.ToLongFunction<? super T> mapper) { ++ return null; ++ } ++ ++ @Override ++ public java.util.stream.DoubleStream mapToDouble(java.util.function.ToDoubleFunction<? super T> mapper) { ++ return null; ++ } ++ ++ @Override ++ public <R> java.util.stream.Stream<R> flatMap(java.util.function.Function<? super T, ? extends java.util.stream.Stream<? extends R>> mapper) { ++ return null; ++ } ++ ++ @Override ++ public java.util.stream.IntStream flatMapToInt(java.util.function.Function<? super T, ? extends java.util.stream.IntStream> mapper) { ++ return null; ++ } ++ ++ @Override ++ public java.util.stream.LongStream flatMapToLong(java.util.function.Function<? super T, ? extends java.util.stream.LongStream> mapper) { ++ return null; ++ } ++ ++ @Override ++ public java.util.stream.DoubleStream flatMapToDouble(java.util.function.Function<? super T, ? extends java.util.stream.DoubleStream> mapper) { ++ return null; ++ } ++ ++ @Override ++ public java.util.stream.Stream<T> distinct() { ++ return null; ++ } ++ ++ @Override ++ public java.util.stream.Stream<T> sorted() { ++ return null; ++ } ++ ++ @Override ++ public java.util.stream.Stream<T> sorted(java.util.Comparator<? super T> comparator) { ++ return null; ++ } ++ ++ @Override ++ public java.util.stream.Stream<T> peek(java.util.function.Consumer<? super T> action) { ++ return null; ++ } ++ ++ @Override ++ public java.util.stream.Stream<T> limit(long maxSize) { ++ return null; ++ } ++ ++ @Override ++ public java.util.stream.Stream<T> skip(long n) { ++ return null; ++ } ++ ++ @Override ++ public void forEach(java.util.function.Consumer<? super T> action) { ++ ++ } ++ ++ @Override ++ public void forEachOrdered(java.util.function.Consumer<? super T> action) { ++ ++ } ++ ++ @org.jetbrains.annotations.NotNull ++ @Override ++ public Object[] toArray() { ++ return new Object[0]; ++ } ++ ++ @org.jetbrains.annotations.NotNull ++ @Override ++ public <A> A[] toArray(java.util.function.IntFunction<A[]> generator) { ++ return null; ++ } ++ ++ @Override ++ public T reduce(T identity, java.util.function.BinaryOperator<T> accumulator) { ++ return null; ++ } ++ ++ @org.jetbrains.annotations.NotNull ++ @Override ++ public Optional<T> reduce(java.util.function.BinaryOperator<T> accumulator) { ++ return java.util.Optional.empty(); ++ } ++ ++ @Override ++ public <U> U reduce(U identity, java.util.function.BiFunction<U, ? super T, U> accumulator, java.util.function.BinaryOperator<U> combiner) { ++ return null; ++ } ++ ++ @Override ++ public <R> R collect(java.util.function.Supplier<R> supplier, java.util.function.BiConsumer<R, ? super T> accumulator, java.util.function.BiConsumer<R, R> combiner) { ++ return null; ++ } ++ ++ @Override ++ public <R, A> R collect(java.util.stream.Collector<? super T, A, R> collector) { ++ return null; ++ } ++ ++ @org.jetbrains.annotations.NotNull ++ @Override ++ public Optional<T> min(java.util.Comparator<? super T> comparator) { ++ return java.util.Optional.empty(); ++ } ++ ++ @org.jetbrains.annotations.NotNull ++ @Override ++ public Optional<T> max(java.util.Comparator<? super T> comparator) { ++ return java.util.Optional.empty(); ++ } ++ ++ @Override ++ public long count() { ++ return 0; ++ } ++ ++ @Override ++ public boolean anyMatch(java.util.function.Predicate<? super T> predicate) { ++ return false; ++ } ++ ++ @Override ++ public boolean allMatch(java.util.function.Predicate<? super T> predicate) { ++ return false; ++ } ++ ++ @org.jetbrains.annotations.NotNull ++ @Override ++ public Optional<T> findFirst() { ++ return java.util.Optional.empty(); ++ } ++ ++ @org.jetbrains.annotations.NotNull ++ @Override ++ public Optional<T> findAny() { ++ return java.util.Optional.empty(); ++ } ++ ++ ++ @org.jetbrains.annotations.NotNull ++ @Override ++ public Iterator<T> iterator() { ++ return null; ++ } ++ ++ @org.jetbrains.annotations.NotNull ++ @Override ++ public Spliterator<T> spliterator() { ++ return null; ++ } ++ ++ @Override ++ public boolean isParallel() { ++ return false; ++ } ++ ++ @org.jetbrains.annotations.NotNull ++ @Override ++ public Stream<T> sequential() { ++ return null; ++ } ++ ++ @org.jetbrains.annotations.NotNull ++ @Override ++ public Stream<T> parallel() { ++ return null; ++ } ++ ++ @org.jetbrains.annotations.NotNull ++ @Override ++ public Stream<T> unordered() { ++ return null; ++ } ++ ++ @org.jetbrains.annotations.NotNull ++ @Override ++ public Stream<T> onClose(Runnable closeHandler) { ++ return null; ++ } ++ ++ @Override ++ public void close() { ++ ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java +new file mode 100644 +index 0000000000000000000000000000000000000000..128267ff40b38c7b3ea0feb5133825cc6aae075b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java +@@ -0,0 +1,4 @@ ++package ca.spottedleaf.moonrise.patches.collisions.util; ++ ++public record FluidOcclusionCacheKey(net.minecraft.world.level.block.state.BlockState first, net.minecraft.world.level.block.state.BlockState second, net.minecraft.core.Direction direction, boolean result) { ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e851e81e13edbad6316df63fcb7095d48f85c5b0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.collisions.world; ++ ++public interface CollisionLevel { ++ ++ public int moonrise$getMinSection(); ++ ++ public int moonrise$getMaxSection(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5f5734c00ce8245a1ff69b2d4c3036579d5392e0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java +@@ -0,0 +1,11 @@ ++package ca.spottedleaf.moonrise.patches.entity_tracker; ++ ++import net.minecraft.server.level.ChunkMap; ++ ++public interface EntityTrackerEntity { ++ ++ public ChunkMap.TrackedEntity moonrise$getTrackedEntity(); ++ ++ public void moonrise$setTrackedEntity(final ChunkMap.TrackedEntity trackedEntity); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1fa07bef57d82c6d5242aaaf66011f0913515231 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java +@@ -0,0 +1,13 @@ ++package ca.spottedleaf.moonrise.patches.entity_tracker; ++ ++import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; ++ ++public interface EntityTrackerTrackedEntity { ++ ++ public void moonrise$tick(final NearbyPlayers.TrackedChunk chunk); ++ ++ public void moonrise$removeNonTickThreadPlayers(); ++ ++ public void moonrise$clearPlayers(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2bfdf3721db9a45e36538d71cbefcb1d339e6c58 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.moonrise.patches.starlight.blockstate; ++ ++public interface StarlightAbstractBlockState { ++ ++ public boolean starlight$isConditionallyFullOpaque(); ++ ++ public int starlight$getOpacityIfCached(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ed80017c8f257b981d626a37ffc5480d9b326558 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java +@@ -0,0 +1,18 @@ ++package ca.spottedleaf.moonrise.patches.starlight.chunk; ++ ++import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray; ++ ++public interface StarlightChunk { ++ ++ public SWMRNibbleArray[] starlight$getBlockNibbles(); ++ public void starlight$setBlockNibbles(final SWMRNibbleArray[] nibbles); ++ ++ public SWMRNibbleArray[] starlight$getSkyNibbles(); ++ public void starlight$setSkyNibbles(final SWMRNibbleArray[] nibbles); ++ ++ public boolean[] starlight$getSkyEmptinessMap(); ++ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap); ++ ++ public boolean[] starlight$getBlockEmptinessMap(); ++ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap); ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java +new file mode 100644 +index 0000000000000000000000000000000000000000..154443ac1ee1d6d18b8ff0f40a307d638b213aeb +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java +@@ -0,0 +1,277 @@ ++package ca.spottedleaf.moonrise.patches.starlight.light; ++ ++import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState; ++import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; ++import net.minecraft.core.BlockPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.LevelChunkSection; ++import net.minecraft.world.level.chunk.LightChunkGetter; ++import net.minecraft.world.level.chunk.PalettedContainer; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.phys.shapes.Shapes; ++import net.minecraft.world.phys.shapes.VoxelShape; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Set; ++ ++public final class BlockStarLightEngine extends StarLightEngine { ++ ++ public BlockStarLightEngine(final Level world) { ++ super(false, world); ++ } ++ ++ @Override ++ protected boolean[] getEmptinessMap(final ChunkAccess chunk) { ++ return ((StarlightChunk)chunk).starlight$getBlockEmptinessMap(); ++ } ++ ++ @Override ++ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { ++ ((StarlightChunk)chunk).starlight$setBlockEmptinessMap(to); ++ } ++ ++ @Override ++ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { ++ return ((StarlightChunk)chunk).starlight$getBlockNibbles(); ++ } ++ ++ @Override ++ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { ++ ((StarlightChunk)chunk).starlight$setBlockNibbles(to); ++ } ++ ++ @Override ++ protected boolean canUseChunk(final ChunkAccess chunk) { ++ return chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); ++ } ++ ++ @Override ++ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { ++ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); ++ if (nibble != null) { ++ // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically ++ // because a block was removed - which can decrease light. with sky data, block breaking can only result ++ // in increases, and thus the existing sky block check will actually correctly propagate light through ++ // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove ++ // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running ++ // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence ++ // of vanilla data management we "hide" them. ++ nibble.setHidden(); ++ } ++ } ++ ++ @Override ++ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) { ++ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) { ++ return; ++ } ++ ++ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); ++ if (nibble == null) { ++ if (!initRemovedNibbles) { ++ throw new IllegalStateException(); ++ } else { ++ this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray()); ++ } ++ } else { ++ nibble.setNonNull(); ++ } ++ } ++ ++ @Override ++ protected final void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) { ++ // blocks can change opacity ++ // blocks can change emitted light ++ // blocks can change direction of propagation ++ ++ final int encodeOffset = this.coordinateOffset; ++ final int emittedMask = this.emittedLightMask; ++ ++ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); ++ final BlockState blockState = this.getBlockState(worldX, worldY, worldZ); ++ final int emittedLevel = blockState.getLightEmission() & emittedMask; ++ ++ this.setLightLevel(worldX, worldY, worldZ, emittedLevel); ++ // this accounts for change in emitted light that would cause an increase ++ if (emittedLevel != 0) { ++ this.appendToIncreaseQueue( ++ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | (emittedLevel & 0xFL) << (6 + 6 + 16) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) ++ ); ++ } ++ // this also accounts for a change in emitted light that would cause a decrease ++ // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa) ++ // as it checks all neighbours (even if current level is 0) ++ this.appendToDecreaseQueue( ++ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | (currentLevel & 0xFL) << (6 + 6 + 16) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ // always keep sided transparent false here, new block might be conditionally transparent which would ++ // prevent us from decreasing sources in the directions where the new block is opaque ++ // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always ++ // catch that and fix it. ++ ); ++ // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block ++ } ++ ++ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); ++ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); ++ ++ @Override ++ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, ++ final int expect) { ++ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); ++ int level = centerState.getLightEmission() & 0xF; ++ ++ if (level >= (15 - 1) || level > expect) { ++ return level; ++ } ++ ++ final int sectionOffset = this.chunkSectionIndexOffset; ++ final BlockState conditionallyOpaqueState; ++ int opacity = ((StarlightAbstractBlockState)centerState).starlight$getOpacityIfCached(); ++ ++ if (opacity == -1) { ++ this.recalcCenterPos.set(worldX, worldY, worldZ); ++ opacity = centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos); ++ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) { ++ conditionallyOpaqueState = centerState; ++ } else { ++ conditionallyOpaqueState = null; ++ } ++ } else if (opacity >= 15) { ++ return level; ++ } else { ++ conditionallyOpaqueState = null; ++ } ++ opacity = Math.max(1, opacity); ++ ++ for (final AxisDirection direction : AXIS_DIRECTIONS) { ++ final int offX = worldX + direction.x; ++ final int offY = worldY + direction.y; ++ final int offZ = worldZ + direction.z; ++ ++ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; ++ ++ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); ++ ++ if ((neighbourLevel - 1) <= level) { ++ // don't need to test transparency, we know it wont affect the result. ++ continue; ++ } ++ ++ final BlockState neighbourState = this.getBlockState(offX, offY, offZ); ++ if (((StarlightAbstractBlockState)neighbourState).starlight$isConditionallyFullOpaque()) { ++ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that ++ // we don't read the blockstate because most of the time this is false, so using the faster ++ // known transparency lookup results in a net win ++ this.recalcNeighbourPos.set(offX, offY, offZ); ++ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); ++ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); ++ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { ++ // not allowed to propagate ++ continue; ++ } ++ } ++ ++ // passed transparency, ++ ++ final int calculated = neighbourLevel - opacity; ++ level = Math.max(calculated, level); ++ if (level > expect) { ++ return level; ++ } ++ } ++ ++ return level; ++ } ++ ++ @Override ++ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions) { ++ for (final BlockPos pos : positions) { ++ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); ++ } ++ ++ this.performLightDecrease(lightAccess); ++ } ++ ++ protected List<BlockPos> getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) { ++ final List<BlockPos> sources = new ArrayList<>(); ++ ++ final int offX = chunk.getPos().x << 4; ++ final int offZ = chunk.getPos().z << 4; ++ ++ final LevelChunkSection[] sections = chunk.getSections(); ++ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { ++ final LevelChunkSection section = sections[sectionY - this.minSection]; ++ if (section == null || section.hasOnlyAir()) { ++ // no sources in empty sections ++ continue; ++ } ++ if (!section.maybeHas((final BlockState state) -> { ++ return state.getLightEmission() > 0; ++ })) { ++ // no light sources in palette ++ continue; ++ } ++ final PalettedContainer<BlockState> states = section.states; ++ final int offY = sectionY << 4; ++ ++ for (int index = 0; index < (16 * 16 * 16); ++index) { ++ final BlockState state = states.get(index); ++ if (state.getLightEmission() <= 0) { ++ continue; ++ } ++ ++ // index = x | (z << 4) | (y << 8) ++ sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15))); ++ } ++ } ++ ++ return sources; ++ } ++ ++ @Override ++ public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { ++ // setup sources ++ final int emittedMask = this.emittedLightMask; ++ final List<BlockPos> positions = this.getSources(lightAccess, chunk); ++ for (int i = 0, len = positions.size(); i < len; ++i) { ++ final BlockPos pos = positions.get(i); ++ final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ()); ++ final int emittedLight = blockState.getLightEmission() & emittedMask; ++ ++ if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) { ++ // some other source is brighter ++ continue; ++ } ++ ++ this.appendToIncreaseQueue( ++ ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | (emittedLight & 0xFL) << (6 + 6 + 16) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) ++ ); ++ ++ ++ // propagation wont set this for us ++ this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight); ++ } ++ ++ if (needsEdgeChecks) { ++ // not required to propagate here, but this will reduce the hit of the edge checks ++ this.performLightIncrease(lightAccess); ++ ++ // verify neighbour edges ++ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); ++ } else { ++ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection); ++ ++ this.performLightIncrease(lightAccess); ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4ca68a903e67606fc4ef0bfa9862a73797121c8b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SWMRNibbleArray.java +@@ -0,0 +1,440 @@ ++package ca.spottedleaf.moonrise.patches.starlight.light; ++ ++import net.minecraft.world.level.chunk.DataLayer; ++import java.util.ArrayDeque; ++import java.util.Arrays; ++ ++// SWMR -> Single Writer Multi Reader Nibble Array ++public final class SWMRNibbleArray { ++ ++ /* ++ * Null nibble - nibble does not exist, and should not be written to. Just like vanilla - null ++ * nibbles are always 0 - and they are never written to directly. Only initialised/uninitialised ++ * nibbles can be written to. ++ * ++ * Uninitialised nibble - They are all 0, but the backing array isn't initialised. ++ * ++ * Initialised nibble - Has light data. ++ */ ++ ++ protected static final int INIT_STATE_NULL = 0; // null ++ protected static final int INIT_STATE_UNINIT = 1; // uninitialised ++ protected static final int INIT_STATE_INIT = 2; // initialised ++ protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL ++ ++ public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block ++ // this allows us to maintain only 1 byte array when we're not updating ++ static final ThreadLocal<ArrayDeque<byte[]>> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new); ++ ++ private static byte[] allocateBytes() { ++ final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst(); ++ if (inPool != null) { ++ return inPool; ++ } ++ ++ return new byte[ARRAY_SIZE]; ++ } ++ ++ private static void freeBytes(final byte[] bytes) { ++ WORKING_BYTES_POOL.get().addFirst(bytes); ++ } ++ ++ public static SWMRNibbleArray fromVanilla(final DataLayer nibble) { ++ if (nibble == null) { ++ return new SWMRNibbleArray(null, true); ++ } else if (nibble.isEmpty()) { ++ return new SWMRNibbleArray(); ++ } else { ++ return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later ++ } ++ } ++ ++ protected int stateUpdating; ++ protected volatile int stateVisible; ++ ++ protected byte[] storageUpdating; ++ protected boolean updatingDirty; // only returns whether storageUpdating is dirty ++ protected volatile byte[] storageVisible; ++ ++ public SWMRNibbleArray() { ++ this(null, false); // lazy init ++ } ++ ++ public SWMRNibbleArray(final byte[] bytes) { ++ this(bytes, false); ++ } ++ ++ public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) { ++ if (bytes != null && bytes.length != ARRAY_SIZE) { ++ throw new IllegalArgumentException("Data of wrong length: " + bytes.length); ++ } ++ this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT; ++ this.storageUpdating = this.storageVisible = bytes; ++ } ++ ++ public SWMRNibbleArray(final byte[] bytes, final int state) { ++ if (bytes != null && bytes.length != ARRAY_SIZE) { ++ throw new IllegalArgumentException("Data of wrong length: " + bytes.length); ++ } ++ if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) { ++ throw new IllegalArgumentException("Data cannot be null and have state be initialised"); ++ } ++ this.stateUpdating = this.stateVisible = state; ++ this.storageUpdating = this.storageVisible = bytes; ++ } ++ ++ @Override ++ public String toString() { ++ StringBuilder stringBuilder = new StringBuilder(); ++ stringBuilder.append("State: "); ++ switch (this.stateVisible) { ++ case INIT_STATE_NULL: ++ stringBuilder.append("null"); ++ break; ++ case INIT_STATE_UNINIT: ++ stringBuilder.append("uninitialised"); ++ break; ++ case INIT_STATE_INIT: ++ stringBuilder.append("initialised"); ++ break; ++ case INIT_STATE_HIDDEN: ++ stringBuilder.append("hidden"); ++ break; ++ default: ++ stringBuilder.append("unknown"); ++ break; ++ } ++ stringBuilder.append("\nData:\n"); ++ ++ final byte[] data = this.storageVisible; ++ if (data != null) { ++ for (int i = 0; i < 4096; ++i) { ++ // Copied from NibbleArray#toString ++ final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF); ++ ++ stringBuilder.append(Integer.toHexString(level)); ++ if ((i & 15) == 15) { ++ stringBuilder.append("\n"); ++ } ++ ++ if ((i & 255) == 255) { ++ stringBuilder.append("\n"); ++ } ++ } ++ } else { ++ stringBuilder.append("null"); ++ } ++ ++ return stringBuilder.toString(); ++ } ++ ++ public SaveState getSaveState() { ++ synchronized (this) { ++ final int state = this.stateVisible; ++ final byte[] data = this.storageVisible; ++ if (state == INIT_STATE_NULL) { ++ return null; ++ } ++ if (state == INIT_STATE_UNINIT) { ++ return new SaveState(null, state); ++ } ++ final boolean zero = isAllZero(data); ++ if (zero) { ++ return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null; ++ } else { ++ return new SaveState(data.clone(), state); ++ } ++ } ++ } ++ ++ protected static boolean isAllZero(final byte[] data) { ++ for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) { ++ byte whole = data[i << 4]; ++ ++ for (int k = 1; k < (1 << 4); ++k) { ++ whole |= data[(i << 4) | k]; ++ } ++ ++ if (whole != 0) { ++ return false; ++ } ++ } ++ ++ return true; ++ } ++ ++ // operation type: updating on src, updating on other ++ public void extrudeLower(final SWMRNibbleArray other) { ++ if (other.stateUpdating == INIT_STATE_NULL) { ++ throw new IllegalArgumentException(); ++ } ++ ++ if (other.storageUpdating == null) { ++ this.setUninitialised(); ++ return; ++ } ++ ++ final byte[] src = other.storageUpdating; ++ final byte[] into; ++ ++ if (!this.updatingDirty) { ++ if (this.storageUpdating != null) { ++ into = this.storageUpdating = allocateBytes(); ++ } else { ++ this.storageUpdating = into = allocateBytes(); ++ this.stateUpdating = INIT_STATE_INIT; ++ } ++ this.updatingDirty = true; ++ } else { ++ into = this.storageUpdating; ++ } ++ ++ final int start = 0; ++ final int end = (15 | (15 << 4)) >>> 1; ++ ++ /* x | (z << 4) | (y << 8) */ ++ for (int y = 0; y <= 15; ++y) { ++ System.arraycopy(src, start, into, y << (8 - 1), end - start + 1); ++ } ++ } ++ ++ // operation type: updating ++ public void setFull() { ++ if (this.stateUpdating != INIT_STATE_HIDDEN) { ++ this.stateUpdating = INIT_STATE_INIT; ++ } ++ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1); ++ this.updatingDirty = true; ++ } ++ ++ // operation type: updating ++ public void setZero() { ++ if (this.stateUpdating != INIT_STATE_HIDDEN) { ++ this.stateUpdating = INIT_STATE_INIT; ++ } ++ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0); ++ this.updatingDirty = true; ++ } ++ ++ // operation type: updating ++ public void setNonNull() { ++ if (this.stateUpdating == INIT_STATE_HIDDEN) { ++ this.stateUpdating = INIT_STATE_INIT; ++ return; ++ } ++ if (this.stateUpdating != INIT_STATE_NULL) { ++ return; ++ } ++ this.stateUpdating = INIT_STATE_UNINIT; ++ } ++ ++ // operation type: updating ++ public void setNull() { ++ this.stateUpdating = INIT_STATE_NULL; ++ if (this.updatingDirty && this.storageUpdating != null) { ++ freeBytes(this.storageUpdating); ++ } ++ this.storageUpdating = null; ++ this.updatingDirty = false; ++ } ++ ++ // operation type: updating ++ public void setUninitialised() { ++ this.stateUpdating = INIT_STATE_UNINIT; ++ if (this.storageUpdating != null && this.updatingDirty) { ++ freeBytes(this.storageUpdating); ++ } ++ this.storageUpdating = null; ++ this.updatingDirty = false; ++ } ++ ++ // operation type: updating ++ public void setHidden() { ++ if (this.stateUpdating == INIT_STATE_HIDDEN) { ++ return; ++ } ++ if (this.stateUpdating != INIT_STATE_INIT) { ++ this.setNull(); ++ } else { ++ this.stateUpdating = INIT_STATE_HIDDEN; ++ } ++ } ++ ++ // operation type: updating ++ public boolean isDirty() { ++ return this.stateUpdating != this.stateVisible || this.updatingDirty; ++ } ++ ++ // operation type: updating ++ public boolean isNullNibbleUpdating() { ++ return this.stateUpdating == INIT_STATE_NULL; ++ } ++ ++ // operation type: visible ++ public boolean isNullNibbleVisible() { ++ return this.stateVisible == INIT_STATE_NULL; ++ } ++ ++ // opeartion type: updating ++ public boolean isUninitialisedUpdating() { ++ return this.stateUpdating == INIT_STATE_UNINIT; ++ } ++ ++ // operation type: visible ++ public boolean isUninitialisedVisible() { ++ return this.stateVisible == INIT_STATE_UNINIT; ++ } ++ ++ // operation type: updating ++ public boolean isInitialisedUpdating() { ++ return this.stateUpdating == INIT_STATE_INIT; ++ } ++ ++ // operation type: visible ++ public boolean isInitialisedVisible() { ++ return this.stateVisible == INIT_STATE_INIT; ++ } ++ ++ // operation type: updating ++ public boolean isHiddenUpdating() { ++ return this.stateUpdating == INIT_STATE_HIDDEN; ++ } ++ ++ // operation type: updating ++ public boolean isHiddenVisible() { ++ return this.stateVisible == INIT_STATE_HIDDEN; ++ } ++ ++ // operation type: updating ++ protected void swapUpdatingAndMarkDirty() { ++ if (this.updatingDirty) { ++ return; ++ } ++ ++ if (this.storageUpdating == null) { ++ this.storageUpdating = allocateBytes(); ++ Arrays.fill(this.storageUpdating, (byte)0); ++ } else { ++ System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE); ++ } ++ ++ if (this.stateUpdating != INIT_STATE_HIDDEN) { ++ this.stateUpdating = INIT_STATE_INIT; ++ } ++ this.updatingDirty = true; ++ } ++ ++ // operation type: updating ++ public boolean updateVisible() { ++ if (!this.isDirty()) { ++ return false; ++ } ++ ++ synchronized (this) { ++ if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) { ++ this.storageVisible = null; ++ } else { ++ if (this.storageVisible == null) { ++ this.storageVisible = this.storageUpdating.clone(); ++ } else { ++ if (this.storageUpdating != this.storageVisible) { ++ System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE); ++ } ++ } ++ ++ if (this.storageUpdating != this.storageVisible) { ++ freeBytes(this.storageUpdating); ++ } ++ this.storageUpdating = this.storageVisible; ++ } ++ this.updatingDirty = false; ++ this.stateVisible = this.stateUpdating; ++ } ++ ++ return true; ++ } ++ ++ // operation type: visible ++ public DataLayer toVanillaNibble() { ++ synchronized (this) { ++ switch (this.stateVisible) { ++ case INIT_STATE_HIDDEN: ++ case INIT_STATE_NULL: ++ return null; ++ case INIT_STATE_UNINIT: ++ return new DataLayer(); ++ case INIT_STATE_INIT: ++ return new DataLayer(this.storageVisible.clone()); ++ default: ++ throw new IllegalStateException(); ++ } ++ } ++ } ++ ++ /* x | (z << 4) | (y << 8) */ ++ ++ // operation type: updating ++ public int getUpdating(final int x, final int y, final int z) { ++ return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); ++ } ++ ++ // operation type: updating ++ public int getUpdating(final int index) { ++ // indices range from 0 -> 4096 ++ final byte[] bytes = this.storageUpdating; ++ if (bytes == null) { ++ return 0; ++ } ++ final byte value = bytes[index >>> 1]; ++ ++ // if we are an even index, we want lower 4 bits ++ // if we are an odd index, we want upper 4 bits ++ return ((value >>> ((index & 1) << 2)) & 0xF); ++ } ++ ++ // operation type: visible ++ public int getVisible(final int x, final int y, final int z) { ++ return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); ++ } ++ ++ // operation type: visible ++ public int getVisible(final int index) { ++ // indices range from 0 -> 4096 ++ final byte[] visibleBytes = this.storageVisible; ++ if (visibleBytes == null) { ++ return 0; ++ } ++ final byte value = visibleBytes[index >>> 1]; ++ ++ // if we are an even index, we want lower 4 bits ++ // if we are an odd index, we want upper 4 bits ++ return ((value >>> ((index & 1) << 2)) & 0xF); ++ } ++ ++ // operation type: updating ++ public void set(final int x, final int y, final int z, final int value) { ++ this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value); ++ } ++ ++ // operation type: updating ++ public void set(final int index, final int value) { ++ if (!this.updatingDirty) { ++ this.swapUpdatingAndMarkDirty(); ++ } ++ final int shift = (index & 1) << 2; ++ final int i = index >>> 1; ++ ++ this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift)); ++ } ++ ++ public static final class SaveState { ++ ++ public final byte[] data; ++ public final int state; ++ ++ public SaveState(final byte[] data, final int state) { ++ this.data = data; ++ this.state = state; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fdbc015f498164c9d2c578cd84a73def568142a4 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java +@@ -0,0 +1,711 @@ ++package ca.spottedleaf.moonrise.patches.starlight.light; ++ ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState; ++import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; ++import it.unimi.dsi.fastutil.shorts.ShortCollection; ++import it.unimi.dsi.fastutil.shorts.ShortIterator; ++import net.minecraft.core.BlockPos; ++import net.minecraft.world.level.BlockGetter; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.LevelChunkSection; ++import net.minecraft.world.level.chunk.LightChunkGetter; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.phys.shapes.Shapes; ++import net.minecraft.world.phys.shapes.VoxelShape; ++import java.util.Arrays; ++import java.util.Set; ++ ++public final class SkyStarLightEngine extends StarLightEngine { ++ ++ /* ++ Specification for managing the initialisation and de-initialisation of skylight nibble arrays: ++ ++ Skylight nibble initialisation requires that non-empty chunk sections have 1 radius nibbles non-null. ++ ++ This presents some problems, as vanilla is only guaranteed to have 0 radius neighbours loaded when editing blocks. ++ However starlight fixes this so that it has 1 radius loaded. Still, we don't actually have guarantees ++ that we have the necessary chunks loaded to de-initialise neighbour sections (but we do have enough to de-initialise ++ our own) - we need a radius of 2 to de-initialise neighbour nibbles. ++ How do we solve this? ++ ++ Each chunk will store the last known "emptiness" of sections for each of their 1 radius neighbour chunk sections. ++ If the chunk does not have full data, then its nibbles are NOT de-initialised. This is because obviously the ++ chunk did not go through the light stage yet - or its neighbours are not lit. In either case, once the last ++ known "emptiness" of neighbouring sections is filled with data, the chunk will run a full check of the data ++ to see if any of its nibbles need to be de-initialised. ++ ++ The emptiness map allows us to de-initialise neighbour nibbles if the neighbour has it filled with data, ++ and if it doesn't have data then we know it will correctly de-initialise once it fills up. ++ ++ Unlike vanilla, we store whether nibbles are uninitialised on disk - so we don't need any dumb hacking ++ around those. ++ */ ++ ++ protected final int[] heightMapBlockChange = new int[16 * 16]; ++ { ++ Arrays.fill(this.heightMapBlockChange, Integer.MIN_VALUE); // clear heightmap ++ } ++ ++ protected final boolean[] nullPropagationCheckCache; ++ ++ public SkyStarLightEngine(final Level world) { ++ super(true, world); ++ this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)]; ++ } ++ ++ @Override ++ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) { ++ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) { ++ return; ++ } ++ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); ++ if (nibble == null) { ++ if (!initRemovedNibbles) { ++ throw new IllegalStateException(); ++ } else { ++ this.setNibbleInCache(chunkX, chunkY, chunkZ, nibble = new SWMRNibbleArray(null, true)); ++ } ++ } ++ this.initNibble(nibble, chunkX, chunkY, chunkZ, extrude); ++ } ++ ++ @Override ++ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { ++ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); ++ if (nibble != null) { ++ nibble.setNull(); ++ } ++ } ++ ++ protected final void initNibble(final SWMRNibbleArray currNibble, final int chunkX, final int chunkY, final int chunkZ, final boolean extrude) { ++ if (!currNibble.isNullNibbleUpdating()) { ++ // already initialised ++ return; ++ } ++ ++ final boolean[] emptinessMap = this.getEmptinessMap(chunkX, chunkZ); ++ ++ // are we above this chunk's lowest empty section? ++ int lowestY = this.minLightSection - 1; ++ for (int currY = this.maxSection; currY >= this.minSection; --currY) { ++ if (emptinessMap == null) { ++ // cannot delay nibble init for lit chunks, as we need to init to propagate into them. ++ final LevelChunkSection current = this.getChunkSection(chunkX, currY, chunkZ); ++ if (current == null || current.hasOnlyAir()) { ++ continue; ++ } ++ } else { ++ if (emptinessMap[currY - this.minSection]) { ++ continue; ++ } ++ } ++ ++ // should always be full lit here ++ lowestY = currY; ++ break; ++ } ++ ++ if (chunkY > lowestY) { ++ // we need to set this one to full ++ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); ++ nibble.setNonNull(); ++ nibble.setFull(); ++ return; ++ } ++ ++ if (extrude) { ++ // this nibble is going to depend solely on the skylight data above it ++ // find first non-null data above (there does exist one, as we just found it above) ++ for (int currY = chunkY + 1; currY <= this.maxLightSection; ++currY) { ++ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, currY, chunkZ); ++ if (nibble != null && !nibble.isNullNibbleUpdating()) { ++ currNibble.setNonNull(); ++ currNibble.extrudeLower(nibble); ++ break; ++ } ++ } ++ } else { ++ currNibble.setNonNull(); ++ } ++ } ++ ++ protected final void rewriteNibbleCacheForSkylight(final ChunkAccess chunk) { ++ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) { ++ final SWMRNibbleArray nibble = this.nibbleCache[index]; ++ if (nibble != null && nibble.isNullNibbleUpdating()) { ++ // stop propagation in these areas ++ this.nibbleCache[index] = null; ++ nibble.updateVisible(); ++ } ++ } ++ } ++ ++ // rets whether neighbours were init'd ++ ++ protected final boolean checkNullSection(final int chunkX, final int chunkY, final int chunkZ, ++ final boolean extrudeInitialised) { ++ // null chunk sections may have nibble neighbours in the horizontal 1 radius that are ++ // non-null. Propagation to these neighbours is necessary. ++ // What makes this easy is we know none of these neighbours are non-empty (otherwise ++ // this nibble would be initialised). So, we don't have to initialise ++ // the neighbours in the full 1 radius, because there's no worry that any "paths" ++ // to the neighbours on this horizontal plane are blocked. ++ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.nullPropagationCheckCache[chunkY - this.minLightSection]) { ++ return false; ++ } ++ this.nullPropagationCheckCache[chunkY - this.minLightSection] = true; ++ ++ // check horizontal neighbours ++ boolean needInitNeighbours = false; ++ neighbour_search: ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, chunkY, dz + chunkZ); ++ if (nibble != null && !nibble.isNullNibbleUpdating()) { ++ needInitNeighbours = true; ++ break neighbour_search; ++ } ++ } ++ } ++ ++ if (needInitNeighbours) { ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ this.initNibble(dx + chunkX, chunkY, dz + chunkZ, (dx | dz) == 0 ? extrudeInitialised : true, true); ++ } ++ } ++ } ++ ++ return needInitNeighbours; ++ } ++ ++ protected final int getLightLevelExtruded(final int worldX, final int worldY, final int worldZ) { ++ final int chunkX = worldX >> 4; ++ int chunkY = worldY >> 4; ++ final int chunkZ = worldZ >> 4; ++ ++ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); ++ if (nibble != null) { ++ return nibble.getUpdating(worldX, worldY, worldZ); ++ } ++ ++ for (;;) { ++ if (++chunkY > this.maxLightSection) { ++ return 15; ++ } ++ ++ nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); ++ ++ if (nibble != null) { ++ return nibble.getUpdating(worldX, 0, worldZ); ++ } ++ } ++ } ++ ++ @Override ++ protected boolean[] getEmptinessMap(final ChunkAccess chunk) { ++ return ((StarlightChunk)chunk).starlight$getSkyEmptinessMap(); ++ } ++ ++ @Override ++ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { ++ ((StarlightChunk)chunk).starlight$setSkyEmptinessMap(to); ++ } ++ ++ @Override ++ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { ++ return ((StarlightChunk)chunk).starlight$getSkyNibbles(); ++ } ++ ++ @Override ++ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { ++ ((StarlightChunk)chunk).starlight$setSkyNibbles(to); ++ } ++ ++ @Override ++ protected boolean canUseChunk(final ChunkAccess chunk) { ++ // can only use chunks for sky stuff if their sections have been init'd ++ return chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); ++ } ++ ++ @Override ++ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, ++ final int toSection) { ++ Arrays.fill(this.nullPropagationCheckCache, false); ++ this.rewriteNibbleCacheForSkylight(chunk); ++ final int chunkX = chunk.getPos().x; ++ final int chunkZ = chunk.getPos().z; ++ for (int y = toSection; y >= fromSection; --y) { ++ this.checkNullSection(chunkX, y, chunkZ, true); ++ } ++ ++ super.checkChunkEdges(lightAccess, chunk, fromSection, toSection); ++ } ++ ++ @Override ++ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) { ++ Arrays.fill(this.nullPropagationCheckCache, false); ++ this.rewriteNibbleCacheForSkylight(chunk); ++ final int chunkX = chunk.getPos().x; ++ final int chunkZ = chunk.getPos().z; ++ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { ++ final int y = (int)iterator.nextShort(); ++ this.checkNullSection(chunkX, y, chunkZ, true); ++ } ++ ++ super.checkChunkEdges(lightAccess, chunk, sections); ++ } ++ ++ @Override ++ protected void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) { ++ // blocks can change opacity ++ // blocks can change direction of propagation ++ ++ // same logic applies from BlockStarLightEngine#checkBlock ++ ++ final int encodeOffset = this.coordinateOffset; ++ ++ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); ++ ++ if (currentLevel == 15) { ++ // must re-propagate clobbered source ++ this.appendToIncreaseQueue( ++ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | (currentLevel & 0xFL) << (6 + 6 + 16) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent ++ ); ++ } else { ++ this.setLightLevel(worldX, worldY, worldZ, 0); ++ } ++ ++ this.appendToDecreaseQueue( ++ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | (currentLevel & 0xFL) << (6 + 6 + 16) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ ); ++ } ++ ++ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); ++ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); ++ ++ @Override ++ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, ++ final int expect) { ++ if (expect == 15) { ++ return expect; ++ } ++ ++ final int sectionOffset = this.chunkSectionIndexOffset; ++ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); ++ int opacity = ((StarlightAbstractBlockState)centerState).starlight$getOpacityIfCached(); ++ ++ final BlockState conditionallyOpaqueState; ++ if (opacity < 0) { ++ this.recalcCenterPos.set(worldX, worldY, worldZ); ++ opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos)); ++ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) { ++ conditionallyOpaqueState = centerState; ++ } else { ++ conditionallyOpaqueState = null; ++ } ++ } else { ++ conditionallyOpaqueState = null; ++ opacity = Math.max(1, opacity); ++ } ++ ++ int level = 0; ++ ++ for (final AxisDirection direction : AXIS_DIRECTIONS) { ++ final int offX = worldX + direction.x; ++ final int offY = worldY + direction.y; ++ final int offZ = worldZ + direction.z; ++ ++ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; ++ ++ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); ++ ++ if ((neighbourLevel - 1) <= level) { ++ // don't need to test transparency, we know it wont affect the result. ++ continue; ++ } ++ ++ final BlockState neighbourState = this.getBlockState(offX, offY, offZ); ++ ++ if (((StarlightAbstractBlockState)neighbourState).starlight$isConditionallyFullOpaque()) { ++ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that ++ // we don't read the blockstate because most of the time this is false, so using the faster ++ // known transparency lookup results in a net win ++ this.recalcNeighbourPos.set(offX, offY, offZ); ++ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); ++ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); ++ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { ++ // not allowed to propagate ++ continue; ++ } ++ } ++ ++ final int calculated = neighbourLevel - opacity; ++ level = Math.max(calculated, level); ++ if (level > expect) { ++ return level; ++ } ++ } ++ ++ return level; ++ } ++ ++ @Override ++ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions) { ++ this.rewriteNibbleCacheForSkylight(atChunk); ++ Arrays.fill(this.nullPropagationCheckCache, false); ++ ++ final BlockGetter world = lightAccess.getLevel(); ++ final int chunkX = atChunk.getPos().x; ++ final int chunkZ = atChunk.getPos().z; ++ final int heightMapOffset = chunkX * -16 + (chunkZ * (-16 * 16)); ++ ++ // setup heightmap for changes ++ for (final BlockPos pos : positions) { ++ final int index = pos.getX() + (pos.getZ() << 4) + heightMapOffset; ++ final int curr = this.heightMapBlockChange[index]; ++ if (pos.getY() > curr) { ++ this.heightMapBlockChange[index] = pos.getY(); ++ } ++ } ++ ++ // note: light sets are delayed while processing skylight source changes due to how ++ // nibbles are initialised, as we want to avoid clobbering nibble values so what when ++ // below nibbles are initialised they aren't reading from partially modified nibbles ++ ++ // now we can recalculate the sources for the changed columns ++ for (int index = 0; index < (16 * 16); ++index) { ++ final int maxY = this.heightMapBlockChange[index]; ++ if (maxY == Integer.MIN_VALUE) { ++ // not changed ++ continue; ++ } ++ this.heightMapBlockChange[index] = Integer.MIN_VALUE; // restore default for next caller ++ ++ final int columnX = (index & 15) | (chunkX << 4); ++ final int columnZ = (index >>> 4) | (chunkZ << 4); ++ ++ // try and propagate from the above y ++ // delay light set until after processing all sources to setup ++ final int maxPropagationY = this.tryPropagateSkylight(world, columnX, maxY, columnZ, true, true); ++ ++ // maxPropagationY is now the highest block that could not be propagated to ++ ++ // remove all sources below that are 15 ++ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; ++ final int encodeOffset = this.coordinateOffset; ++ ++ if (this.getLightLevelExtruded(columnX, maxPropagationY, columnZ) == 15) { ++ // ensure section is checked ++ this.checkNullSection(columnX >> 4, maxPropagationY >> 4, columnZ >> 4, true); ++ ++ for (int currY = maxPropagationY; currY >= (this.minLightSection << 4); --currY) { ++ if ((currY & 15) == 15) { ++ // ensure section is checked ++ this.checkNullSection(columnX >> 4, (currY >> 4), columnZ >> 4, true); ++ } ++ ++ // ensure section below is always checked ++ final SWMRNibbleArray nibble = this.getNibbleFromCache(columnX >> 4, currY >> 4, columnZ >> 4); ++ if (nibble == null) { ++ // advance currY to the the top of the section below ++ currY = (currY) & (~15); ++ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually ++ // end up there ++ continue; ++ } ++ ++ if (nibble.getUpdating(columnX, currY, columnZ) != 15) { ++ break; ++ } ++ ++ // delay light set until after processing all sources to setup ++ this.appendToDecreaseQueue( ++ ((columnX + (columnZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | (15L << (6 + 6 + 16)) ++ | (propagateDirection << (6 + 6 + 16 + 4)) ++ // do not set transparent blocks for the same reason we don't in the checkBlock method ++ ); ++ } ++ } ++ } ++ ++ // delayed light sets are processed here, and must be processed before checkBlock as checkBlock reads ++ // immediate light value ++ this.processDelayedIncreases(); ++ this.processDelayedDecreases(); ++ ++ for (final BlockPos pos : positions) { ++ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); ++ } ++ ++ this.performLightDecrease(lightAccess); ++ } ++ ++ protected final int[] heightMapGen = new int[32 * 32]; ++ ++ @Override ++ protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { ++ this.rewriteNibbleCacheForSkylight(chunk); ++ Arrays.fill(this.nullPropagationCheckCache, false); ++ ++ final BlockGetter world = lightAccess.getLevel(); ++ final ChunkPos chunkPos = chunk.getPos(); ++ final int chunkX = chunkPos.x; ++ final int chunkZ = chunkPos.z; ++ ++ final LevelChunkSection[] sections = chunk.getSections(); ++ ++ int highestNonEmptySection = this.maxSection; ++ while (highestNonEmptySection == (this.minSection - 1) || ++ sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].hasOnlyAir()) { ++ this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false); ++ // try propagate FULL to neighbours ++ ++ // check neighbours to see if we need to propagate into them ++ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { ++ final int neighbourX = chunkX + direction.x; ++ final int neighbourZ = chunkZ + direction.z; ++ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ); ++ if (neighbourNibble == null) { ++ // unloaded neighbour ++ // most of the time we fall here ++ continue; ++ } ++ ++ // it looks like we need to propagate into the neighbour ++ ++ final int incX; ++ final int incZ; ++ final int startX; ++ final int startZ; ++ ++ if (direction.x != 0) { ++ // x direction ++ incX = 0; ++ incZ = 1; ++ ++ if (direction.x < 0) { ++ // negative ++ startX = chunkX << 4; ++ } else { ++ startX = chunkX << 4 | 15; ++ } ++ startZ = chunkZ << 4; ++ } else { ++ // z direction ++ incX = 1; ++ incZ = 0; ++ ++ if (direction.z < 0) { ++ // negative ++ startZ = chunkZ << 4; ++ } else { ++ startZ = chunkZ << 4 | 15; ++ } ++ startX = chunkX << 4; ++ } ++ ++ final int encodeOffset = this.coordinateOffset; ++ final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction ++ ++ for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) { ++ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { ++ this.appendToIncreaseQueue( ++ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | (15L << (6 + 6 + 16)) // we know we're at full lit here ++ | (propagateDirection << (6 + 6 + 16 + 4)) ++ // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY) ++ ); ++ } ++ } ++ } ++ ++ if (highestNonEmptySection-- == (this.minSection - 1)) { ++ break; ++ } ++ } ++ ++ if (highestNonEmptySection >= this.minSection) { ++ // fill out our other sources ++ final int minX = chunkPos.x << 4; ++ final int maxX = chunkPos.x << 4 | 15; ++ final int minZ = chunkPos.z << 4; ++ final int maxZ = chunkPos.z << 4 | 15; ++ final int startY = highestNonEmptySection << 4 | 15; ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false); ++ } ++ } ++ } // else: apparently the chunk is empty ++ ++ if (needsEdgeChecks) { ++ // not required to propagate here, but this will reduce the hit of the edge checks ++ this.performLightIncrease(lightAccess); ++ ++ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { ++ this.checkNullSection(chunkX, y, chunkZ, false); ++ } ++ // no need to rewrite the nibble cache again ++ super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection); ++ } else { ++ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { ++ this.checkNullSection(chunkX, y, chunkZ, false); ++ } ++ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection); ++ ++ this.performLightIncrease(lightAccess); ++ } ++ } ++ ++ protected final void processDelayedIncreases() { ++ // copied from performLightIncrease ++ final long[] queue = this.increaseQueue; ++ final int decodeOffsetX = -this.encodeOffsetX; ++ final int decodeOffsetY = -this.encodeOffsetY; ++ final int decodeOffsetZ = -this.encodeOffsetZ; ++ ++ for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) { ++ final long queueValue = queue[i]; ++ ++ final int posX = ((int)queueValue & 63) + decodeOffsetX; ++ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; ++ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; ++ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); ++ ++ this.setLightLevel(posX, posY, posZ, propagatedLightLevel); ++ } ++ } ++ ++ protected final void processDelayedDecreases() { ++ // copied from performLightDecrease ++ final long[] queue = this.decreaseQueue; ++ final int decodeOffsetX = -this.encodeOffsetX; ++ final int decodeOffsetY = -this.encodeOffsetY; ++ final int decodeOffsetZ = -this.encodeOffsetZ; ++ ++ for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) { ++ final long queueValue = queue[i]; ++ ++ final int posX = ((int)queueValue & 63) + decodeOffsetX; ++ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; ++ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; ++ ++ this.setLightLevel(posX, posY, posZ, 0); ++ } ++ } ++ ++ // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays ++ // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so ++ // clobbering the light values will result in broken propagation) ++ protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ, ++ final boolean extrudeInitialised, final boolean delayLightSet) { ++ final BlockPos.MutableBlockPos mutablePos = this.mutablePos3; ++ final int encodeOffset = this.coordinateOffset; ++ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards. ++ ++ if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) { ++ return startY; ++ } ++ ++ // ensure this section is always checked ++ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); ++ ++ BlockState above = this.getBlockState(worldX, startY + 1, worldZ); ++ ++ for (;startY >= (this.minLightSection << 4); --startY) { ++ if ((startY & 15) == 15) { ++ // ensure this section is always checked ++ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); ++ } ++ final BlockState current = this.getBlockState(worldX, startY, worldZ); ++ ++ final VoxelShape fromShape; ++ if (((StarlightAbstractBlockState)above).starlight$isConditionallyFullOpaque()) { ++ this.mutablePos2.set(worldX, startY + 1, worldZ); ++ fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms); ++ if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { ++ // above wont let us propagate ++ break; ++ } ++ } else { ++ fromShape = Shapes.empty(); ++ } ++ ++ final int opacityIfCached = ((StarlightAbstractBlockState)current).starlight$getOpacityIfCached(); ++ // does light propagate from the top down? ++ if (opacityIfCached != -1) { ++ if (opacityIfCached != 0) { ++ // we cannot propagate 15 through this ++ break; ++ } ++ // most of the time it falls here. ++ // add to propagate ++ // light set delayed until we determine if this nibble section is null ++ this.appendToIncreaseQueue( ++ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | (15L << (6 + 6 + 16)) // we know we're at full lit here ++ | (propagateDirection << (6 + 6 + 16 + 4)) ++ ); ++ } else { ++ mutablePos.set(worldX, startY, worldZ); ++ long flags = 0L; ++ if (((StarlightAbstractBlockState)current).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms); ++ ++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { ++ // can't propagate here, we're done on this column. ++ break; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = current.getLightBlock(world, mutablePos); ++ if (opacity > 0) { ++ // let the queued value (if any) handle it from here. ++ break; ++ } ++ ++ // light set delayed until we determine if this nibble section is null ++ this.appendToIncreaseQueue( ++ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | (15L << (6 + 6 + 16)) // we know we're at full lit here ++ | (propagateDirection << (6 + 6 + 16 + 4)) ++ | flags ++ ); ++ } ++ ++ above = current; ++ ++ if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) { ++ // we skip empty sections here, as this is just an easy way of making sure the above block ++ // can propagate through air. ++ ++ // nothing can propagate in null sections, remove the queue entry for it ++ --this.increaseQueueInitialLength; ++ ++ // advance currY to the the top of the section below ++ startY = (startY) & (~15); ++ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually ++ // end up there ++ ++ // make sure this is marked as AIR ++ above = AIR_BLOCK_STATE; ++ } else if (!delayLightSet) { ++ this.setLightLevel(worldX, startY, worldZ, 15); ++ } ++ } ++ ++ return startY; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java +new file mode 100644 +index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017c58191e2 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java +@@ -0,0 +1,1573 @@ ++package ca.spottedleaf.moonrise.patches.starlight.light; ++ ++import ca.spottedleaf.concurrentutil.util.IntegerUtil; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.starlight.blockstate.StarlightAbstractBlockState; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.shorts.ShortCollection; ++import it.unimi.dsi.fastutil.shorts.ShortIterator; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Direction; ++import net.minecraft.core.SectionPos; ++import net.minecraft.world.level.BlockGetter; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.LevelHeightAccessor; ++import net.minecraft.world.level.LightLayer; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.LevelChunkSection; ++import net.minecraft.world.level.chunk.LightChunkGetter; ++import net.minecraft.world.phys.shapes.Shapes; ++import net.minecraft.world.phys.shapes.VoxelShape; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.List; ++import java.util.Set; ++import java.util.function.Consumer; ++import java.util.function.IntConsumer; ++ ++public abstract class StarLightEngine { ++ ++ protected static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState(); ++ ++ protected static final AxisDirection[] DIRECTIONS = AxisDirection.values(); ++ protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS; ++ protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] { ++ AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X, ++ AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z ++ }; ++ ++ protected static enum AxisDirection { ++ ++ // Declaration order is important and relied upon. Do not change without modifying propagation code. ++ POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0), ++ POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1), ++ POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0); ++ ++ static { ++ POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X; ++ POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z; ++ POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y; ++ } ++ ++ protected AxisDirection opposite; ++ ++ public final int x; ++ public final int y; ++ public final int z; ++ public final Direction nms; ++ public final long everythingButThisDirection; ++ public final long everythingButTheOppositeDirection; ++ ++ AxisDirection(final int x, final int y, final int z) { ++ this.x = x; ++ this.y = y; ++ this.z = z; ++ this.nms = Direction.fromDelta(x, y, z); ++ this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal())); ++ // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction. ++ this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1))); ++ } ++ ++ public AxisDirection getOpposite() { ++ return this.opposite; ++ } ++ } ++ ++ // I'd like to thank https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1 ++ // for explaining how light propagates via breadth-first search ++ ++ // While the above is a good start to understanding the general idea of what the general principles are, it's not ++ // exactly how the vanilla light engine should behave for minecraft. ++ ++ // similar to the above, except the chunk section indices vary from [-1, 1], or [0, 2] ++ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] ++ // index = x + (z * 5) + (y * 25) ++ // null index indicates the chunk section doesn't exist (empty or out of bounds) ++ protected final LevelChunkSection[] sectionCache; ++ ++ // the exact same as above, except for storing fast access to SWMRNibbleArray ++ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] ++ // index = x + (z * 5) + (y * 25) ++ protected final SWMRNibbleArray[] nibbleCache; ++ ++ // the exact same as above, except for storing fast access to nibbles to call change callbacks for ++ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] ++ // index = x + (z * 5) + (y * 25) ++ protected final boolean[] notifyUpdateCache; ++ ++ // always initialsed during start of lighting. ++ // index = x + (z * 5) ++ protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5]; ++ ++ // index = x + (z * 5) ++ protected final boolean[][] emptinessMapCache = new boolean[5 * 5][]; ++ ++ protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos(); ++ protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos(); ++ protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos(); ++ ++ protected int encodeOffsetX; ++ protected int encodeOffsetY; ++ protected int encodeOffsetZ; ++ ++ protected int coordinateOffset; ++ ++ protected int chunkOffsetX; ++ protected int chunkOffsetY; ++ protected int chunkOffsetZ; ++ ++ protected int chunkIndexOffset; ++ protected int chunkSectionIndexOffset; ++ ++ protected final boolean skylightPropagator; ++ protected final int emittedLightMask; ++ protected final boolean isClientSide; ++ ++ protected final Level world; ++ protected final int minLightSection; ++ protected final int maxLightSection; ++ protected final int minSection; ++ protected final int maxSection; ++ ++ protected StarLightEngine(final boolean skylightPropagator, final Level world) { ++ this.skylightPropagator = skylightPropagator; ++ this.emittedLightMask = skylightPropagator ? 0 : 0xF; ++ this.isClientSide = world.isClientSide; ++ this.world = world; ++ this.minLightSection = WorldUtil.getMinLightSection(world); ++ this.maxLightSection = WorldUtil.getMaxLightSection(world); ++ this.minSection = WorldUtil.getMinSection(world); ++ this.maxSection = WorldUtil.getMaxSection(world); ++ ++ this.sectionCache = new LevelChunkSection[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer ++ this.nibbleCache = new SWMRNibbleArray[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer ++ this.notifyUpdateCache = new boolean[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer ++ } ++ ++ protected final void setupEncodeOffset(final int centerX, final int centerY, final int centerZ) { ++ // 31 = center + encodeOffset ++ this.encodeOffsetX = 31 - centerX; ++ this.encodeOffsetY = (-(this.minLightSection - 1) << 4); // we want 0 to be the smallest encoded value ++ this.encodeOffsetZ = 31 - centerZ; ++ ++ // coordinateIndex = x | (z << 6) | (y << 12) ++ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << 6) + (this.encodeOffsetY << 12); ++ ++ // 2 = (centerX >> 4) + chunkOffset ++ this.chunkOffsetX = 2 - (centerX >> 4); ++ this.chunkOffsetY = -(this.minLightSection - 1); // lowest should be 0 ++ this.chunkOffsetZ = 2 - (centerZ >> 4); ++ ++ // chunk index = x + (5 * z) ++ this.chunkIndexOffset = this.chunkOffsetX + (5 * this.chunkOffsetZ); ++ ++ // chunk section index = x + (5 * z) + ((5*5) * y) ++ this.chunkSectionIndexOffset = this.chunkIndexOffset + ((5 * 5) * this.chunkOffsetY); ++ } ++ ++ protected final void setupCaches(final LightChunkGetter chunkProvider, final int centerX, final int centerY, final int centerZ, ++ final boolean relaxed, final boolean tryToLoadChunksFor2Radius) { ++ final int centerChunkX = centerX >> 4; ++ final int centerChunkY = centerY >> 4; ++ final int centerChunkZ = centerZ >> 4; ++ ++ this.setupEncodeOffset(centerChunkX * 16 + 7, centerChunkY * 16 + 7, centerChunkZ * 16 + 7); ++ ++ final int radius = tryToLoadChunksFor2Radius ? 2 : 1; ++ ++ for (int dz = -radius; dz <= radius; ++dz) { ++ for (int dx = -radius; dx <= radius; ++dx) { ++ final int cx = centerChunkX + dx; ++ final int cz = centerChunkZ + dz; ++ final boolean isTwoRadius = Math.max(IntegerUtil.branchlessAbs(dx), IntegerUtil.branchlessAbs(dz)) == 2; ++ final ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz); ++ ++ if (chunk == null) { ++ if (relaxed | isTwoRadius) { ++ continue; ++ } ++ throw new IllegalArgumentException("Trying to propagate light update before 1 radius neighbours ready"); ++ } ++ ++ if (!this.canUseChunk(chunk)) { ++ continue; ++ } ++ ++ this.setChunkInCache(cx, cz, chunk); ++ this.setEmptinessMapCache(cx, cz, this.getEmptinessMap(chunk)); ++ if (!isTwoRadius) { ++ this.setBlocksForChunkInCache(cx, cz, chunk.getSections()); ++ this.setNibblesForChunkInCache(cx, cz, this.getNibblesOnChunk(chunk)); ++ } ++ } ++ } ++ } ++ ++ protected final ChunkAccess getChunkInCache(final int chunkX, final int chunkZ) { ++ return this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset]; ++ } ++ ++ protected final void setChunkInCache(final int chunkX, final int chunkZ, final ChunkAccess chunk) { ++ this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk; ++ } ++ ++ protected final LevelChunkSection getChunkSection(final int chunkX, final int chunkY, final int chunkZ) { ++ return this.sectionCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset]; ++ } ++ ++ protected final void setChunkSectionInCache(final int chunkX, final int chunkY, final int chunkZ, final LevelChunkSection section) { ++ this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section; ++ } ++ ++ protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] sections) { ++ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { ++ this.setChunkSectionInCache(chunkX, cy, chunkZ, ++ sections == null ? null : (cy >= this.minSection && cy <= this.maxSection ? sections[cy - this.minSection] : null)); ++ } ++ } ++ ++ protected final SWMRNibbleArray getNibbleFromCache(final int chunkX, final int chunkY, final int chunkZ) { ++ return this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset]; ++ } ++ ++ protected final SWMRNibbleArray[] getNibblesForChunkFromCache(final int chunkX, final int chunkZ) { ++ final SWMRNibbleArray[] ret = new SWMRNibbleArray[this.maxLightSection - this.minLightSection + 1]; ++ ++ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { ++ ret[cy - this.minLightSection] = this.nibbleCache[chunkX + 5*chunkZ + (cy * (5 * 5)) + this.chunkSectionIndexOffset]; ++ } ++ ++ return ret; ++ } ++ ++ protected final void setNibbleInCache(final int chunkX, final int chunkY, final int chunkZ, final SWMRNibbleArray nibble) { ++ this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset] = nibble; ++ } ++ ++ protected final void setNibblesForChunkInCache(final int chunkX, final int chunkZ, final SWMRNibbleArray[] nibbles) { ++ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { ++ this.setNibbleInCache(chunkX, cy, chunkZ, nibbles == null ? null : nibbles[cy - this.minLightSection]); ++ } ++ } ++ ++ protected final void updateVisible(final LightChunkGetter lightAccess) { ++ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) { ++ final SWMRNibbleArray nibble = this.nibbleCache[index]; ++ if (!this.notifyUpdateCache[index] && (nibble == null || !nibble.isDirty())) { ++ continue; ++ } ++ ++ final int chunkX = (index % 5) - this.chunkOffsetX; ++ final int chunkZ = ((index / 5) % 5) - this.chunkOffsetZ; ++ final int ySections = this.maxSection - this.minSection + 1; ++ final int chunkY = ((index / (5*5)) % (ySections + 2 + 2)) - this.chunkOffsetY; ++ if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) { ++ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, chunkY, chunkZ)); ++ } ++ } ++ } ++ ++ protected final void destroyCaches() { ++ Arrays.fill(this.sectionCache, null); ++ Arrays.fill(this.nibbleCache, null); ++ Arrays.fill(this.chunkCache, null); ++ Arrays.fill(this.emptinessMapCache, null); ++ if (this.isClientSide) { ++ Arrays.fill(this.notifyUpdateCache, false); ++ } ++ } ++ ++ protected final BlockState getBlockState(final int worldX, final int worldY, final int worldZ) { ++ final LevelChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; ++ ++ if (section != null) { ++ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15); ++ } ++ ++ return AIR_BLOCK_STATE; ++ } ++ ++ protected final BlockState getBlockState(final int sectionIndex, final int localIndex) { ++ final LevelChunkSection section = this.sectionCache[sectionIndex]; ++ ++ if (section != null) { ++ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.states.get(localIndex); ++ } ++ ++ return AIR_BLOCK_STATE; ++ } ++ ++ protected final int getLightLevel(final int worldX, final int worldY, final int worldZ) { ++ final SWMRNibbleArray nibble = this.nibbleCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; ++ ++ return nibble == null ? 0 : nibble.getUpdating((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8)); ++ } ++ ++ protected final int getLightLevel(final int sectionIndex, final int localIndex) { ++ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; ++ ++ return nibble == null ? 0 : nibble.getUpdating(localIndex); ++ } ++ ++ protected final void setLightLevel(final int worldX, final int worldY, final int worldZ, final int level) { ++ final int sectionIndex = (worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset; ++ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; ++ ++ if (nibble != null) { ++ nibble.set((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8), level); ++ if (this.isClientSide) { ++ int cx1 = (worldX - 1) >> 4; ++ int cx2 = (worldX + 1) >> 4; ++ int cy1 = (worldY - 1) >> 4; ++ int cy2 = (worldY + 1) >> 4; ++ int cz1 = (worldZ - 1) >> 4; ++ int cz2 = (worldZ + 1) >> 4; ++ for (int x = cx1; x <= cx2; ++x) { ++ for (int y = cy1; y <= cy2; ++y) { ++ for (int z = cz1; z <= cz2; ++z) { ++ this.notifyUpdateCache[x + 5 * z + (5 * 5) * y + this.chunkSectionIndexOffset] = true; ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) { ++ if (this.isClientSide) { ++ int cx1 = (worldX - 1) >> 4; ++ int cx2 = (worldX + 1) >> 4; ++ int cy1 = (worldY - 1) >> 4; ++ int cy2 = (worldY + 1) >> 4; ++ int cz1 = (worldZ - 1) >> 4; ++ int cz2 = (worldZ + 1) >> 4; ++ for (int x = cx1; x <= cx2; ++x) { ++ for (int y = cy1; y <= cy2; ++y) { ++ for (int z = cz1; z <= cz2; ++z) { ++ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true; ++ } ++ } ++ } ++ } ++ } ++ ++ protected final void setLightLevel(final int sectionIndex, final int localIndex, final int worldX, final int worldY, final int worldZ, final int level) { ++ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; ++ ++ if (nibble != null) { ++ nibble.set(localIndex, level); ++ if (this.isClientSide) { ++ int cx1 = (worldX - 1) >> 4; ++ int cx2 = (worldX + 1) >> 4; ++ int cy1 = (worldY - 1) >> 4; ++ int cy2 = (worldY + 1) >> 4; ++ int cz1 = (worldZ - 1) >> 4; ++ int cz2 = (worldZ + 1) >> 4; ++ for (int x = cx1; x <= cx2; ++x) { ++ for (int y = cy1; y <= cy2; ++y) { ++ for (int z = cz1; z <= cz2; ++z) { ++ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true; ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ protected final boolean[] getEmptinessMap(final int chunkX, final int chunkZ) { ++ return this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset]; ++ } ++ ++ protected final void setEmptinessMapCache(final int chunkX, final int chunkZ, final boolean[] emptinessMap) { ++ this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = emptinessMap; ++ } ++ ++ public static SWMRNibbleArray[] getFilledEmptyLight(final LevelHeightAccessor world) { ++ return getFilledEmptyLight(WorldUtil.getTotalLightSections(world)); ++ } ++ ++ private static SWMRNibbleArray[] getFilledEmptyLight(final int totalLightSections) { ++ final SWMRNibbleArray[] ret = new SWMRNibbleArray[totalLightSections]; ++ ++ for (int i = 0, len = ret.length; i < len; ++i) { ++ ret[i] = new SWMRNibbleArray(null, true); ++ } ++ ++ return ret; ++ } ++ ++ protected abstract boolean[] getEmptinessMap(final ChunkAccess chunk); ++ ++ protected abstract void setEmptinessMap(final ChunkAccess chunk, final boolean[] to); ++ ++ protected abstract SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk); ++ ++ protected abstract void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to); ++ ++ protected abstract boolean canUseChunk(final ChunkAccess chunk); ++ ++ public final void blocksChangedInChunk(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, ++ final Set<BlockPos> positions, final Boolean[] changedSections) { ++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); ++ try { ++ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); ++ if (chunk == null) { ++ return; ++ } ++ if (changedSections != null) { ++ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, changedSections, false); ++ if (ret != null) { ++ this.setEmptinessMap(chunk, ret); ++ } ++ } ++ if (!positions.isEmpty()) { ++ this.propagateBlockChanges(lightAccess, chunk, positions); ++ } ++ this.updateVisible(lightAccess); ++ } finally { ++ this.destroyCaches(); ++ } ++ } ++ ++ // subclasses should not initialise caches, as this will always be done by the super call ++ // subclasses should not invoke updateVisible, as this will always be done by the super call ++ protected abstract void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions); ++ ++ protected abstract void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ); ++ ++ // if ret > expect, then the real value is at least ret (early returns if ret > expect, rather than calculating actual) ++ // if ret == expect, then expect is the correct light value for pos ++ // if ret < expect, then ret is the real light value ++ protected abstract int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, ++ final int expect); ++ ++ protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16]; ++ protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16]; ++ ++ protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess chunk, ++ final int chunkX, final int chunkY, final int chunkZ) { ++ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); ++ if (currNibble == null) { ++ return; ++ } ++ ++ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { ++ final int neighbourOffX = direction.x; ++ final int neighbourOffZ = direction.z; ++ ++ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX, ++ chunkY, chunkZ + neighbourOffZ); ++ ++ if (neighbourNibble == null) { ++ continue; ++ } ++ ++ if (!currNibble.isInitialisedUpdating() && !neighbourNibble.isInitialisedUpdating()) { ++ // both are zero, nothing to check. ++ continue; ++ } ++ ++ // this chunk ++ final int incX; ++ final int incZ; ++ final int startX; ++ final int startZ; ++ ++ if (neighbourOffX != 0) { ++ // x direction ++ incX = 0; ++ incZ = 1; ++ ++ if (direction.x < 0) { ++ // negative ++ startX = chunkX << 4; ++ } else { ++ startX = chunkX << 4 | 15; ++ } ++ startZ = chunkZ << 4; ++ } else { ++ // z direction ++ incX = 1; ++ incZ = 0; ++ ++ if (neighbourOffZ < 0) { ++ // negative ++ startZ = chunkZ << 4; ++ } else { ++ startZ = chunkZ << 4 | 15; ++ } ++ startX = chunkX << 4; ++ } ++ ++ int centerDelayedChecks = 0; ++ int neighbourDelayedChecks = 0; ++ for (int currY = chunkY << 4, maxY = currY | 15; currY <= maxY; ++currY) { ++ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { ++ final int neighbourX = currX + neighbourOffX; ++ final int neighbourZ = currZ + neighbourOffZ; ++ ++ final int currentIndex = (currX & 15) | ++ ((currZ & 15)) << 4 | ++ ((currY & 15) << 8); ++ final int currentLevel = currNibble.getUpdating(currentIndex); ++ ++ final int neighbourIndex = ++ (neighbourX & 15) | ++ ((neighbourZ & 15)) << 4 | ++ ((currY & 15) << 8); ++ final int neighbourLevel = neighbourNibble.getUpdating(neighbourIndex); ++ ++ // the checks are delayed because the checkBlock method clobbers light values - which then ++ // affect later calculate light value operations. While they don't affect it in a behaviourly significant ++ // way, they do have a negative performance impact due to simply queueing more values ++ ++ if (this.calculateLightValue(lightAccess, currX, currY, currZ, currentLevel) != currentLevel) { ++ this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex; ++ } ++ ++ if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != neighbourLevel) { ++ this.chunkCheckDelayedUpdatesNeighbour[neighbourDelayedChecks++] = neighbourIndex; ++ } ++ } ++ } ++ ++ final int currentChunkOffX = chunkX << 4; ++ final int currentChunkOffZ = chunkZ << 4; ++ final int neighbourChunkOffX = (chunkX + direction.x) << 4; ++ final int neighbourChunkOffZ = (chunkZ + direction.z) << 4; ++ final int chunkOffY = chunkY << 4; ++ for (int i = 0, len = Math.max(centerDelayedChecks, neighbourDelayedChecks); i < len; ++i) { ++ // try to queue neighbouring data together ++ // index = x | (z << 4) | (y << 8) ++ if (i < centerDelayedChecks) { ++ final int value = this.chunkCheckDelayedUpdatesCenter[i]; ++ this.checkBlock(lightAccess, currentChunkOffX | (value & 15), ++ chunkOffY | (value >>> 8), ++ currentChunkOffZ | ((value >>> 4) & 0xF)); ++ } ++ if (i < neighbourDelayedChecks) { ++ final int value = this.chunkCheckDelayedUpdatesNeighbour[i]; ++ this.checkBlock(lightAccess, neighbourChunkOffX | (value & 15), ++ chunkOffY | (value >>> 8), ++ neighbourChunkOffZ | ((value >>> 4) & 0xF)); ++ } ++ } ++ } ++ } ++ ++ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) { ++ final ChunkPos chunkPos = chunk.getPos(); ++ final int chunkX = chunkPos.x; ++ final int chunkZ = chunkPos.z; ++ ++ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { ++ this.checkChunkEdge(lightAccess, chunk, chunkX, iterator.nextShort(), chunkZ); ++ } ++ ++ this.performLightDecrease(lightAccess); ++ } ++ ++ // subclasses should not initialise caches, as this will always be done by the super call ++ // subclasses should not invoke updateVisible, as this will always be done by the super call ++ // verifies that light levels on this chunks edges are consistent with this chunk's neighbours ++ // edges. if they are not, they are decreased (effectively performing the logic in checkBlock). ++ // This does not resolve skylight source problems. ++ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { ++ final ChunkPos chunkPos = chunk.getPos(); ++ final int chunkX = chunkPos.x; ++ final int chunkZ = chunkPos.z; ++ ++ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) { ++ this.checkChunkEdge(lightAccess, chunk, chunkX, currSectionY, chunkZ); ++ } ++ ++ this.performLightDecrease(lightAccess); ++ } ++ ++ // pulls light from neighbours, and adds them into the increase queue. does not actually propagate. ++ protected final void propagateNeighbourLevels(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { ++ final ChunkPos chunkPos = chunk.getPos(); ++ final int chunkX = chunkPos.x; ++ final int chunkZ = chunkPos.z; ++ ++ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) { ++ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, currSectionY, chunkZ); ++ if (currNibble == null) { ++ continue; ++ } ++ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { ++ final int neighbourOffX = direction.x; ++ final int neighbourOffZ = direction.z; ++ ++ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX, ++ currSectionY, chunkZ + neighbourOffZ); ++ ++ if (neighbourNibble == null || !neighbourNibble.isInitialisedUpdating()) { ++ // can't pull from 0 ++ continue; ++ } ++ ++ // neighbour chunk ++ final int incX; ++ final int incZ; ++ final int startX; ++ final int startZ; ++ ++ if (neighbourOffX != 0) { ++ // x direction ++ incX = 0; ++ incZ = 1; ++ ++ if (direction.x < 0) { ++ // negative ++ startX = (chunkX << 4) - 1; ++ } else { ++ startX = (chunkX << 4) + 16; ++ } ++ startZ = chunkZ << 4; ++ } else { ++ // z direction ++ incX = 1; ++ incZ = 0; ++ ++ if (neighbourOffZ < 0) { ++ // negative ++ startZ = (chunkZ << 4) - 1; ++ } else { ++ startZ = (chunkZ << 4) + 16; ++ } ++ startX = chunkX << 4; ++ } ++ ++ final long propagateDirection = 1L << direction.getOpposite().ordinal(); // we only want to check in this direction towards this chunk ++ final int encodeOffset = this.coordinateOffset; ++ ++ for (int currY = currSectionY << 4, maxY = currY | 15; currY <= maxY; ++currY) { ++ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { ++ final int level = neighbourNibble.getUpdating( ++ (currX & 15) ++ | ((currZ & 15) << 4) ++ | ((currY & 15) << 8) ++ ); ++ ++ if (level <= 1) { ++ // nothing to propagate ++ continue; ++ } ++ ++ this.appendToIncreaseQueue( ++ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((level & 0xFL) << (6 + 6 + 16)) ++ | (propagateDirection << (6 + 6 + 16 + 4)) ++ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the current block is transparent, must check. ++ ); ++ } ++ } ++ } ++ } ++ } ++ ++ public static Boolean[] getEmptySectionsForChunk(final ChunkAccess chunk) { ++ final LevelChunkSection[] sections = chunk.getSections(); ++ final Boolean[] ret = new Boolean[sections.length]; ++ ++ for (int i = 0; i < sections.length; ++i) { ++ if (sections[i] == null || sections[i].hasOnlyAir()) { ++ ret[i] = Boolean.TRUE; ++ } else { ++ ret[i] = Boolean.FALSE; ++ } ++ } ++ ++ return ret; ++ } ++ ++ public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptinessChanges) { ++ final int chunkX = chunk.getPos().x; ++ final int chunkZ = chunk.getPos().z; ++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); ++ try { ++ // force current chunk into cache ++ this.setChunkInCache(chunkX, chunkZ, chunk); ++ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); ++ this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk)); ++ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); ++ ++ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false); ++ if (ret != null) { ++ this.setEmptinessMap(chunk, ret); ++ } ++ this.updateVisible(lightAccess); ++ } finally { ++ this.destroyCaches(); ++ } ++ } ++ ++ public final void handleEmptySectionChanges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, ++ final Boolean[] emptinessChanges) { ++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); ++ try { ++ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); ++ if (chunk == null) { ++ return; ++ } ++ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false); ++ if (ret != null) { ++ this.setEmptinessMap(chunk, ret); ++ } ++ this.updateVisible(lightAccess); ++ } finally { ++ this.destroyCaches(); ++ } ++ } ++ ++ protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles); ++ ++ protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ); ++ ++ // subclasses should not initialise caches, as this will always be done by the super call ++ // subclasses should not invoke updateVisible, as this will always be done by the super call ++ // subclasses are guaranteed that this is always called before a changed block set ++ // newChunk specifies whether the changes describe a "first load" of a chunk or changes to existing, already loaded chunks ++ // rets non-null when the emptiness map changed and needs to be updated ++ protected final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, ++ final Boolean[] emptinessChanges, final boolean unlit) { ++ final Level world = (Level)lightAccess.getLevel(); ++ final int chunkX = chunk.getPos().x; ++ final int chunkZ = chunk.getPos().z; ++ ++ boolean[] chunkEmptinessMap = this.getEmptinessMap(chunkX, chunkZ); ++ boolean[] ret = null; ++ final boolean needsInit = unlit || chunkEmptinessMap == null; ++ if (needsInit) { ++ this.setEmptinessMapCache(chunkX, chunkZ, ret = chunkEmptinessMap = new boolean[WorldUtil.getTotalSections(world)]); ++ } ++ ++ // update emptiness map ++ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) { ++ Boolean valueBoxed = emptinessChanges[sectionIndex]; ++ if (valueBoxed == null) { ++ if (!needsInit) { ++ continue; ++ } ++ final LevelChunkSection section = this.getChunkSection(chunkX, sectionIndex + this.minSection, chunkZ); ++ emptinessChanges[sectionIndex] = valueBoxed = section == null || section.hasOnlyAir() ? Boolean.TRUE : Boolean.FALSE; ++ } ++ chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue(); ++ } ++ ++ // now init neighbour nibbles ++ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) { ++ final Boolean valueBoxed = emptinessChanges[sectionIndex]; ++ final int sectionY = sectionIndex + this.minSection; ++ if (valueBoxed == null) { ++ continue; ++ } ++ ++ final boolean empty = valueBoxed.booleanValue(); ++ ++ if (empty) { ++ continue; ++ } ++ ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ // if we're not empty, we also need to initialise nibbles ++ // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up ++ final boolean extrude = (dx | dz) != 0 || !unlit; ++ for (int dy = 1; dy >= -1; --dy) { ++ this.initNibble(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false); ++ } ++ } ++ } ++ } ++ ++ // check for de-init and lazy-init ++ // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running ++ // init checks. ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ // does this neighbour have 1 radius loaded? ++ boolean neighboursLoaded = true; ++ neighbour_loaded_search: ++ for (int dz2 = -1; dz2 <= 1; ++dz2) { ++ for (int dx2 = -1; dx2 <= 1; ++dx2) { ++ if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) { ++ neighboursLoaded = false; ++ break neighbour_loaded_search; ++ } ++ } ++ } ++ ++ for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) { ++ // check neighbours to see if we need to de-init this one ++ boolean allEmpty = true; ++ neighbour_search: ++ for (int dy2 = -1; dy2 <= 1; ++dy2) { ++ for (int dz2 = -1; dz2 <= 1; ++dz2) { ++ for (int dx2 = -1; dx2 <= 1; ++dx2) { ++ final int y = sectionY + dy2; ++ if (y < this.minSection || y > this.maxSection) { ++ // empty ++ continue; ++ } ++ final boolean[] emptinessMap = this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ); ++ if (emptinessMap != null) { ++ if (!emptinessMap[y - this.minSection]) { ++ allEmpty = false; ++ break neighbour_search; ++ } ++ } else { ++ final LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ); ++ if (section != null && !section.hasOnlyAir()) { ++ allEmpty = false; ++ break neighbour_search; ++ } ++ } ++ } ++ } ++ } ++ ++ if (allEmpty & neighboursLoaded) { ++ // can only de-init when neighbours are loaded ++ // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting ++ // to be correct ++ ++ // all were empty, so de-init ++ this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ); ++ } else if (!allEmpty) { ++ // must init ++ final boolean extrude = (dx | dz) != 0 || !unlit; ++ this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false); ++ } ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) { ++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); ++ try { ++ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); ++ if (chunk == null) { ++ return; ++ } ++ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); ++ this.updateVisible(lightAccess); ++ } finally { ++ this.destroyCaches(); ++ } ++ } ++ ++ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) { ++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); ++ try { ++ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); ++ if (chunk == null) { ++ return; ++ } ++ this.checkChunkEdges(lightAccess, chunk, sections); ++ this.updateVisible(lightAccess); ++ } finally { ++ this.destroyCaches(); ++ } ++ } ++ ++ // subclasses should not initialise caches, as this will always be done by the super call ++ // subclasses should not invoke updateVisible, as this will always be done by the super call ++ // needsEdgeChecks applies when possibly loading vanilla data, which means we need to validate the current ++ // chunks light values with respect to neighbours ++ // subclasses should note that the emptiness changes are propagated BEFORE this is called, so this function ++ // does not need to detect empty chunks itself (and it should do no handling for them either!) ++ protected abstract void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks); ++ ++ public final void light(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptySections) { ++ final int chunkX = chunk.getPos().x; ++ final int chunkZ = chunk.getPos().z; ++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); ++ ++ try { ++ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.maxLightSection - this.minLightSection + 1); ++ // force current chunk into cache ++ this.setChunkInCache(chunkX, chunkZ, chunk); ++ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); ++ this.setNibblesForChunkInCache(chunkX, chunkZ, nibbles); ++ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); ++ ++ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptySections, true); ++ if (ret != null) { ++ this.setEmptinessMap(chunk, ret); ++ } ++ this.lightChunk(lightAccess, chunk, true); ++ this.setNibbles(chunk, nibbles); ++ this.updateVisible(lightAccess); ++ } finally { ++ this.destroyCaches(); ++ } ++ } ++ ++ public final void relightChunks(final LightChunkGetter lightAccess, final Set<ChunkPos> chunks, ++ final Consumer<ChunkPos> chunkLightCallback, final IntConsumer onComplete) { ++ // it's recommended for maximum performance that the set is ordered according to a BFS from the center of ++ // the region of chunks to relight ++ // it's required that tickets are added for each chunk to keep them loaded ++ final Long2ObjectOpenHashMap<SWMRNibbleArray[]> nibblesByChunk = new Long2ObjectOpenHashMap<>(); ++ final Long2ObjectOpenHashMap<boolean[]> emptinessMapByChunk = new Long2ObjectOpenHashMap<>(); ++ ++ final int[] neighbourLightOrder = new int[] { ++ // d = 0 ++ 0, 0, ++ // d = 1 ++ -1, 0, ++ 0, -1, ++ 1, 0, ++ 0, 1, ++ // d = 2 ++ -1, 1, ++ 1, 1, ++ -1, -1, ++ 1, -1, ++ }; ++ ++ int lightCalls = 0; ++ ++ for (final ChunkPos chunkPos : chunks) { ++ final int chunkX = chunkPos.x; ++ final int chunkZ = chunkPos.z; ++ final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(chunkX, chunkZ); ++ if (chunk == null || !this.canUseChunk(chunk)) { ++ throw new IllegalStateException(); ++ } ++ ++ for (int i = 0, len = neighbourLightOrder.length; i < len; i += 2) { ++ final int dx = neighbourLightOrder[i]; ++ final int dz = neighbourLightOrder[i + 1]; ++ final int neighbourX = dx + chunkX; ++ final int neighbourZ = dz + chunkZ; ++ ++ final ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX, neighbourZ); ++ if (neighbour == null || !this.canUseChunk(neighbour)) { ++ continue; ++ } ++ ++ if (nibblesByChunk.get(CoordinateUtils.getChunkKey(neighbourX, neighbourZ)) != null) { ++ // lit already called for neighbour, no need to light it now ++ continue; ++ } ++ ++ // light neighbour chunk ++ this.setupEncodeOffset(neighbourX * 16 + 7, 128, neighbourZ * 16 + 7); ++ try { ++ // insert all neighbouring chunks for this neighbour that we have data for ++ for (int dz2 = -1; dz2 <= 1; ++dz2) { ++ for (int dx2 = -1; dx2 <= 1; ++dx2) { ++ final int neighbourX2 = neighbourX + dx2; ++ final int neighbourZ2 = neighbourZ + dz2; ++ final long key = CoordinateUtils.getChunkKey(neighbourX2, neighbourZ2); ++ final ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX2, neighbourZ2); ++ if (neighbour2 == null || !this.canUseChunk(neighbour2)) { ++ continue; ++ } ++ ++ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(key); ++ if (nibbles == null) { ++ // we haven't lit this chunk ++ continue; ++ } ++ ++ this.setChunkInCache(neighbourX2, neighbourZ2, neighbour2); ++ this.setBlocksForChunkInCache(neighbourX2, neighbourZ2, neighbour2.getSections()); ++ this.setNibblesForChunkInCache(neighbourX2, neighbourZ2, nibbles); ++ this.setEmptinessMapCache(neighbourX2, neighbourZ2, emptinessMapByChunk.get(key)); ++ } ++ } ++ ++ final long key = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); ++ ++ // now insert the neighbour chunk and light it ++ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.world); ++ nibblesByChunk.put(key, nibbles); ++ ++ this.setChunkInCache(neighbourX, neighbourZ, neighbour); ++ this.setBlocksForChunkInCache(neighbourX, neighbourZ, neighbour.getSections()); ++ this.setNibblesForChunkInCache(neighbourX, neighbourZ, nibbles); ++ ++ final boolean[] neighbourEmptiness = this.handleEmptySectionChanges(lightAccess, neighbour, getEmptySectionsForChunk(neighbour), true); ++ emptinessMapByChunk.put(key, neighbourEmptiness); ++ if (chunks.contains(new ChunkPos(neighbourX, neighbourZ))) { ++ this.setEmptinessMap(neighbour, neighbourEmptiness); ++ } ++ ++ this.lightChunk(lightAccess, neighbour, false); ++ } finally { ++ this.destroyCaches(); ++ } ++ } ++ ++ // done lighting all neighbours, so the chunk is now fully lit ++ ++ // make sure nibbles are fully updated before calling back ++ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ for (final SWMRNibbleArray nibble : nibbles) { ++ nibble.updateVisible(); ++ } ++ ++ this.setNibbles(chunk, nibbles); ++ ++ for (int y = this.minLightSection; y <= this.maxLightSection; ++y) { ++ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkZ)); ++ } ++ ++ // now do callback ++ if (chunkLightCallback != null) { ++ chunkLightCallback.accept(chunkPos); ++ } ++ ++lightCalls; ++ } ++ ++ if (onComplete != null) { ++ onComplete.accept(lightCalls); ++ } ++ } ++ ++ // contains: ++ // lower (6 + 6 + 16) = 28 bits: encoded coordinate position (x | (z << 6) | (y << (6 + 6)))) ++ // next 4 bits: propagated light level (0, 15] ++ // next 6 bits: propagation direction bitset ++ // next 24 bits: unused ++ // last 3 bits: state flags ++ // state flags: ++ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading light ++ // updates for block sources ++ protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 2; ++ // whether the propagation needs to check if its current level is equal to the expected level ++ // used only in increase propagation ++ protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 1; ++ // whether the propagation needs to consider if its block is conditionally transparent ++ protected static final long FLAG_HAS_SIDED_TRANSPARENT_BLOCKS = Long.MIN_VALUE; ++ ++ protected long[] increaseQueue = new long[16 * 16 * 16]; ++ protected int increaseQueueInitialLength; ++ protected long[] decreaseQueue = new long[16 * 16 * 16]; ++ protected int decreaseQueueInitialLength; ++ ++ protected final long[] resizeIncreaseQueue() { ++ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2); ++ } ++ ++ protected final long[] resizeDecreaseQueue() { ++ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2); ++ } ++ ++ protected final void appendToIncreaseQueue(final long value) { ++ final int idx = this.increaseQueueInitialLength++; ++ long[] queue = this.increaseQueue; ++ if (idx >= queue.length) { ++ queue = this.resizeIncreaseQueue(); ++ queue[idx] = value; ++ } else { ++ queue[idx] = value; ++ } ++ } ++ ++ protected final void appendToDecreaseQueue(final long value) { ++ final int idx = this.decreaseQueueInitialLength++; ++ long[] queue = this.decreaseQueue; ++ if (idx >= queue.length) { ++ queue = this.resizeDecreaseQueue(); ++ queue[idx] = value; ++ } else { ++ queue[idx] = value; ++ } ++ } ++ ++ protected static final AxisDirection[][] OLD_CHECK_DIRECTIONS = new AxisDirection[1 << 6][]; ++ protected static final int ALL_DIRECTIONS_BITSET = (1 << 6) - 1; ++ static { ++ for (int i = 0; i < OLD_CHECK_DIRECTIONS.length; ++i) { ++ final List<AxisDirection> directions = new ArrayList<>(); ++ for (int bitset = i, len = Integer.bitCount(i), index = 0; index < len; ++index, bitset ^= IntegerUtil.getTrailingBit(bitset)) { ++ directions.add(AXIS_DIRECTIONS[IntegerUtil.trailingZeros(bitset)]); ++ } ++ OLD_CHECK_DIRECTIONS[i] = directions.toArray(new AxisDirection[0]); ++ } ++ } ++ ++ protected final void performLightIncrease(final LightChunkGetter lightAccess) { ++ final BlockGetter world = lightAccess.getLevel(); ++ long[] queue = this.increaseQueue; ++ int queueReadIndex = 0; ++ int queueLength = this.increaseQueueInitialLength; ++ this.increaseQueueInitialLength = 0; ++ final int decodeOffsetX = -this.encodeOffsetX; ++ final int decodeOffsetY = -this.encodeOffsetY; ++ final int decodeOffsetZ = -this.encodeOffsetZ; ++ final int encodeOffset = this.coordinateOffset; ++ final int sectionOffset = this.chunkSectionIndexOffset; ++ ++ while (queueReadIndex < queueLength) { ++ final long queueValue = queue[queueReadIndex++]; ++ ++ final int posX = ((int)queueValue & 63) + decodeOffsetX; ++ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; ++ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; ++ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xFL); ++ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63L)]; ++ ++ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) { ++ if (this.getLightLevel(posX, posY, posZ) != propagatedLightLevel) { ++ // not at the level we expect, so something changed. ++ continue; ++ } ++ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) { ++ // these are used to restore block sources after a propagation decrease ++ this.setLightLevel(posX, posY, posZ, propagatedLightLevel); ++ } ++ ++ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { ++ // we don't need to worry about our state here. ++ for (final AxisDirection propagate : checkDirections) { ++ final int offX = posX + propagate.x; ++ final int offY = posY + propagate.y; ++ final int offZ = posZ + propagate.z; ++ ++ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; ++ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); ++ ++ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; ++ final int currentLevel; ++ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { ++ continue; // already at the level we want or unloaded ++ } ++ ++ final BlockState blockState = this.getBlockState(sectionIndex, localIndex); ++ if (blockState == null) { ++ continue; ++ } ++ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached(); ++ if (opacityCached != -1) { ++ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached); ++ if (targetLevel > currentLevel) { ++ currentNibble.set(localIndex, targetLevel); ++ this.postLightUpdate(offX, offY, offZ); ++ ++ if (targetLevel > 1) { ++ if (queueLength >= queue.length) { ++ queue = this.resizeIncreaseQueue(); ++ } ++ queue[queueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((targetLevel & 0xFL) << (6 + 6 + 16)) ++ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); ++ continue; ++ } ++ } ++ continue; ++ } else { ++ this.mutablePos1.set(offX, offY, offZ); ++ long flags = 0; ++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); ++ ++ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { ++ continue; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = blockState.getLightBlock(world, this.mutablePos1); ++ final int targetLevel = propagatedLightLevel - Math.max(1, opacity); ++ if (targetLevel <= currentLevel) { ++ continue; ++ } ++ ++ currentNibble.set(localIndex, targetLevel); ++ this.postLightUpdate(offX, offY, offZ); ++ ++ if (targetLevel > 1) { ++ if (queueLength >= queue.length) { ++ queue = this.resizeIncreaseQueue(); ++ } ++ queue[queueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((targetLevel & 0xFL) << (6 + 6 + 16)) ++ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) ++ | (flags); ++ } ++ continue; ++ } ++ } ++ } else { ++ // we actually need to worry about our state here ++ final BlockState fromBlock = this.getBlockState(posX, posY, posZ); ++ this.mutablePos2.set(posX, posY, posZ); ++ for (final AxisDirection propagate : checkDirections) { ++ final int offX = posX + propagate.x; ++ final int offY = posY + propagate.y; ++ final int offZ = posZ + propagate.z; ++ ++ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); ++ ++ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { ++ continue; ++ } ++ ++ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; ++ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); ++ ++ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; ++ final int currentLevel; ++ ++ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { ++ continue; // already at the level we want ++ } ++ ++ final BlockState blockState = this.getBlockState(sectionIndex, localIndex); ++ if (blockState == null) { ++ continue; ++ } ++ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached(); ++ if (opacityCached != -1) { ++ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached); ++ if (targetLevel > currentLevel) { ++ currentNibble.set(localIndex, targetLevel); ++ this.postLightUpdate(offX, offY, offZ); ++ ++ if (targetLevel > 1) { ++ if (queueLength >= queue.length) { ++ queue = this.resizeIncreaseQueue(); ++ } ++ queue[queueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((targetLevel & 0xFL) << (6 + 6 + 16)) ++ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); ++ continue; ++ } ++ } ++ continue; ++ } else { ++ this.mutablePos1.set(offX, offY, offZ); ++ long flags = 0; ++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); ++ ++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { ++ continue; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = blockState.getLightBlock(world, this.mutablePos1); ++ final int targetLevel = propagatedLightLevel - Math.max(1, opacity); ++ if (targetLevel <= currentLevel) { ++ continue; ++ } ++ ++ currentNibble.set(localIndex, targetLevel); ++ this.postLightUpdate(offX, offY, offZ); ++ ++ if (targetLevel > 1) { ++ if (queueLength >= queue.length) { ++ queue = this.resizeIncreaseQueue(); ++ } ++ queue[queueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((targetLevel & 0xFL) << (6 + 6 + 16)) ++ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) ++ | (flags); ++ } ++ continue; ++ } ++ } ++ } ++ } ++ } ++ ++ protected final void performLightDecrease(final LightChunkGetter lightAccess) { ++ final BlockGetter world = lightAccess.getLevel(); ++ long[] queue = this.decreaseQueue; ++ long[] increaseQueue = this.increaseQueue; ++ int queueReadIndex = 0; ++ int queueLength = this.decreaseQueueInitialLength; ++ this.decreaseQueueInitialLength = 0; ++ int increaseQueueLength = this.increaseQueueInitialLength; ++ final int decodeOffsetX = -this.encodeOffsetX; ++ final int decodeOffsetY = -this.encodeOffsetY; ++ final int decodeOffsetZ = -this.encodeOffsetZ; ++ final int encodeOffset = this.coordinateOffset; ++ final int sectionOffset = this.chunkSectionIndexOffset; ++ final int emittedMask = this.emittedLightMask; ++ ++ while (queueReadIndex < queueLength) { ++ final long queueValue = queue[queueReadIndex++]; ++ ++ final int posX = ((int)queueValue & 63) + decodeOffsetX; ++ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; ++ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; ++ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); ++ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)]; ++ ++ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { ++ // we don't need to worry about our state here. ++ for (final AxisDirection propagate : checkDirections) { ++ final int offX = posX + propagate.x; ++ final int offY = posY + propagate.y; ++ final int offZ = posZ + propagate.z; ++ ++ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; ++ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); ++ ++ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; ++ final int lightLevel; ++ ++ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { ++ // already at lowest (or unloaded), nothing we can do ++ continue; ++ } ++ ++ final BlockState blockState = this.getBlockState(sectionIndex, localIndex); ++ if (blockState == null) { ++ continue; ++ } ++ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached(); ++ if (opacityCached != -1) { ++ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached)); ++ if (lightLevel > targetLevel) { ++ // it looks like another source propagated here, so re-propagate it ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((lightLevel & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | FLAG_RECHECK_LEVEL; ++ continue; ++ } ++ final int emittedLight = blockState.getLightEmission() & emittedMask; ++ if (emittedLight != 0) { ++ // re-propagate source ++ // note: do not set recheck level, or else the propagation will fail ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((emittedLight & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL); ++ } ++ ++ currentNibble.set(localIndex, 0); ++ this.postLightUpdate(offX, offY, offZ); ++ ++ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... ++ if (queueLength >= queue.length) { ++ queue = this.resizeDecreaseQueue(); ++ } ++ queue[queueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((targetLevel & 0xFL) << (6 + 6 + 16)) ++ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); ++ continue; ++ } ++ continue; ++ } else { ++ this.mutablePos1.set(offX, offY, offZ); ++ long flags = 0; ++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); ++ ++ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { ++ continue; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = blockState.getLightBlock(world, this.mutablePos1); ++ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); ++ if (lightLevel > targetLevel) { ++ // it looks like another source propagated here, so re-propagate it ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((lightLevel & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (FLAG_RECHECK_LEVEL | flags); ++ continue; ++ } ++ final int emittedLight = blockState.getLightEmission() & emittedMask; ++ if (emittedLight != 0) { ++ // re-propagate source ++ // note: do not set recheck level, or else the propagation will fail ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((emittedLight & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (flags | FLAG_WRITE_LEVEL); ++ } ++ ++ currentNibble.set(localIndex, 0); ++ this.postLightUpdate(offX, offY, offZ); ++ ++ if (targetLevel > 0) { ++ if (queueLength >= queue.length) { ++ queue = this.resizeDecreaseQueue(); ++ } ++ queue[queueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((targetLevel & 0xFL) << (6 + 6 + 16)) ++ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) ++ | flags; ++ } ++ continue; ++ } ++ } ++ } else { ++ // we actually need to worry about our state here ++ final BlockState fromBlock = this.getBlockState(posX, posY, posZ); ++ this.mutablePos2.set(posX, posY, posZ); ++ for (final AxisDirection propagate : checkDirections) { ++ final int offX = posX + propagate.x; ++ final int offY = posY + propagate.y; ++ final int offZ = posZ + propagate.z; ++ ++ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; ++ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); ++ ++ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); ++ ++ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { ++ continue; ++ } ++ ++ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; ++ final int lightLevel; ++ ++ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { ++ // already at lowest (or unloaded), nothing we can do ++ continue; ++ } ++ ++ final BlockState blockState = this.getBlockState(sectionIndex, localIndex); ++ if (blockState == null) { ++ continue; ++ } ++ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached(); ++ if (opacityCached != -1) { ++ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached)); ++ if (lightLevel > targetLevel) { ++ // it looks like another source propagated here, so re-propagate it ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((lightLevel & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | FLAG_RECHECK_LEVEL; ++ continue; ++ } ++ final int emittedLight = blockState.getLightEmission() & emittedMask; ++ if (emittedLight != 0) { ++ // re-propagate source ++ // note: do not set recheck level, or else the propagation will fail ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((emittedLight & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL); ++ } ++ ++ currentNibble.set(localIndex, 0); ++ this.postLightUpdate(offX, offY, offZ); ++ ++ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... ++ if (queueLength >= queue.length) { ++ queue = this.resizeDecreaseQueue(); ++ } ++ queue[queueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((targetLevel & 0xFL) << (6 + 6 + 16)) ++ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); ++ continue; ++ } ++ continue; ++ } else { ++ this.mutablePos1.set(offX, offY, offZ); ++ long flags = 0; ++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); ++ ++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { ++ continue; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = blockState.getLightBlock(world, this.mutablePos1); ++ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); ++ if (lightLevel > targetLevel) { ++ // it looks like another source propagated here, so re-propagate it ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((lightLevel & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (FLAG_RECHECK_LEVEL | flags); ++ continue; ++ } ++ final int emittedLight = blockState.getLightEmission() & emittedMask; ++ if (emittedLight != 0) { ++ // re-propagate source ++ // note: do not set recheck level, or else the propagation will fail ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((emittedLight & 0xFL) << (6 + 6 + 16)) ++ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) ++ | (flags | FLAG_WRITE_LEVEL); ++ } ++ ++ currentNibble.set(localIndex, 0); ++ this.postLightUpdate(offX, offY, offZ); ++ ++ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... ++ if (queueLength >= queue.length) { ++ queue = this.resizeDecreaseQueue(); ++ } ++ queue[queueLength++] = ++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) ++ | ((targetLevel & 0xFL) << (6 + 6 + 16)) ++ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) ++ | flags; ++ } ++ continue; ++ } ++ } ++ } ++ } ++ ++ // propagate sources we clobbered ++ this.increaseQueueInitialLength = increaseQueueLength; ++ this.performLightIncrease(lightAccess); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c64ab41198a5e0c7cbcbe6452af11f82f5938862 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java +@@ -0,0 +1,930 @@ ++package ca.spottedleaf.moonrise.patches.starlight.light; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus; ++import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; ++import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.shorts.ShortCollection; ++import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.SectionPos; ++import net.minecraft.server.level.ChunkLevel; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.DataLayer; ++import net.minecraft.world.level.chunk.LightChunkGetter; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.lighting.LayerLightEventListener; ++import net.minecraft.world.level.lighting.LevelLightEngine; ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Set; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.function.BooleanSupplier; ++import java.util.function.Consumer; ++import java.util.function.IntConsumer; ++ ++public final class StarLightInterface { ++ ++ public static final TicketType<Long> CHUNK_WORK_TICKET = TicketType.create("starlight:chunk_work_ticket", Long::compareTo); ++ public static final int LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(ChunkStatus.LIGHT); ++ // ticket level = ChunkLevel.byStatus(FullChunkStatus.FULL) - input ++ public static final int REGION_LIGHT_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.FULL) - LIGHT_TICKET_LEVEL; ++ ++ /** ++ * Can be {@code null}, indicating the light is all empty. ++ */ ++ public final Level world; ++ public final LightChunkGetter lightAccess; ++ ++ private final ArrayDeque<SkyStarLightEngine> cachedSkyPropagators; ++ private final ArrayDeque<BlockStarLightEngine> cachedBlockPropagators; ++ ++ private final LightQueue lightQueue; ++ ++ private final LayerLightEventListener skyReader; ++ private final LayerLightEventListener blockReader; ++ private final boolean isClientSide; ++ ++ public final int minSection; ++ public final int maxSection; ++ public final int minLightSection; ++ public final int maxLightSection; ++ ++ public final LevelLightEngine lightEngine; ++ ++ private final boolean hasBlockLight; ++ private final boolean hasSkyLight; ++ ++ public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) { ++ this.lightAccess = lightAccess; ++ this.world = lightAccess == null ? null : (Level)lightAccess.getLevel(); ++ this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null; ++ this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null; ++ this.isClientSide = !(this.world instanceof ServerLevel); ++ if (this.world == null) { ++ this.minSection = -4; ++ this.maxSection = 19; ++ this.minLightSection = -5; ++ this.maxLightSection = 20; ++ } else { ++ this.minSection = WorldUtil.getMinSection(this.world); ++ this.maxSection = WorldUtil.getMaxSection(this.world); ++ this.minLightSection = WorldUtil.getMinLightSection(this.world); ++ this.maxLightSection = WorldUtil.getMaxLightSection(this.world); ++ } ++ ++ if (this.world instanceof ServerLevel) { ++ this.lightQueue = new ServerLightQueue(this); ++ } else { ++ this.lightQueue = new ClientLightQueue(this); ++ } ++ ++ this.lightEngine = lightEngine; ++ this.hasBlockLight = hasBlockLight; ++ this.hasSkyLight = hasSkyLight; ++ this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { ++ @Override ++ public void checkBlock(final BlockPos blockPos) { ++ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); ++ } ++ ++ @Override ++ public void propagateLightSources(final ChunkPos chunkPos) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean hasLightWork() { ++ // not really correct... ++ return StarLightInterface.this.hasUpdates(); ++ } ++ ++ @Override ++ public int runLightUpdates() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public DataLayer getDataLayerData(final SectionPos pos) { ++ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); ++ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) { ++ return null; ++ } ++ ++ final int sectionY = pos.getY(); ++ ++ if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) { ++ return null; ++ } ++ ++ if (((StarlightChunk)chunk).starlight$getSkyEmptinessMap() == null) { ++ return null; ++ } ++ ++ return ((StarlightChunk)chunk).starlight$getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble(); ++ } ++ ++ @Override ++ public int getLightValue(final BlockPos blockPos) { ++ return StarLightInterface.this.getSkyLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4)); ++ } ++ ++ @Override ++ public void updateSectionStatus(final SectionPos pos, final boolean notReady) { ++ StarLightInterface.this.sectionChange(pos, notReady); ++ } ++ }; ++ this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { ++ @Override ++ public void checkBlock(final BlockPos blockPos) { ++ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); ++ } ++ ++ @Override ++ public void propagateLightSources(final ChunkPos chunkPos) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean hasLightWork() { ++ // not really correct... ++ return StarLightInterface.this.hasUpdates(); ++ } ++ ++ @Override ++ public int runLightUpdates() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public DataLayer getDataLayerData(final SectionPos pos) { ++ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); ++ ++ if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) { ++ return null; ++ } ++ ++ return ((StarlightChunk)chunk).starlight$getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble(); ++ } ++ ++ @Override ++ public int getLightValue(final BlockPos blockPos) { ++ return StarLightInterface.this.getBlockLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4)); ++ } ++ ++ @Override ++ public void updateSectionStatus(final SectionPos pos, final boolean notReady) { ++ StarLightInterface.this.sectionChange(pos, notReady); ++ } ++ }; ++ } ++ ++ public ClientLightQueue getClientLightQueue() { ++ if (this.lightQueue instanceof ClientLightQueue clientLightQueue) { ++ return clientLightQueue; ++ } ++ return null; ++ } ++ ++ public ServerLightQueue getServerLightQueue() { ++ if (this.lightQueue instanceof ServerLightQueue serverLightQueue) { ++ return serverLightQueue; ++ } ++ return null; ++ } ++ ++ public boolean hasSkyLight() { ++ return this.hasSkyLight; ++ } ++ ++ public boolean hasBlockLight() { ++ return this.hasBlockLight; ++ } ++ ++ public int getSkyLightValue(final BlockPos blockPos, final ChunkAccess chunk) { ++ if (!this.hasSkyLight) { ++ return 0; ++ } ++ final int x = blockPos.getX(); ++ int y = blockPos.getY(); ++ final int z = blockPos.getZ(); ++ ++ final int minSection = this.minSection; ++ final int maxSection = this.maxSection; ++ final int minLightSection = this.minLightSection; ++ final int maxLightSection = this.maxLightSection; ++ ++ if (chunk == null || (!this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) { ++ return 15; ++ } ++ ++ int sectionY = y >> 4; ++ ++ if (sectionY > maxLightSection) { ++ return 15; ++ } ++ ++ if (sectionY < minLightSection) { ++ sectionY = minLightSection; ++ y = sectionY << 4; ++ } ++ ++ final SWMRNibbleArray[] nibbles = ((StarlightChunk)chunk).starlight$getSkyNibbles(); ++ final SWMRNibbleArray immediate = nibbles[sectionY - minLightSection]; ++ ++ if (!immediate.isNullNibbleVisible()) { ++ return immediate.getVisible(x, y, z); ++ } ++ ++ final boolean[] emptinessMap = ((StarlightChunk)chunk).starlight$getSkyEmptinessMap(); ++ ++ if (emptinessMap == null) { ++ return 15; ++ } ++ ++ // are we above this chunk's lowest empty section? ++ int lowestY = minLightSection - 1; ++ for (int currY = maxSection; currY >= minSection; --currY) { ++ if (emptinessMap[currY - minSection]) { ++ continue; ++ } ++ ++ // should always be full lit here ++ lowestY = currY; ++ break; ++ } ++ ++ if (sectionY > lowestY) { ++ return 15; ++ } ++ ++ // this nibble is going to depend solely on the skylight data above it ++ // find first non-null data above (there does exist one, as we just found it above) ++ for (int currY = sectionY + 1; currY <= maxLightSection; ++currY) { ++ final SWMRNibbleArray nibble = nibbles[currY - minLightSection]; ++ if (!nibble.isNullNibbleVisible()) { ++ return nibble.getVisible(x, 0, z); ++ } ++ } ++ ++ // should never reach here ++ return 15; ++ } ++ ++ public int getBlockLightValue(final BlockPos blockPos, final ChunkAccess chunk) { ++ if (!this.hasBlockLight) { ++ return 0; ++ } ++ final int y = blockPos.getY(); ++ final int cy = y >> 4; ++ ++ final int minLightSection = this.minLightSection; ++ final int maxLightSection = this.maxLightSection; ++ ++ if (cy < minLightSection || cy > maxLightSection) { ++ return 0; ++ } ++ ++ if (chunk == null) { ++ return 0; ++ } ++ ++ final SWMRNibbleArray nibble = ((StarlightChunk)chunk).starlight$getBlockNibbles()[cy - minLightSection]; ++ return nibble.getVisible(blockPos.getX(), y, blockPos.getZ()); ++ } ++ ++ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { ++ final ChunkAccess chunk = this.getAnyChunkNow(pos.getX() >> 4, pos.getZ() >> 4); ++ ++ final int sky = this.getSkyLightValue(pos, chunk) - ambientDarkness; ++ // Don't fetch the block light level if the skylight level is 15, since the value will never be higher. ++ if (sky == 15) { ++ return 15; ++ } ++ final int block = this.getBlockLightValue(pos, chunk); ++ return Math.max(sky, block); ++ } ++ ++ public LayerLightEventListener getSkyReader() { ++ return this.skyReader; ++ } ++ ++ public LayerLightEventListener getBlockReader() { ++ return this.blockReader; ++ } ++ ++ public boolean isClientSide() { ++ return this.isClientSide; ++ } ++ ++ public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) { ++ if (this.world == null) { ++ // empty world ++ return null; ++ } ++ return ((ChunkSystemLevel)this.world).moonrise$getAnyChunkIfLoaded(chunkX, chunkZ); ++ } ++ ++ public boolean hasUpdates() { ++ return !this.lightQueue.isEmpty(); ++ } ++ ++ public Level getWorld() { ++ return this.world; ++ } ++ ++ public LightChunkGetter getLightAccess() { ++ return this.lightAccess; ++ } ++ ++ public SkyStarLightEngine getSkyLightEngine() { ++ if (this.cachedSkyPropagators == null) { ++ return null; ++ } ++ final SkyStarLightEngine ret; ++ synchronized (this.cachedSkyPropagators) { ++ ret = this.cachedSkyPropagators.pollFirst(); ++ } ++ ++ if (ret == null) { ++ return new SkyStarLightEngine(this.world); ++ } ++ return ret; ++ } ++ ++ public void releaseSkyLightEngine(final SkyStarLightEngine engine) { ++ if (this.cachedSkyPropagators == null) { ++ return; ++ } ++ synchronized (this.cachedSkyPropagators) { ++ this.cachedSkyPropagators.addFirst(engine); ++ } ++ } ++ ++ public BlockStarLightEngine getBlockLightEngine() { ++ if (this.cachedBlockPropagators == null) { ++ return null; ++ } ++ final BlockStarLightEngine ret; ++ synchronized (this.cachedBlockPropagators) { ++ ret = this.cachedBlockPropagators.pollFirst(); ++ } ++ ++ if (ret == null) { ++ return new BlockStarLightEngine(this.world); ++ } ++ return ret; ++ } ++ ++ public void releaseBlockLightEngine(final BlockStarLightEngine engine) { ++ if (this.cachedBlockPropagators == null) { ++ return; ++ } ++ synchronized (this.cachedBlockPropagators) { ++ this.cachedBlockPropagators.addFirst(engine); ++ } ++ } ++ ++ public LightQueue.ChunkTasks blockChange(final BlockPos pos) { ++ if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world ++ return null; ++ } ++ ++ return this.lightQueue.queueBlockChange(pos); ++ } ++ ++ public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) { ++ if (this.world == null) { // empty world ++ return null; ++ } ++ ++ return this.lightQueue.queueSectionChange(pos, newEmptyValue); ++ } ++ ++ public void forceLoadInChunk(final ChunkAccess chunk, final Boolean[] emptySections) { ++ final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); ++ final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); ++ ++ try { ++ if (skyEngine != null) { ++ skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); ++ } ++ if (blockEngine != null) { ++ blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); ++ } ++ } finally { ++ this.releaseSkyLightEngine(skyEngine); ++ this.releaseBlockLightEngine(blockEngine); ++ } ++ } ++ ++ public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) { ++ final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); ++ final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); ++ ++ try { ++ if (skyEngine != null) { ++ skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); ++ } ++ if (blockEngine != null) { ++ blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); ++ } ++ } finally { ++ this.releaseSkyLightEngine(skyEngine); ++ this.releaseBlockLightEngine(blockEngine); ++ } ++ } ++ ++ public void lightChunk(final ChunkAccess chunk, final Boolean[] emptySections) { ++ final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); ++ final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); ++ ++ try { ++ if (skyEngine != null) { ++ skyEngine.light(this.lightAccess, chunk, emptySections); ++ } ++ if (blockEngine != null) { ++ blockEngine.light(this.lightAccess, chunk, emptySections); ++ } ++ } finally { ++ this.releaseSkyLightEngine(skyEngine); ++ this.releaseBlockLightEngine(blockEngine); ++ } ++ } ++ ++ public void relightChunks(final Set<ChunkPos> chunks, final Consumer<ChunkPos> chunkLightCallback, ++ final IntConsumer onComplete) { ++ final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); ++ final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); ++ ++ try { ++ if (skyEngine != null) { ++ skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null, ++ blockEngine == null ? onComplete : null); ++ } ++ if (blockEngine != null) { ++ blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete); ++ } ++ } finally { ++ this.releaseSkyLightEngine(skyEngine); ++ this.releaseBlockLightEngine(blockEngine); ++ } ++ } ++ ++ public void checkChunkEdges(final int chunkX, final int chunkZ) { ++ this.checkSkyEdges(chunkX, chunkZ); ++ this.checkBlockEdges(chunkX, chunkZ); ++ } ++ ++ public void checkSkyEdges(final int chunkX, final int chunkZ) { ++ final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); ++ ++ try { ++ if (skyEngine != null) { ++ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); ++ } ++ } finally { ++ this.releaseSkyLightEngine(skyEngine); ++ } ++ } ++ ++ public void checkBlockEdges(final int chunkX, final int chunkZ) { ++ final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); ++ try { ++ if (blockEngine != null) { ++ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); ++ } ++ } finally { ++ this.releaseBlockLightEngine(blockEngine); ++ } ++ } ++ ++ public void propagateChanges() { ++ final LightQueue lightQueue = this.lightQueue; ++ if (lightQueue instanceof ClientLightQueue clientLightQueue) { ++ clientLightQueue.drainTasks(); ++ } // else: invalid usage, although we won't throw because mods... ++ } ++ ++ public static abstract class LightQueue { ++ ++ protected final StarLightInterface lightInterface; ++ ++ public LightQueue(final StarLightInterface lightInterface) { ++ this.lightInterface = lightInterface; ++ } ++ ++ public abstract boolean isEmpty(); ++ ++ public abstract ChunkTasks queueBlockChange(final BlockPos pos); ++ ++ public abstract ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue); ++ ++ public abstract ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections); ++ ++ public abstract ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections); ++ ++ public static abstract class ChunkTasks implements Runnable { ++ ++ public final long chunkCoordinate; ++ ++ protected final StarLightInterface lightEngine; ++ protected final LightQueue queue; ++ protected final MultiThreadedQueue<Runnable> onComplete = new MultiThreadedQueue<>(); ++ protected final Set<BlockPos> changedPositions = new HashSet<>(); ++ protected Boolean[] changedSectionSet; ++ protected ShortOpenHashSet queuedEdgeChecksSky; ++ protected ShortOpenHashSet queuedEdgeChecksBlock; ++ protected List<BooleanSupplier> lightTasks; ++ ++ public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue) { ++ this.chunkCoordinate = chunkCoordinate; ++ this.lightEngine = lightEngine; ++ this.queue = queue; ++ } ++ ++ @Override ++ public abstract void run(); ++ ++ public void queueOrRunTask(final Runnable run) { ++ if (!this.onComplete.add(run)) { ++ run.run(); ++ } ++ } ++ ++ protected void addChangedPosition(final BlockPos pos) { ++ this.changedPositions.add(pos.immutable()); ++ } ++ ++ protected void setChangedSection(final int y, final Boolean newEmptyValue) { ++ if (this.changedSectionSet == null) { ++ this.changedSectionSet = new Boolean[this.lightEngine.maxSection - this.lightEngine.minSection + 1]; ++ } ++ this.changedSectionSet[y - this.lightEngine.minSection] = newEmptyValue; ++ } ++ ++ protected void addLightTask(final BooleanSupplier lightTask) { ++ if (this.lightTasks == null) { ++ this.lightTasks = new ArrayList<>(); ++ } ++ this.lightTasks.add(lightTask); ++ } ++ ++ protected void addEdgeChecksSky(final ShortCollection values) { ++ if (this.queuedEdgeChecksSky == null) { ++ this.queuedEdgeChecksSky = new ShortOpenHashSet(Math.max(8, values.size())); ++ } ++ this.queuedEdgeChecksSky.addAll(values); ++ } ++ ++ protected void addEdgeChecksBlock(final ShortCollection values) { ++ if (this.queuedEdgeChecksBlock == null) { ++ this.queuedEdgeChecksBlock = new ShortOpenHashSet(Math.max(8, values.size())); ++ } ++ this.queuedEdgeChecksBlock.addAll(values); ++ } ++ ++ protected final void runTasks() { ++ boolean litChunk = false; ++ if (this.lightTasks != null) { ++ for (final BooleanSupplier run : this.lightTasks) { ++ if (run.getAsBoolean()) { ++ litChunk = true; ++ break; ++ } ++ } ++ } ++ ++ if (!litChunk) { ++ final SkyStarLightEngine skyEngine = this.lightEngine.getSkyLightEngine(); ++ final BlockStarLightEngine blockEngine = this.lightEngine.getBlockLightEngine(); ++ try { ++ final long coordinate = this.chunkCoordinate; ++ final int chunkX = CoordinateUtils.getChunkX(coordinate); ++ final int chunkZ = CoordinateUtils.getChunkZ(coordinate); ++ ++ final Set<BlockPos> positions = this.changedPositions; ++ final Boolean[] sectionChanges = this.changedSectionSet; ++ ++ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { ++ skyEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges); ++ } ++ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { ++ blockEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges); ++ } ++ ++ if (skyEngine != null && this.queuedEdgeChecksSky != null) { ++ skyEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksSky); ++ } ++ if (blockEngine != null && this.queuedEdgeChecksBlock != null) { ++ blockEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksBlock); ++ } ++ } finally { ++ this.lightEngine.releaseSkyLightEngine(skyEngine); ++ this.lightEngine.releaseBlockLightEngine(blockEngine); ++ } ++ } ++ ++ Runnable run; ++ while ((run = this.onComplete.pollOrBlockAdds()) != null) { ++ run.run(); ++ } ++ } ++ } ++ } ++ ++ public static final class ClientLightQueue extends LightQueue { ++ ++ private final Long2ObjectLinkedOpenHashMap<ClientChunkTasks> chunkTasks = new Long2ObjectLinkedOpenHashMap<>(); ++ ++ public ClientLightQueue(final StarLightInterface lightInterface) { ++ super(lightInterface); ++ } ++ ++ @Override ++ public synchronized boolean isEmpty() { ++ return this.chunkTasks.isEmpty(); ++ } ++ ++ // must hold synchronized lock on this object ++ private ClientChunkTasks getOrCreate(final long key) { ++ return this.chunkTasks.computeIfAbsent(key, (final long keyInMap) -> { ++ return new ClientChunkTasks(keyInMap, ClientLightQueue.this.lightInterface, ClientLightQueue.this); ++ }); ++ } ++ ++ @Override ++ public synchronized ClientChunkTasks queueBlockChange(final BlockPos pos) { ++ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); ++ tasks.addChangedPosition(pos); ++ return tasks; ++ } ++ ++ @Override ++ public synchronized ClientChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { ++ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); ++ ++ tasks.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue)); ++ ++ return tasks; ++ } ++ ++ @Override ++ public synchronized ClientChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { ++ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); ++ ++ tasks.addEdgeChecksSky(sections); ++ ++ return tasks; ++ } ++ ++ @Override ++ public synchronized ClientChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { ++ final ClientChunkTasks tasks = this.getOrCreate(CoordinateUtils.getChunkKey(pos)); ++ ++ tasks.addEdgeChecksBlock(sections); ++ ++ return tasks; ++ } ++ ++ public synchronized ClientChunkTasks removeFirstTask() { ++ if (this.chunkTasks.isEmpty()) { ++ return null; ++ } ++ return this.chunkTasks.removeFirst(); ++ } ++ ++ public void drainTasks() { ++ ClientChunkTasks task; ++ while ((task = this.removeFirstTask()) != null) { ++ task.runTasks(); ++ } ++ } ++ ++ public static final class ClientChunkTasks extends ChunkTasks { ++ ++ public ClientChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final ClientLightQueue queue) { ++ super(chunkCoordinate, lightEngine, queue); ++ } ++ ++ @Override ++ public void run() { ++ this.runTasks(); ++ } ++ } ++ } ++ ++ public static final class ServerLightQueue extends LightQueue { ++ ++ private final ConcurrentLong2ReferenceChainedHashTable<ServerChunkTasks> chunkTasks = new ConcurrentLong2ReferenceChainedHashTable<>(); ++ ++ public ServerLightQueue(final StarLightInterface lightInterface) { ++ super(lightInterface); ++ } ++ ++ public void lowerPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ if (task != null) { ++ task.lowerPriority(priority); ++ } ++ } ++ ++ public void setPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ if (task != null) { ++ task.setPriority(priority); ++ } ++ } ++ ++ public void raisePriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ if (task != null) { ++ task.raisePriority(priority); ++ } ++ } ++ ++ public PrioritisedExecutor.Priority getPriority(final int chunkX, final int chunkZ) { ++ final ServerChunkTasks task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ if (task != null) { ++ return task.getPriority(); ++ } ++ ++ return PrioritisedExecutor.Priority.COMPLETING; ++ } ++ ++ @Override ++ public boolean isEmpty() { ++ return this.chunkTasks.isEmpty(); ++ } ++ ++ @Override ++ public ServerChunkTasks queueBlockChange(final BlockPos pos) { ++ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { ++ if (valueInMap == null) { ++ valueInMap = new ServerChunkTasks( ++ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this ++ ); ++ } ++ valueInMap.addChangedPosition(pos); ++ return valueInMap; ++ }); ++ ++ ret.schedule(); ++ ++ return ret; ++ } ++ ++ @Override ++ public ServerChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { ++ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { ++ if (valueInMap == null) { ++ valueInMap = new ServerChunkTasks( ++ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this ++ ); ++ } ++ ++ valueInMap.setChangedSection(pos.getY(), Boolean.valueOf(newEmptyValue)); ++ ++ return valueInMap; ++ }); ++ ++ ret.schedule(); ++ ++ return ret; ++ } ++ ++ public ServerChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final PrioritisedExecutor.Priority priority) { ++ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { ++ if (valueInMap == null) { ++ valueInMap = new ServerChunkTasks( ++ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this, priority ++ ); ++ } ++ ++ valueInMap.addLightTask(lightTask); ++ ++ return valueInMap; ++ }); ++ ++ ret.schedule(); ++ ++ return ret; ++ } ++ ++ @Override ++ public ServerChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { ++ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { ++ if (valueInMap == null) { ++ valueInMap = new ServerChunkTasks( ++ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this ++ ); ++ } ++ ++ valueInMap.addEdgeChecksSky(sections); ++ ++ return valueInMap; ++ }); ++ ++ ret.schedule(); ++ ++ return ret; ++ } ++ ++ @Override ++ public ServerChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { ++ final ServerChunkTasks ret = this.chunkTasks.compute(CoordinateUtils.getChunkKey(pos), (final long keyInMap, ServerChunkTasks valueInMap) -> { ++ if (valueInMap == null) { ++ valueInMap = new ServerChunkTasks( ++ keyInMap, ServerLightQueue.this.lightInterface, ServerLightQueue.this ++ ); ++ } ++ ++ valueInMap.addEdgeChecksBlock(sections); ++ ++ return valueInMap; ++ }); ++ ++ ret.schedule(); ++ ++ return ret; ++ } ++ ++ public static final class ServerChunkTasks extends ChunkTasks { ++ ++ private final AtomicBoolean ticketAdded = new AtomicBoolean(); ++ private final PrioritisedExecutor.PrioritisedTask task; ++ ++ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, ++ final ServerLightQueue queue) { ++ this(chunkCoordinate, lightEngine, queue, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, ++ final ServerLightQueue queue, final PrioritisedExecutor.Priority priority) { ++ super(chunkCoordinate, lightEngine, queue); ++ this.task = ((ChunkSystemServerLevel)(ServerLevel)lightEngine.getWorld()).moonrise$getChunkTaskScheduler().radiusAwareScheduler.createTask( ++ CoordinateUtils.getChunkX(chunkCoordinate), CoordinateUtils.getChunkZ(chunkCoordinate), ++ ((ChunkSystemChunkStatus)ChunkStatus.LIGHT).moonrise$getWriteRadius(), this, priority ++ ); ++ } ++ ++ public boolean markTicketAdded() { ++ return !this.ticketAdded.get() && !this.ticketAdded.getAndSet(true); ++ } ++ ++ public void schedule() { ++ this.task.queue(); ++ } ++ ++ public boolean cancel() { ++ return this.task.cancel(); ++ } ++ ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.task.getPriority(); ++ } ++ ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ this.task.lowerPriority(priority); ++ } ++ ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ this.task.setPriority(priority); ++ } ++ ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ this.task.raisePriority(priority); ++ } ++ ++ @Override ++ public void run() { ++ ((ServerLightQueue)this.queue).chunkTasks.remove(this.chunkCoordinate, this); ++ ++ this.runTasks(); ++ } ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7fe59ab70557aa6a484a02db2b2007fdd9e4bbb8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightLightingProvider.java +@@ -0,0 +1,29 @@ ++package ca.spottedleaf.moonrise.patches.starlight.light; ++ ++import net.minecraft.core.SectionPos; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.LightLayer; ++import net.minecraft.world.level.chunk.DataLayer; ++import net.minecraft.world.level.chunk.LevelChunk; ++import java.util.Collection; ++import java.util.function.Consumer; ++import java.util.function.IntConsumer; ++ ++public interface StarLightLightingProvider { ++ ++ public StarLightInterface starlight$getLightEngine(); ++ ++ public void starlight$clientUpdateLight(final LightLayer lightType, final SectionPos pos, ++ final DataLayer nibble, final boolean trustEdges); ++ ++ public void starlight$clientRemoveLightData(final ChunkPos chunkPos); ++ ++ public void starlight$clientChunkLoad(final ChunkPos pos, final LevelChunk chunk); ++ ++ public default int starlight$serverRelightChunks(final Collection<ChunkPos> chunks, ++ final Consumer<ChunkPos> chunkLightCallback, ++ final IntConsumer onComplete) throws UnsupportedOperationException { ++ throw new UnsupportedOperationException(); ++ } ++ ++} +diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..57692a503e147a00ac4e1586cd78e12b71a80d3f +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java +@@ -0,0 +1,188 @@ ++package ca.spottedleaf.moonrise.patches.starlight.util; ++ ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk; ++import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray; ++import ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine; ++import com.mojang.logging.LogUtils; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.ListTag; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import org.slf4j.Logger; ++ ++public final class SaveUtil { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private static final int STARLIGHT_LIGHT_VERSION = 9; ++ ++ public static int getLightVersion() { ++ return STARLIGHT_LIGHT_VERSION; ++ } ++ ++ private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; ++ private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; ++ private static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; ++ ++ public static void saveLightHook(final Level world, final ChunkAccess chunk, final CompoundTag nbt) { ++ try { ++ saveLightHookReal(world, chunk, nbt); ++ } catch (final Throwable ex) { ++ // failing to inject is not fatal so we catch anything here. if it fails, it will have correctly set lit to false ++ // for Vanilla to relight on load and it will not set our lit tag so we will relight on load ++ LOGGER.warn("Failed to inject light data into save data for chunk " + chunk.getPos() + ", chunk light will be recalculated on its next load", ex); ++ } ++ } ++ ++ private static void saveLightHookReal(final Level world, final ChunkAccess chunk, final CompoundTag tag) { ++ if (tag == null) { ++ return; ++ } ++ ++ final int minSection = WorldUtil.getMinLightSection(world); ++ final int maxSection = WorldUtil.getMaxLightSection(world); ++ ++ SWMRNibbleArray[] blockNibbles = ((StarlightChunk)chunk).starlight$getBlockNibbles(); ++ SWMRNibbleArray[] skyNibbles = ((StarlightChunk)chunk).starlight$getSkyNibbles(); ++ ++ boolean lit = chunk.isLightCorrect() || !(world instanceof ServerLevel); ++ // diff start - store our tag for whether light data is init'd ++ if (lit) { ++ tag.putBoolean("isLightOn", false); ++ } ++ // diff end - store our tag for whether light data is init'd ++ ChunkStatus status = ChunkStatus.byName(tag.getString("Status")); ++ ++ CompoundTag[] sections = new CompoundTag[maxSection - minSection + 1]; ++ ++ ListTag sectionsStored = tag.getList("sections", 10); ++ ++ for (int i = 0; i < sectionsStored.size(); ++i) { ++ CompoundTag sectionStored = sectionsStored.getCompound(i); ++ int k = sectionStored.getByte("Y"); ++ ++ // strip light data ++ sectionStored.remove("BlockLight"); ++ sectionStored.remove("SkyLight"); ++ ++ if (!sectionStored.isEmpty()) { ++ sections[k - minSection] = sectionStored; ++ } ++ } ++ ++ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { ++ for (int i = minSection; i <= maxSection; ++i) { ++ SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState(); ++ SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState(); ++ if (blockNibble != null || skyNibble != null) { ++ CompoundTag section = sections[i - minSection]; ++ if (section == null) { ++ section = new CompoundTag(); ++ section.putByte("Y", (byte)i); ++ sections[i - minSection] = section; ++ } ++ ++ // we store under the same key so mod programs editing nbt ++ // can still read the data, hopefully. ++ // however, for compatibility we store chunks as unlit so vanilla ++ // is forced to re-light them if it encounters our data. It's too much of a burden ++ // to try and maintain compatibility with a broken and inferior skylight management system. ++ ++ if (blockNibble != null) { ++ if (blockNibble.data != null) { ++ section.putByteArray("BlockLight", blockNibble.data); ++ } ++ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state); ++ } ++ ++ if (skyNibble != null) { ++ if (skyNibble.data != null) { ++ section.putByteArray("SkyLight", skyNibble.data); ++ } ++ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state); ++ } ++ } ++ } ++ } ++ ++ // rewrite section list ++ sectionsStored.clear(); ++ for (CompoundTag section : sections) { ++ if (section != null) { ++ sectionsStored.add(section); ++ } ++ } ++ tag.put("sections", sectionsStored); ++ if (lit) { ++ tag.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // only mark as fully lit after we have successfully injected our data ++ } ++ } ++ ++ public static void loadLightHook(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) { ++ try { ++ loadLightHookReal(world, pos, tag, into); ++ } catch (final Throwable ex) { ++ // failing to inject is not fatal so we catch anything here. if it fails, then we simply relight. Not a problem, we get correct ++ // lighting in both cases. ++ LOGGER.warn("Failed to load light for chunk " + pos + ", light will be recalculated", ex); ++ } ++ } ++ ++ private static void loadLightHookReal(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) { ++ if (into == null) { ++ return; ++ } ++ final int minSection = WorldUtil.getMinLightSection(world); ++ final int maxSection = WorldUtil.getMaxLightSection(world); ++ ++ into.setLightCorrect(false); // mark as unlit in case we fail parsing ++ ++ SWMRNibbleArray[] blockNibbles = StarLightEngine.getFilledEmptyLight(world); ++ SWMRNibbleArray[] skyNibbles = StarLightEngine.getFilledEmptyLight(world); ++ ++ ++ // start copy from the original method ++ boolean lit = tag.get("isLightOn") != null && tag.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; ++ boolean canReadSky = world.dimensionType().hasSkyLight(); ++ ChunkStatus status = ChunkStatus.byName(tag.getString("Status")); ++ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { // diff - we add the status check here ++ ListTag sections = tag.getList("sections", 10); ++ ++ for (int i = 0; i < sections.size(); ++i) { ++ CompoundTag sectionData = sections.getCompound(i); ++ int y = sectionData.getByte("Y"); ++ ++ if (sectionData.contains("BlockLight", 7)) { ++ // this is where our diff is ++ blockNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety ++ } else { ++ blockNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG)); ++ } ++ ++ if (canReadSky) { ++ if (sectionData.contains("SkyLight", 7)) { ++ // we store under the same key so mod programs editing nbt ++ // can still read the data, hopefully. ++ // however, for compatibility we store chunks as unlit so vanilla ++ // is forced to re-light them if it encounters our data. It's too much of a burden ++ // to try and maintain compatibility with a broken and inferior skylight management system. ++ skyNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety ++ } else { ++ skyNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG)); ++ } ++ } ++ } ++ } ++ // end copy from vanilla ++ ++ ((StarlightChunk)into).starlight$setBlockNibbles(blockNibbles); ++ ((StarlightChunk)into).starlight$setSkyNibbles(skyNibbles); ++ into.setLightCorrect(lit); // now we set lit here, only after we've correctly parsed data ++ } ++ ++ private SaveUtil() {} ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java +index a79abe9b26f68d573812e91554124783075ae17a..183d99ec9b94ca20a823c46a2d6bf0a215046d48 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java ++++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java +@@ -25,6 +25,10 @@ import java.util.List; + import java.util.concurrent.CompletableFuture; + import java.util.function.Consumer; + ++/** ++ * @deprecated Use {@link ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem} ++ */ ++@Deprecated(forRemoval = true) + public final class ChunkSystem { + + private static final Logger LOGGER = LogUtils.getLogger(); +@@ -35,35 +39,17 @@ public final class ChunkSystem { + } + + public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) { +- scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); ++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkTask(level, chunkX, chunkZ, run); // Paper - reroute + } + + public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) { +- level.chunkSource.mainThreadProcessor.execute(run); ++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkTask(level, chunkX, chunkZ, run, priority); // Paper - reroute + } + + public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen, + final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority, + final Consumer<ChunkAccess> onComplete) { +- if (gen) { +- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); +- return; +- } +- scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { +- if (chunk == null) { +- if (onComplete != null) { +- onComplete.accept(null); +- } +- } else { +- if (chunk.getPersistedStatus().isOrAfter(toStatus)) { +- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); +- } else { +- if (onComplete != null) { +- onComplete.accept(null); +- } +- } +- } +- }); ++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkLoad(level, chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete); // Paper - reroute + } + + static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo); +@@ -71,160 +57,29 @@ public final class ChunkSystem { + private static long chunkLoadCounter = 0L; + public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, + final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) { +- if (!Bukkit.isPrimaryThread()) { +- scheduleChunkTask(level, chunkX, chunkZ, () -> { +- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); +- }, priority); +- return; +- } +- +- final int minLevel = 33 + ChunkSystem.getDistance(toStatus); +- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; +- final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); +- +- if (addTicket) { +- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); +- } +- level.chunkSource.runDistanceManagerUpdates(); +- +- final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> { +- try { +- if (onComplete != null) { +- onComplete.accept(chunk); +- } +- } catch (final Throwable thr) { +- LOGGER.error("Exception handling chunk load callback", thr); +- SneakyThrow.sneaky(thr); +- } finally { +- if (addTicket) { +- level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); +- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); +- } +- } +- }; +- +- final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); +- +- if (holder == null || holder.getTicketLevel() > minLevel) { +- loadCallback.accept(null); +- return; +- } +- +- final CompletableFuture<ChunkResult<ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap); +- +- if (loadFuture.isDone()) { +- loadCallback.accept(loadFuture.join().orElse(null)); +- return; +- } +- +- loadFuture.whenCompleteAsync((final ChunkResult<ChunkAccess> result, final Throwable thr) -> { +- if (thr != null) { +- loadCallback.accept(null); +- return; +- } +- loadCallback.accept(result.orElse(null)); +- }, (final Runnable r) -> { +- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST); +- }); ++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - reroute + } + + public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, + final FullChunkStatus toStatus, final boolean addTicket, + final PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) { +- // This method goes unused until the chunk system rewrite +- if (toStatus == FullChunkStatus.INACCESSIBLE) { +- throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); +- } +- +- if (!Bukkit.isPrimaryThread()) { +- scheduleChunkTask(level, chunkX, chunkZ, () -> { +- scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); +- }, priority); +- return; +- } +- +- final int minLevel = 33 - (toStatus.ordinal() - 1); +- final int radius = toStatus.ordinal() - 1; +- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; +- final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); +- +- if (addTicket) { +- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); +- } +- level.chunkSource.runDistanceManagerUpdates(); +- +- final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> { +- try { +- if (onComplete != null) { +- onComplete.accept(chunk); +- } +- } catch (final Throwable thr) { +- LOGGER.error("Exception handling chunk load callback", thr); +- SneakyThrow.sneaky(thr); +- } finally { +- if (addTicket) { +- level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); +- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); +- } +- } +- }; +- +- final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); +- +- if (holder == null || holder.getTicketLevel() > minLevel) { +- loadCallback.accept(null); +- return; +- } +- +- final CompletableFuture<ChunkResult<LevelChunk>> tickingState; +- switch (toStatus) { +- case FULL: { +- tickingState = holder.getFullChunkFuture(); +- break; +- } +- case BLOCK_TICKING: { +- tickingState = holder.getTickingChunkFuture(); +- break; +- } +- case ENTITY_TICKING: { +- tickingState = holder.getEntityTickingChunkFuture(); +- break; +- } +- default: { +- throw new IllegalStateException("Cannot reach here"); +- } +- } +- +- if (tickingState.isDone()) { +- loadCallback.accept(tickingState.join().orElse(null)); +- return; +- } +- +- tickingState.whenCompleteAsync((final ChunkResult<LevelChunk> result, final Throwable thr) -> { +- if (thr != null) { +- loadCallback.accept(null); +- return; +- } +- loadCallback.accept(result.orElse(null)); +- }, (final Runnable r) -> { +- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST); +- }); ++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - reroute + } + + public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) { +- return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values()); ++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getVisibleChunkHolders(level); // Paper - reroute + } + + public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) { +- return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values()); ++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getUpdatingChunkHolders(level); // Paper - reroute + } + + public static int getVisibleChunkHolderCount(final ServerLevel level) { +- return level.chunkSource.chunkMap.visibleChunkMap.size(); ++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getVisibleChunkHolderCount(level); // Paper - reroute + } + + public static int getUpdatingChunkHolderCount(final ServerLevel level) { +- return level.chunkSource.chunkMap.updatingChunkMap.size(); ++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getUpdatingChunkHolderCount(level); // Paper - reroute + } + + public static boolean hasAnyChunkHolders(final ServerLevel level) { +@@ -268,27 +123,19 @@ public final class ChunkSystem { + } + + public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { +- return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ); ++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getUnloadingChunkHolder(level, chunkX, chunkZ); // Paper - reroute + } + + public static int getSendViewDistance(final ServerPlayer player) { +- return getLoadViewDistance(player); ++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getSendViewDistance(player); // Paper - reroute + } + + public static int getLoadViewDistance(final ServerPlayer player) { +- final ServerLevel level = player.serverLevel(); +- if (level == null) { +- return Bukkit.getViewDistance(); +- } +- return level.chunkSource.chunkMap.getPlayerViewDistance(player); ++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getLoadViewDistance(player); // Paper - reroute + } + + public static int getTickViewDistance(final ServerPlayer player) { +- final ServerLevel level = player.serverLevel(); +- if (level == null) { +- return Bukkit.getSimulationDistance(); +- } +- return level.chunkSource.chunkMap.distanceManager.simulationDistance; ++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getTickViewDistance(player); // Paper - reroute + } + + private ChunkSystem() { +diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java +index 46bf42d5ea9e7b046f962531c5962d287cf44a41..362765d977aaa1996f9cef3404c0676d7bbddf38 100644 +--- a/src/main/java/io/papermc/paper/command/PaperCommand.java ++++ b/src/main/java/io/papermc/paper/command/PaperCommand.java +@@ -42,6 +42,8 @@ public final class PaperCommand extends Command { + commands.put(Set.of("dumpitem"), new DumpItemCommand()); + commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand()); + commands.put(Set.of("dumplisteners"), new DumpListenersCommand()); ++ commands.put(Set.of("fixlight"), new FixLightCommand()); // Paper - rewrite chunk system ++ commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand()); // Paper - rewrite chunk system + + return commands.entrySet().stream() + .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) +diff --git a/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd01fba0fc +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java +@@ -0,0 +1,277 @@ ++package io.papermc.paper.command.subcommands; ++ ++import ca.spottedleaf.moonrise.common.util.JsonUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; ++import io.papermc.paper.command.CommandUtil; ++import io.papermc.paper.command.PaperSubcommand; ++import java.io.File; ++import java.util.ArrayList; ++import java.util.Collections; ++import java.util.List; ++import java.util.Locale; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ImposterProtoChunk; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import org.bukkit.Bukkit; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.CraftWorld; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++import static net.kyori.adventure.text.Component.text; ++import static net.kyori.adventure.text.format.NamedTextColor.BLUE; ++import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA; ++import static net.kyori.adventure.text.format.NamedTextColor.GREEN; ++import static net.kyori.adventure.text.format.NamedTextColor.RED; ++ ++@DefaultQualifier(NonNull.class) ++public final class ChunkDebugCommand implements PaperSubcommand { ++ @Override ++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { ++ switch (subCommand) { ++ case "debug" -> this.doDebug(sender, args); ++ case "chunkinfo" -> this.doChunkInfo(sender, args); ++ case "holderinfo" -> this.doHolderInfo(sender, args); ++ } ++ return true; ++ } ++ ++ @Override ++ public List<String> tabComplete(final CommandSender sender, final String subCommand, final String[] args) { ++ switch (subCommand) { ++ case "debug" -> { ++ if (args.length == 1) { ++ return CommandUtil.getListMatchingLast(sender, args, "help", "chunks"); ++ } ++ } ++ case "holderinfo" -> { ++ List<String> worldNames = new ArrayList<>(); ++ worldNames.add("*"); ++ for (org.bukkit.World world : Bukkit.getWorlds()) { ++ worldNames.add(world.getName()); ++ } ++ if (args.length == 1) { ++ return CommandUtil.getListMatchingLast(sender, args, worldNames); ++ } ++ } ++ case "chunkinfo" -> { ++ List<String> worldNames = new ArrayList<>(); ++ worldNames.add("*"); ++ for (org.bukkit.World world : Bukkit.getWorlds()) { ++ worldNames.add(world.getName()); ++ } ++ if (args.length == 1) { ++ return CommandUtil.getListMatchingLast(sender, args, worldNames); ++ } ++ } ++ } ++ return Collections.emptyList(); ++ } ++ ++ private void doChunkInfo(final CommandSender sender, final String[] args) { ++ List<org.bukkit.World> worlds; ++ if (args.length < 1 || args[0].equals("*")) { ++ worlds = Bukkit.getWorlds(); ++ } else { ++ worlds = new ArrayList<>(args.length); ++ for (final String arg : args) { ++ org.bukkit.@Nullable World world = Bukkit.getWorld(arg); ++ if (world == null) { ++ sender.sendMessage(text("World '" + arg + "' is invalid", RED)); ++ return; ++ } ++ worlds.add(world); ++ } ++ } ++ ++ int accumulatedTotal = 0; ++ int accumulatedInactive = 0; ++ int accumulatedBorder = 0; ++ int accumulatedTicking = 0; ++ int accumulatedEntityTicking = 0; ++ ++ for (final org.bukkit.World bukkitWorld : worlds) { ++ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); ++ ++ int total = 0; ++ int inactive = 0; ++ int full = 0; ++ int blockTicking = 0; ++ int entityTicking = 0; ++ ++ for (final NewChunkHolder holder : ((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders()) { ++ final NewChunkHolder.ChunkCompletion completion = holder.getLastChunkCompletion(); ++ final ChunkAccess chunk = completion == null ? null : completion.chunk(); ++ ++ if (!(chunk instanceof LevelChunk fullChunk)) { ++ continue; ++ } ++ ++ ++total; ++ ++ switch (holder.getChunkStatus()) { ++ case INACCESSIBLE: { ++ ++inactive; ++ break; ++ } ++ case FULL: { ++ ++full; ++ break; ++ } ++ case BLOCK_TICKING: { ++ ++blockTicking; ++ break; ++ } ++ case ENTITY_TICKING: { ++ ++entityTicking; ++ break; ++ } ++ } ++ } ++ ++ accumulatedTotal += total; ++ accumulatedInactive += inactive; ++ accumulatedBorder += full; ++ accumulatedTicking += blockTicking; ++ accumulatedEntityTicking += entityTicking; ++ ++ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":"))); ++ sender.sendMessage(text().color(DARK_AQUA).append( ++ text("Total: ", BLUE), text(total), ++ text(" Inactive: ", BLUE), text(inactive), ++ text(" Full: ", BLUE), text(full), ++ text(" Block Ticking: ", BLUE), text(blockTicking), ++ text(" Entity Ticking: ", BLUE), text(entityTicking) ++ )); ++ } ++ if (worlds.size() > 1) { ++ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA))); ++ sender.sendMessage(text().color(DARK_AQUA).append( ++ text("Total: ", BLUE), text(accumulatedTotal), ++ text(" Inactive: ", BLUE), text(accumulatedInactive), ++ text(" Full: ", BLUE), text(accumulatedBorder), ++ text(" Block Ticking: ", BLUE), text(accumulatedTicking), ++ text(" Entity Ticking: ", BLUE), text(accumulatedEntityTicking) ++ )); ++ } ++ } ++ ++ private void doHolderInfo(final CommandSender sender, final String[] args) { ++ List<org.bukkit.World> worlds; ++ if (args.length < 1 || args[0].equals("*")) { ++ worlds = Bukkit.getWorlds(); ++ } else { ++ worlds = new ArrayList<>(args.length); ++ for (final String arg : args) { ++ org.bukkit.@Nullable World world = Bukkit.getWorld(arg); ++ if (world == null) { ++ sender.sendMessage(text("World '" + arg + "' is invalid", RED)); ++ return; ++ } ++ worlds.add(world); ++ } ++ } ++ ++ int accumulatedTotal = 0; ++ int accumulatedCanUnload = 0; ++ int accumulatedNull = 0; ++ int accumulatedReadOnly = 0; ++ int accumulatedProtoChunk = 0; ++ int accumulatedFullChunk = 0; ++ ++ for (final org.bukkit.World bukkitWorld : worlds) { ++ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); ++ ++ int total = 0; ++ int canUnload = 0; ++ int nullChunks = 0; ++ int readOnly = 0; ++ int protoChunk = 0; ++ int fullChunk = 0; ++ ++ for (final NewChunkHolder holder : ((ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders()) { ++ final NewChunkHolder.ChunkCompletion completion = holder.getLastChunkCompletion(); ++ final ChunkAccess chunk = completion == null ? null : completion.chunk(); ++ ++ ++total; ++ ++ if (chunk == null) { ++ ++nullChunks; ++ } else if (chunk instanceof ImposterProtoChunk) { ++ ++readOnly; ++ } else if (chunk instanceof ProtoChunk) { ++ ++protoChunk; ++ } else if (chunk instanceof LevelChunk) { ++ ++fullChunk; ++ } ++ ++ if (holder.isSafeToUnload() == null) { ++ ++canUnload; ++ } ++ } ++ ++ accumulatedTotal += total; ++ accumulatedCanUnload += canUnload; ++ accumulatedNull += nullChunks; ++ accumulatedReadOnly += readOnly; ++ accumulatedProtoChunk += protoChunk; ++ accumulatedFullChunk += fullChunk; ++ ++ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":"))); ++ sender.sendMessage(text().color(DARK_AQUA).append( ++ text("Total: ", BLUE), text(total), ++ text(" Unloadable: ", BLUE), text(canUnload), ++ text(" Null: ", BLUE), text(nullChunks), ++ text(" ReadOnly: ", BLUE), text(readOnly), ++ text(" Proto: ", BLUE), text(protoChunk), ++ text(" Full: ", BLUE), text(fullChunk) ++ )); ++ } ++ if (worlds.size() > 1) { ++ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA))); ++ sender.sendMessage(text().color(DARK_AQUA).append( ++ text("Total: ", BLUE), text(accumulatedTotal), ++ text(" Unloadable: ", BLUE), text(accumulatedCanUnload), ++ text(" Null: ", BLUE), text(accumulatedNull), ++ text(" ReadOnly: ", BLUE), text(accumulatedReadOnly), ++ text(" Proto: ", BLUE), text(accumulatedProtoChunk), ++ text(" Full: ", BLUE), text(accumulatedFullChunk) ++ )); ++ } ++ } ++ ++ private void doDebug(final CommandSender sender, final String[] args) { ++ if (args.length < 1) { ++ sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED)); ++ return; ++ } ++ ++ final String debugType = args[0].toLowerCase(Locale.ROOT); ++ switch (debugType) { ++ case "chunks" -> { ++ if (args.length >= 2 && args[1].toLowerCase(Locale.ROOT).equals("help")) { ++ sender.sendMessage(text("Use /paper debug chunks to dump loaded chunk information to a file", RED)); ++ break; ++ } ++ final File file = ChunkTaskScheduler.getChunkDebugFile(); ++ sender.sendMessage(text("Writing chunk information dump to " + file, GREEN)); ++ try { ++ JsonUtil.writeJson(ChunkTaskScheduler.debugAllWorlds(MinecraftServer.getServer()), file); ++ sender.sendMessage(text("Successfully written chunk information!", GREEN)); ++ } catch (Throwable thr) { ++ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); ++ sender.sendMessage(text("Failed to dump chunk information, see console", RED)); ++ } ++ } ++ // "help" & default ++ default -> sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED)); ++ } ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..85950a1aa732ab8c01ad28bec9e0de140e1a172e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java +@@ -0,0 +1,116 @@ ++package io.papermc.paper.command.subcommands; ++ ++import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider; ++import io.papermc.paper.command.PaperSubcommand; ++import io.papermc.paper.util.MCUtil; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.level.ThreadedLevelLightEngine; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.bukkit.entity.Player; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++import java.text.DecimalFormat; ++ ++import static net.kyori.adventure.text.Component.text; ++import static net.kyori.adventure.text.format.NamedTextColor.BLUE; ++import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA; ++import static net.kyori.adventure.text.format.NamedTextColor.RED; ++ ++@DefaultQualifier(NonNull.class) ++public final class FixLightCommand implements PaperSubcommand { ++ ++ private static final ThreadLocal<DecimalFormat> ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { ++ return new DecimalFormat("#,##0.0"); ++ }); ++ ++ @Override ++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { ++ this.doFixLight(sender, args); ++ return true; ++ } ++ ++ private void doFixLight(final CommandSender sender, final String[] args) { ++ if (!(sender instanceof Player)) { ++ sender.sendMessage(text("Only players can use this command", RED)); ++ return; ++ } ++ @Nullable Runnable post = null; ++ int radius = 2; ++ if (args.length > 0) { ++ try { ++ final int parsed = Integer.parseInt(args[0]); ++ if (parsed < 0) { ++ sender.sendMessage(text("Radius cannot be negative!", RED)); ++ return; ++ } ++ final int maxRadius = 32; ++ radius = Math.min(maxRadius, parsed); ++ if (radius != parsed) { ++ post = () -> sender.sendMessage(text("Radius '" + parsed + "' was not in the required range [0, " + maxRadius + "], it was lowered to the maximum (" + maxRadius + " chunks).", RED)); ++ } ++ } catch (final Exception e) { ++ sender.sendMessage(text("'" + args[0] + "' is not a valid number.", RED)); ++ return; ++ } ++ } ++ ++ CraftPlayer player = (CraftPlayer) sender; ++ ServerPlayer handle = player.getHandle(); ++ ServerLevel world = (ServerLevel) handle.level(); ++ ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine(); ++ this.starlightFixLight(handle, world, lightengine, radius, post); ++ } ++ ++ private void starlightFixLight( ++ final ServerPlayer sender, ++ final ServerLevel world, ++ final ThreadedLevelLightEngine lightengine, ++ final int radius, ++ final @Nullable Runnable done ++ ) { ++ final long start = System.nanoTime(); ++ final java.util.LinkedHashSet<ChunkPos> chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos ++ ++ final int[] pending = new int[1]; ++ for (java.util.Iterator<ChunkPos> iterator = chunks.iterator(); iterator.hasNext(); ) { ++ final ChunkPos chunkPos = iterator.next(); ++ ++ final @Nullable ChunkAccess chunk = (ChunkAccess) world.getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z); ++ if (chunk == null || !chunk.isLightCorrect() || !chunk.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { ++ // cannot relight this chunk ++ iterator.remove(); ++ continue; ++ } ++ ++ ++pending[0]; ++ } ++ ++ final int[] relitChunks = new int[1]; ++ ((StarLightLightingProvider)lightengine).starlight$serverRelightChunks(chunks, ++ (final ChunkPos chunkPos) -> { ++ ++relitChunks[0]; ++ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append( ++ text("Relit chunk ", BLUE), text(chunkPos.toString()), ++ text(", progress: ", BLUE), text(ONE_DECIMAL_PLACES.get().format(100.0 * (double) (relitChunks[0]) / (double) pending[0]) + "%") ++ )); ++ }, ++ (final int totalRelit) -> { ++ final long end = System.nanoTime(); ++ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append( ++ text("Relit ", BLUE), text(totalRelit), ++ text(" chunks. Took ", BLUE), text(ONE_DECIMAL_PLACES.get().format(1.0e-6 * (end - start)) + "ms") ++ )); ++ if (done != null) { ++ done.run(); ++ } ++ } ++ ); ++ sender.getBukkitEntity().sendMessage(text().color(BLUE).append(text("Relighting "), text(pending[0], DARK_AQUA), text(" chunks"))); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +index 2a5453707bc172d8d0efe3f11959cb0b5f830984..b8499c1cea97a1a88a53053bc7da132f2fd3928d 100644 +--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java ++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +@@ -29,6 +29,45 @@ public class GlobalConfiguration extends ConfigurationPart { + public static GlobalConfiguration get() { + return instance; + } ++ ++ public ChunkLoadingBasic chunkLoadingBasic; ++ ++ public class ChunkLoadingBasic extends ConfigurationPart { ++ @Comment("The maximum rate in chunks per second that the server will send to any individual player. Set to -1 to disable this limit.") ++ public double playerMaxChunkSendRate = 75.0; ++ ++ @Comment( ++ "The maximum rate at which chunks will load for any individual player. " + ++ "Note that this setting also affects chunk generations, since a chunk load is always first issued to test if a" + ++ "chunk is already generated. Set to -1 to disable this limit." ++ ) ++ public double playerMaxChunkLoadRate = 100.0; ++ ++ @Comment("The maximum rate at which chunks will generate for any individual player. Set to -1 to disable this limit.") ++ public double playerMaxChunkGenerateRate = -1.0; ++ } ++ ++ public ChunkLoadingAdvanced chunkLoadingAdvanced; ++ ++ public class ChunkLoadingAdvanced extends ConfigurationPart { ++ @Comment( ++ "Set to true if the server will match the chunk send radius that clients have configured" + ++ "in their view distance settings if the client is less-than the server's send distance." ++ ) ++ public boolean autoConfigSendDistance = true; ++ ++ @Comment( ++ "Specifies the maximum amount of concurrent chunk loads that an individual player can have." + ++ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit." ++ ) ++ public int playerMaxConcurrentChunkLoads = 0; ++ ++ @Comment( ++ "Specifies the maximum amount of concurrent chunk generations that an individual player can have." + ++ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit." ++ ) ++ public int playerMaxConcurrentChunkGenerates = 0; ++ } + static void set(GlobalConfiguration instance) { + GlobalConfiguration.instance = instance; + } +@@ -130,21 +169,6 @@ public class GlobalConfiguration extends ConfigurationPart { + public int incomingPacketThreshold = 300; + } + +- public ChunkLoading chunkLoading; +- +- public class ChunkLoading extends ConfigurationPart { +- public int minLoadRadius = 2; +- public int maxConcurrentSends = 2; +- public boolean autoconfigSendDistance = true; +- public double targetPlayerChunkSendRate = 100.0; +- public double globalMaxChunkSendRate = -1.0; +- public boolean enableFrustumPriority = false; +- public double globalMaxChunkLoadRate = -1.0; +- public double playerMaxConcurrentLoads = 20.0; +- public double globalMaxConcurrentLoads = 500.0; +- public double playerMaxChunkLoadRate = -1.0; +- } +- + public UnsupportedSettings unsupportedSettings; + + public class UnsupportedSettings extends ConfigurationPart { +@@ -203,7 +227,7 @@ public class GlobalConfiguration extends ConfigurationPart { + + @PostProcess + private void postProcess() { +- //io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this); ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.init(this); + } + } + +diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8424cf9d4617b4732d44cc460d25b04481068989 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java +@@ -0,0 +1,10 @@ ++package io.papermc.paper.threadedregions; ++ ++// placeholder class for Folia ++public class TickRegions { ++ ++ public static int getRegionChunkShift() { ++ return ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ThreadedTicketLevelPropagator.SECTION_SHIFT; ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java +index 73e83d56a340f0c7dcb8ff737d621003e72c6de4..d05297d77147ab68f8c5bb08f13a1f882a686c4f 100644 +--- a/src/main/java/io/papermc/paper/util/TickThread.java ++++ b/src/main/java/io/papermc/paper/util/TickThread.java +@@ -6,7 +6,7 @@ import net.minecraft.world.entity.Entity; + import org.bukkit.Bukkit; + import java.util.concurrent.atomic.AtomicInteger; + +-public final class TickThread extends Thread { ++public class TickThread extends Thread { + + public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.strict-thread-checks"); + +@@ -16,6 +16,10 @@ public final class TickThread extends Thread { + } + } + ++ /** ++ * @deprecated ++ */ ++ @Deprecated + public static void softEnsureTickThread(final String reason) { + if (!STRICT_THREAD_CHECKS) { + return; +@@ -23,6 +27,10 @@ public final class TickThread extends Thread { + ensureTickThread(reason); + } + ++ /** ++ * @deprecated ++ */ ++ @Deprecated + public static void ensureTickThread(final String reason) { + if (!isTickThread()) { + MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); +@@ -30,6 +38,21 @@ public final class TickThread extends Thread { + } + } + ++ public static void ensureTickThread(final ServerLevel world, final net.minecraft.core.BlockPos pos, final String reason) { ++ if (!isTickThreadFor(world, pos)) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ public static void ensureTickThread(final ServerLevel world, final net.minecraft.world.level.ChunkPos pos, final String reason) { ++ if (!isTickThreadFor(world, pos)) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ + public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) { + if (!isTickThreadFor(world, chunkX, chunkZ)) { + MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); +@@ -44,6 +67,21 @@ public final class TickThread extends Thread { + } + } + ++ public static void ensureTickThread(final ServerLevel world, final net.minecraft.world.phys.AABB aabb, final String reason) { ++ if (!isTickThreadFor(world, aabb)) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ public static void ensureTickThread(final ServerLevel world, final double blockX, final double blockZ, final String reason) { ++ if (!isTickThreadFor(world, blockX, blockZ)) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ + public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */ + + private static final AtomicInteger ID_GENERATOR = new AtomicInteger(); +@@ -66,13 +104,45 @@ public final class TickThread extends Thread { + } + + public static boolean isTickThread() { +- return Bukkit.isPrimaryThread(); ++ return Thread.currentThread() instanceof TickThread; ++ } ++ ++ public static boolean isShutdownThread() { ++ return false; ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.core.BlockPos pos) { ++ return isTickThread(); ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.level.ChunkPos pos) { ++ return isTickThread(); ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.phys.Vec3 pos) { ++ return isTickThread(); + } + + public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) { + return isTickThread(); + } + ++ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.phys.AABB aabb) { ++ return isTickThread(); ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final double blockX, final double blockZ) { ++ return isTickThread(); ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final net.minecraft.world.phys.Vec3 position, final net.minecraft.world.phys.Vec3 deltaMovement, final int buffer) { ++ return isTickThread(); ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) { ++ return isTickThread(); ++ } ++ + public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) { + return isTickThread(); + } +diff --git a/src/main/java/net/minecraft/core/Direction.java b/src/main/java/net/minecraft/core/Direction.java +index 03c45ee77276462818a6f774b5945b25924aa3f0..f15dd2ccb99ade10ac1e49b63e6f4080bd39b3c9 100644 +--- a/src/main/java/net/minecraft/core/Direction.java ++++ b/src/main/java/net/minecraft/core/Direction.java +@@ -27,7 +27,7 @@ import org.joml.Quaternionf; + import org.joml.Vector3f; + import org.joml.Vector4f; + +-public enum Direction implements StringRepresentable { ++public enum Direction implements StringRepresentable, ca.spottedleaf.moonrise.patches.collisions.util.CollisionDirection { // Paper - optimise collisions + DOWN(0, 1, -1, "down", Direction.AxisDirection.NEGATIVE, Direction.Axis.Y, new Vec3i(0, -1, 0)), + UP(1, 0, -1, "up", Direction.AxisDirection.POSITIVE, Direction.Axis.Y, new Vec3i(0, 1, 0)), + NORTH(2, 3, 2, "north", Direction.AxisDirection.NEGATIVE, Direction.Axis.Z, new Vec3i(0, 0, -1)), +@@ -60,6 +60,46 @@ public enum Direction implements StringRepresentable { + private final int adjY; + private final int adjZ; + // Paper end - Perf: Inline shift direction fields ++ // Paper start - optimise collisions ++ private static final int RANDOM_OFFSET = 2017601568; ++ private Direction opposite; ++ private Quaternionf rotation; ++ private int id; ++ private int stepX; ++ private int stepY; ++ private int stepZ; ++ ++ private Quaternionf getRotationUncached() { ++ switch ((Direction)(Object)this) { ++ case DOWN: { ++ return new Quaternionf().rotationX(3.1415927F); ++ } ++ case UP: { ++ return new Quaternionf(); ++ } ++ case NORTH: { ++ return new Quaternionf().rotationXYZ(1.5707964F, 0.0F, 3.1415927F); ++ } ++ case SOUTH: { ++ return new Quaternionf().rotationX(1.5707964F); ++ } ++ case WEST: { ++ return new Quaternionf().rotationXYZ(1.5707964F, 0.0F, 1.5707964F); ++ } ++ case EAST: { ++ return new Quaternionf().rotationXYZ(1.5707964F, 0.0F, -1.5707964F); ++ } ++ default: { ++ throw new IllegalStateException(); ++ } ++ } ++ } ++ ++ @Override ++ public final int moonrise$uniqueId() { ++ return this.id; ++ } ++ // Paper end - optimise collisions + + private Direction( + final int id, +@@ -134,14 +174,13 @@ public enum Direction implements StringRepresentable { + } + + public Quaternionf getRotation() { +- return switch (this) { +- case DOWN -> new Quaternionf().rotationX((float) Math.PI); +- case UP -> new Quaternionf(); +- case NORTH -> new Quaternionf().rotationXYZ((float) (Math.PI / 2), 0.0F, (float) Math.PI); +- case SOUTH -> new Quaternionf().rotationX((float) (Math.PI / 2)); +- case WEST -> new Quaternionf().rotationXYZ((float) (Math.PI / 2), 0.0F, (float) (Math.PI / 2)); +- case EAST -> new Quaternionf().rotationXYZ((float) (Math.PI / 2), 0.0F, (float) (-Math.PI / 2)); +- }; ++ // Paper start - optimise collisions ++ try { ++ return (Quaternionf)this.rotation.clone(); ++ } catch (final CloneNotSupportedException ex) { ++ throw new InternalError(ex); ++ } ++ // Paper end - optimise collisions + } + + public int get3DDataValue() { +@@ -165,7 +204,7 @@ public enum Direction implements StringRepresentable { + } + + public Direction getOpposite() { +- return from3DDataValue(this.oppositeIndex); ++ return this.opposite; // Paper - optimise collisions + } + + public Direction getClockWise(Direction.Axis axis) { +@@ -551,4 +590,17 @@ public enum Direction implements StringRepresentable { + return this.faces.length; + } + } ++ ++ // Paper start - optimise collisions ++ static { ++ for (final Direction direction : VALUES) { ++ ((Direction)(Object)direction).opposite = from3DDataValue(((Direction)(Object)direction).oppositeIndex); ++ ((Direction)(Object)direction).rotation = ((Direction)(Object)direction).getRotationUncached(); ++ ((Direction)(Object)direction).id = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(direction.ordinal() + RANDOM_OFFSET) + RANDOM_OFFSET); ++ ((Direction)(Object)direction).stepX = ((Direction)(Object)direction).normal.getX(); ++ ((Direction)(Object)direction).stepY = ((Direction)(Object)direction).normal.getY(); ++ ((Direction)(Object)direction).stepZ = ((Direction)(Object)direction).normal.getZ(); ++ } ++ } ++ // Paper end - optimise collisions + } +diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java +index c33f85b570f159ab465b5a10a8044a81f2797f43..244a19ecd0234fa1d7a6ecfea20751595688605d 100644 +--- a/src/main/java/net/minecraft/server/Main.java ++++ b/src/main/java/net/minecraft/server/Main.java +@@ -320,6 +320,7 @@ public class Main { + + convertable_conversionsession.saveDataTag(iregistrycustom_dimension, savedata); + */ ++ Class.forName(net.minecraft.world.entity.npc.VillagerTrades.class.getName()); // Paper - load this sync so it won't fail later async + final DedicatedServer dedicatedserver = (DedicatedServer) MinecraftServer.spin((thread) -> { + DedicatedServer dedicatedserver1 = new DedicatedServer(optionset, worldLoader.get(), thread, convertable_conversionsession, resourcepackrepository, worldstem, dedicatedserversettings, DataFixers.getDataFixer(), services, LoggerChunkProgressListener::createFromGameruleRadius); + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index e14c0e1ccf526f81e28db5545d9e2351641e1bc8..91cc3eb6db2875710064f6b31413ccc84af56bb2 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -198,7 +198,7 @@ import org.bukkit.event.server.ServerLoadEvent; + + import co.aikar.timings.MinecraftTimings; // Paper + +-public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource, AutoCloseable { ++public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements ServerInfo, ChunkIOErrorReporter, CommandSource, AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer { // Paper - rewrite chunk system + + private static MinecraftServer SERVER; // Paper + public static final Logger LOGGER = LogUtils.getLogger(); +@@ -322,7 +322,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + + public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) { + AtomicReference<S> atomicreference = new AtomicReference(); +- Thread thread = new Thread(() -> { ++ Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system + ((MinecraftServer) atomicreference.get()).runServer(); + }, "Server thread"); + +@@ -341,6 +341,77 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + return s0; + } + ++ // Paper start - rewrite chunk system ++ private volatile Throwable chunkSystemCrash; ++ ++ @Override ++ public final void moonrise$setChunkSystemCrash(final Throwable throwable) { ++ this.chunkSystemCrash = throwable; ++ } ++ ++ private static final long CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME = 25L * 1000L; // 25us ++ private static final long MAX_CHUNK_EXEC_TIME = 1000L; // 1us ++ private static final long TASK_EXECUTION_FAILURE_BACKOFF = 5L * 1000L; // 5us ++ ++ private long lastMidTickExecute; ++ private long lastMidTickExecuteFailure; ++ ++ private boolean tickMidTickTasks() { ++ // give all worlds a fair chance at by targeting them all. ++ // if we execute too many tasks, that's fine - we have logic to correctly handle overuse of allocated time. ++ boolean executed = false; ++ for (final ServerLevel world : this.getAllLevels()) { ++ long currTime = System.nanoTime(); ++ if (currTime - ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getLastMidTickFailure() <= TASK_EXECUTION_FAILURE_BACKOFF) { ++ continue; ++ } ++ if (!world.getChunkSource().pollTask()) { ++ // we need to back off if this fails ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$setLastMidTickFailure(currTime); ++ } else { ++ executed = true; ++ } ++ } ++ ++ return executed; ++ } ++ ++ @Override ++ public final void moonrise$executeMidTickTasks() { ++ final long startTime = System.nanoTime(); ++ if ((startTime - this.lastMidTickExecute) <= CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME || (startTime - this.lastMidTickExecuteFailure) <= TASK_EXECUTION_FAILURE_BACKOFF) { ++ // it's shown to be bad to constantly hit the queue (chunk loads slow to a crawl), even if no tasks are executed. ++ // so, backoff to prevent this ++ return; ++ } ++ ++ for (;;) { ++ final boolean moreTasks = this.tickMidTickTasks(); ++ final long currTime = System.nanoTime(); ++ final long diff = currTime - startTime; ++ ++ if (!moreTasks || diff >= MAX_CHUNK_EXEC_TIME) { ++ if (!moreTasks) { ++ this.lastMidTickExecuteFailure = currTime; ++ } ++ ++ // note: negative values reduce the time ++ long overuse = diff - MAX_CHUNK_EXEC_TIME; ++ if (overuse >= (10L * 1000L * 1000L)) { // 10ms ++ // make sure something like a GC or dumb plugin doesn't screw us over... ++ overuse = 10L * 1000L * 1000L; // 10ms ++ } ++ ++ final double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME; ++ final long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME); ++ ++ this.lastMidTickExecute = currTime + extraSleep; ++ return; ++ } ++ } ++ } ++ // Paper end - rewrite chunk system ++ + public MinecraftServer(OptionSet options, WorldLoader.DataLoadContext worldLoader, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) { + super("Server"); + SERVER = this; // Paper - better singleton +@@ -657,7 +728,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + this.forceDifficulty(); + for (ServerLevel worldserver : this.getAllLevels()) { + this.prepareLevels(worldserver.getChunkSource().chunkMap.progressListener, worldserver); +- worldserver.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API ++ // Paper - rewrite chunk system + this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(worldserver.getWorld())); + } + +@@ -870,6 +941,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + public abstract boolean shouldRconBroadcast(); + + public boolean saveAllChunks(boolean suppressLogs, boolean flush, boolean force) { ++ // Paper start - add close param ++ return this.saveAllChunks(suppressLogs, flush, force, false); ++ } ++ public boolean saveAllChunks(boolean suppressLogs, boolean flush, boolean force, boolean close) { ++ // Paper end - add close param + boolean flag3 = false; + + for (Iterator iterator = this.getAllLevels().iterator(); iterator.hasNext(); flag3 = true) { +@@ -879,7 +955,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + MinecraftServer.LOGGER.info("Saving chunks for level '{}'/{}", worldserver, worldserver.dimension().location()); + } + +- worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force); ++ worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force, close); // Paper - add close param + } + + // CraftBukkit start - moved to WorldServer.save +@@ -980,7 +1056,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + } + } + +- while (this.levels.values().stream().anyMatch((worldserver1) -> { ++ while (false && this.levels.values().stream().anyMatch((worldserver1) -> { // Paper - rewrite chunk system + return worldserver1.getChunkSource().chunkMap.hasWork(); + })) { + this.nextTickTimeNanos = Util.getNanos() + TimeUtil.NANOSECONDS_PER_MILLISECOND; +@@ -997,19 +1073,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + this.waitUntilNextTick(); + } + +- this.saveAllChunks(false, true, false); +- iterator = this.getAllLevels().iterator(); +- +- while (iterator.hasNext()) { +- worldserver = (ServerLevel) iterator.next(); +- if (worldserver != null) { +- try { +- worldserver.close(); +- } catch (IOException ioexception) { +- MinecraftServer.LOGGER.error("Exception closing the level", ioexception); +- } +- } +- } ++ this.saveAllChunks(false, true, true, true); // Paper - rewrite chunk system + + this.isSaving = false; + this.resources.close(); +@@ -1029,6 +1093,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + } + // Spigot end + ++ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.deinit(); // Paper - rewrite chunk system + } + + public String getLocalIp() { +@@ -1203,6 +1268,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + this.tickServer(flag ? () -> { + return false; + } : this::haveTime); ++ // Paper start - rewrite chunk system ++ final Throwable crash = this.chunkSystemCrash; ++ if (crash != null) { ++ this.chunkSystemCrash = null; ++ throw new RuntimeException("Chunk system crash propagated to tick()", crash); ++ } ++ // Paper end - rewrite chunk system + this.profiler.popPush("nextTickWait"); + this.mayHaveDelayedTasks = true; + this.delayedTasksMaxNextTickTimeNanos = Math.max(Util.getNanos() + i, this.nextTickTimeNanos); +@@ -1392,6 +1464,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + + private boolean pollTaskInternal() { + if (super.pollTask()) { ++ this.moonrise$executeMidTickTasks(); // Paper - rewrite chunk system + return true; + } else { + boolean ret = false; // Paper - force execution of all worlds, do not just bias the first +@@ -1594,7 +1667,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + // Paper start - Folia scheduler API + ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) Bukkit.getGlobalRegionScheduler()).tick(); + getAllLevels().forEach(level -> { +- for (final Entity entity : level.getEntities().getAll()) { ++ for (final Entity entity : level.moonrise$getEntityLookup().getAllCopy()) { // Paper - rewrite chunk system + if (entity.isRemoved()) { + continue; + } +@@ -2656,6 +2729,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + + } + ++ // Paper start - rewrite chunk system ++ @Override ++ public boolean isSameThread() { ++ return io.papermc.paper.util.TickThread.isTickThread(); ++ } ++ // Paper end - rewrite chunk system ++ + // CraftBukkit start + public boolean isDebugging() { + return false; +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index 0761d5bc5f2813bb4a9f664ac7a05b9744d0a778..7d2896918ff5fed37e5de5a22c37b0c7f32634a8 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -476,7 +476,33 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + return world.dimension() == net.minecraft.world.level.Level.NETHER ? this.getProperties().allowNether : true; + } + ++ private static final java.util.concurrent.atomic.AtomicInteger ASYNC_DEBUG_CHUNKS_COUNT = new java.util.concurrent.atomic.AtomicInteger(); // Paper - rewrite chunk system ++ + public void handleConsoleInput(String command, CommandSourceStack commandSource) { ++ // Paper start - rewrite chunk system ++ if (command.equalsIgnoreCase("paper debug chunks --async")) { ++ LOGGER.info("Scheduling async debug chunks"); ++ Runnable run = () -> { ++ LOGGER.info("Async debug chunks executing"); ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(this, false); ++ CommandSender sender = MinecraftServer.getServer().console; ++ java.io.File file = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getChunkDebugFile(); ++ sender.sendMessage(net.kyori.adventure.text.Component.text("Writing chunk information dump to " + file, net.kyori.adventure.text.format.NamedTextColor.GREEN)); ++ try { ++ ca.spottedleaf.moonrise.common.util.JsonUtil.writeJson(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.debugAllWorlds(this), file); ++ sender.sendMessage(net.kyori.adventure.text.Component.text("Successfully written chunk information!", net.kyori.adventure.text.format.NamedTextColor.GREEN)); ++ } catch (Throwable thr) { ++ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); ++ sender.sendMessage(net.kyori.adventure.text.Component.text("Failed to dump chunk information, see console", net.kyori.adventure.text.format.NamedTextColor.RED)); ++ } ++ }; ++ Thread t = new Thread(run); ++ t.setName("Async debug thread #" + ASYNC_DEBUG_CHUNKS_COUNT.getAndIncrement()); ++ t.setDaemon(true); ++ t.start(); ++ return; ++ } ++ // Paper end - rewrite chunk system + this.serverCommandQueue.add(new ConsoleInput(command, commandSource)); // Paper - Perf: use proper queue + } + +diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java +index c643bb0daa5cd264fd6ebab7acf0a2bdd7fe7029..9bc59697fc71d4e3c226aa7fe958f57193fc4bd4 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java ++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java +@@ -32,28 +32,20 @@ import net.minecraft.world.level.lighting.LevelLightEngine; + import net.minecraft.server.MinecraftServer; + // CraftBukkit end + +-public class ChunkHolder extends GenerationChunkHolder { ++public class ChunkHolder extends GenerationChunkHolder implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder { // Paper - rewrite chunk system + + public static final ChunkResult<LevelChunk> UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk"); + private static final CompletableFuture<ChunkResult<LevelChunk>> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK); + private final LevelHeightAccessor levelHeightAccessor; +- private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage +- private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage +- private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage +- public int oldTicketLevel; +- private int ticketLevel; +- private int queueLevel; ++ // Paper - rewrite chunk system + private boolean hasChangedSections; + private final ShortSet[] changedBlocksPerSection; + private final BitSet blockChangedLightSectionFilter; + private final BitSet skyChangedLightSectionFilter; + private final LevelLightEngine lightEngine; +- private final ChunkHolder.LevelChangeListener onLevelChange; ++ // Paper - rewrite chunk system + public final ChunkHolder.PlayerProvider playerProvider; +- private boolean wasAccessibleSinceLastSave; +- private CompletableFuture<?> pendingFullStateConfirmation; +- private CompletableFuture<?> sendSync; +- private CompletableFuture<?> saveSync; ++ // Paper - rewrite chunk system + + private final ChunkMap chunkMap; // Paper + +@@ -67,23 +59,110 @@ public class ChunkHolder extends GenerationChunkHolder { + } + // Paper end + ++ // Paper start - rewrite chunk system ++ private ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder; ++ ++ private static final ServerPlayer[] EMPTY_PLAYER_ARRAY = new ServerPlayer[0]; ++ private final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> playersSentChunkTo = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_ARRAY, 0); ++ ++ private ChunkMap getChunkMap() { ++ return (ChunkMap)this.playerProvider; ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder moonrise$getRealChunkHolder() { ++ return this.newChunkHolder; ++ } ++ ++ @Override ++ public final void moonrise$setRealChunkHolder(final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder) { ++ this.newChunkHolder = newChunkHolder; ++ } ++ ++ @Override ++ public final void moonrise$addReceivedChunk(final ServerPlayer player) { ++ if (!this.playersSentChunkTo.add(player)) { ++ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player); ++ } ++ } ++ ++ @Override ++ public final void moonrise$removeReceivedChunk(final ServerPlayer player) { ++ if (!this.playersSentChunkTo.remove(player)) { ++ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.getChunkMap().level) + "' to player " + player); ++ } ++ } ++ ++ @Override ++ public final boolean moonrise$hasChunkBeenSent() { ++ return this.playersSentChunkTo.size() != 0; ++ } ++ ++ @Override ++ public final boolean moonrise$hasChunkBeenSent(final ServerPlayer to) { ++ return this.playersSentChunkTo.contains(to); ++ } ++ ++ @Override ++ public final List<ServerPlayer> moonrise$getPlayers(final boolean onlyOnWatchDistanceEdge) { ++ final List<ServerPlayer> ret = new java.util.ArrayList<>(); ++ final ServerPlayer[] raw = this.playersSentChunkTo.getRawDataUnchecked(); ++ for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) { ++ final ServerPlayer player = raw[i]; ++ if (onlyOnWatchDistanceEdge && !((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getPlayerChunkLoader().isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) { ++ continue; ++ } ++ ret.add(player); ++ } ++ ++ return ret; ++ } ++ ++ @Override ++ public final LevelChunk moonrise$getFullChunk() { ++ if (this.newChunkHolder.isFullChunkReady()) { ++ if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) { ++ return levelChunk; ++ } // else: race condition: chunk unload ++ } ++ return null; ++ } ++ ++ private boolean isRadiusLoaded(final int radius) { ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager manager = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.getChunkMap().level).moonrise$getChunkTaskScheduler() ++ .chunkHolderManager; ++ final ChunkPos pos = this.pos; ++ final int chunkX = pos.x; ++ final int chunkZ = pos.z; ++ for (int dz = -radius; dz <= radius; ++dz) { ++ for (int dx = -radius; dx <= radius; ++dx) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = manager.getChunkHolder(dx + chunkX, dz + chunkZ); ++ ++ if (holder == null || !holder.isFullChunkReady()) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ // Paper end - rewrite chunk system ++ + public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) { + super(pos); +- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; ++ // Paper - rewrite chunk system + this.blockChangedLightSectionFilter = new BitSet(); + this.skyChangedLightSectionFilter = new BitSet(); +- this.pendingFullStateConfirmation = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error +- this.sendSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error +- this.saveSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error ++ // Paper - rewrite chunk system + this.levelHeightAccessor = world; + this.lightEngine = lightingProvider; +- this.onLevelChange = levelUpdateListener; ++ // Paper - rewrite chunk system + this.playerProvider = playersWatchingChunkProvider; +- this.oldTicketLevel = ChunkLevel.MAX_LEVEL + 1; +- this.ticketLevel = this.oldTicketLevel; +- this.queueLevel = this.oldTicketLevel; ++ // Paper - rewrite chunk system + this.setTicketLevel(level); + this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()]; + this.chunkMap = (ChunkMap)playersWatchingChunkProvider; // Paper +@@ -91,21 +170,13 @@ public class ChunkHolder extends GenerationChunkHolder { + + // Paper start + public @Nullable ChunkAccess getAvailableChunkNow() { +- // TODO can we just getStatusFuture(EMPTY)? +- for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) { +- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(curr); +- if (chunkAccess == null) { +- continue; +- } +- return chunkAccess; +- } +- return null; ++ return this.getChunkIfPresent(ChunkStatus.EMPTY); // Paper - rewrite chunk system + } + // Paper end + // CraftBukkit start + public LevelChunk getFullChunkNow() { + // Note: We use the oldTicketLevel for isLoaded checks. +- if (!ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) return null; ++ if (!this.newChunkHolder.isFullChunkReady()) return null; // Paper - rewrite chunk system + return this.getFullChunkNowUnchecked(); + } + +@@ -115,39 +186,46 @@ public class ChunkHolder extends GenerationChunkHolder { + // CraftBukkit end + + public final CompletableFuture<ChunkResult<LevelChunk>> getTickingChunkFuture() { // Paper - final for inline +- return this.tickingChunkFuture; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public final CompletableFuture<ChunkResult<LevelChunk>> getEntityTickingChunkFuture() { // Paper - final for inline +- return this.entityTickingChunkFuture; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public final CompletableFuture<ChunkResult<LevelChunk>> getFullChunkFuture() { // Paper - final for inline +- return this.fullChunkFuture; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Nullable + public final LevelChunk getTickingChunk() { // Paper - final for inline +- return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error ++ // Paper start - rewrite chunk system ++ if (this.newChunkHolder.isTickingReady()) { ++ if (this.newChunkHolder.getCurrentChunk() instanceof LevelChunk levelChunk) { ++ return levelChunk; ++ } // else: race condition: chunk unload ++ } ++ return null; ++ // Paper end - rewrite chunk system + } + + @Nullable + public LevelChunk getChunkToSend() { +- return !this.sendSync.isDone() ? null : this.getTickingChunk(); ++ // Paper start - rewrite chunk system ++ final LevelChunk ret = this.moonrise$getFullChunk(); ++ if (ret != null && this.isRadiusLoaded(1)) { ++ return ret; ++ } ++ return null; ++ // Paper end - rewrite chunk system + } + + public CompletableFuture<?> getSendSyncFuture() { +- return this.sendSync; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public void addSendDependency(CompletableFuture<?> postProcessingFuture) { +- if (this.sendSync.isDone()) { +- this.sendSync = postProcessingFuture; +- } else { +- this.sendSync = this.sendSync.thenCombine(postProcessingFuture, (object, object1) -> { +- return null; +- }); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + + } + +@@ -166,26 +244,20 @@ public class ChunkHolder extends GenerationChunkHolder { + // Paper end + + public CompletableFuture<?> getSaveSyncFuture() { +- return this.saveSync; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public boolean isReadyForSaving() { +- return this.getGenerationRefCount() == 0 && this.saveSync.isDone(); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void addSaveDependency(CompletableFuture<?> savingFuture) { +- if (this.saveSync.isDone()) { +- this.saveSync = savingFuture; +- } else { +- this.saveSync = this.saveSync.thenCombine(savingFuture, (object, object1) -> { +- return null; +- }); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + + } + + public void blockChanged(BlockPos pos) { +- LevelChunk chunk = this.getTickingChunk(); ++ LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system + + if (chunk != null) { + int i = this.levelHeightAccessor.getSectionIndex(pos.getY()); +@@ -205,7 +277,7 @@ public class ChunkHolder extends GenerationChunkHolder { + + if (ichunkaccess != null) { + ichunkaccess.setUnsaved(true); +- LevelChunk chunk = this.getTickingChunk(); ++ LevelChunk chunk = this.getChunkToSend(); // Paper - rewrite chunk system + + if (chunk != null) { + int j = this.lightEngine.getMinLightSection(); +@@ -231,7 +303,7 @@ public class ChunkHolder extends GenerationChunkHolder { + List list; + + if (!this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) { +- list = this.playerProvider.getPlayers(this.pos, true); ++ list = this.moonrise$getPlayers(true); // Paper - rewrite chunk system + if (!list.isEmpty()) { + ClientboundLightUpdatePacket packetplayoutlightupdate = new ClientboundLightUpdatePacket(chunk.getPos(), this.lightEngine, this.skyChangedLightSectionFilter, this.blockChangedLightSectionFilter); + +@@ -243,7 +315,7 @@ public class ChunkHolder extends GenerationChunkHolder { + } + + if (this.hasChangedSections) { +- list = this.playerProvider.getPlayers(this.pos, false); ++ list = this.moonrise$getPlayers(false); // Paper - rewrite chunk system + + for (int i = 0; i < this.changedBlocksPerSection.length; ++i) { + ShortSet shortset = this.changedBlocksPerSection[i]; +@@ -309,193 +381,40 @@ public class ChunkHolder extends GenerationChunkHolder { + + @Override + public final int getTicketLevel() { // Paper - final for inline +- return this.ticketLevel; ++ return this.newChunkHolder.getTicketLevel(); // Paper - rewrite chunk system + } + + @Override + public int getQueueLevel() { +- return this.queueLevel; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void setQueueLevel(int level) { +- this.queueLevel = level; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public void setTicketLevel(int level) { +- this.ticketLevel = level; ++ // Paper - rewrite chunk system + } + + private void scheduleFullChunkPromotion(ChunkMap chunkLoadingManager, CompletableFuture<ChunkResult<LevelChunk>> chunkFuture, Executor executor, FullChunkStatus target) { +- this.pendingFullStateConfirmation.cancel(false); +- CompletableFuture<Void> completablefuture1 = new CompletableFuture(); +- +- completablefuture1.thenRunAsync(() -> { +- chunkLoadingManager.onFullChunkStatusChange(this.pos, target); +- }, executor); +- this.pendingFullStateConfirmation = completablefuture1; +- chunkFuture.thenAccept((chunkresult) -> { +- chunkresult.ifSuccess((chunk) -> { +- completablefuture1.complete(null); // CraftBukkit - decompile error +- }); +- }); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void demoteFullChunk(ChunkMap chunkLoadingManager, FullChunkStatus target) { +- this.pendingFullStateConfirmation.cancel(false); +- chunkLoadingManager.onFullChunkStatusChange(this.pos, target); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + protected void updateFutures(ChunkMap chunkLoadingManager, Executor executor) { +- FullChunkStatus fullchunkstatus = ChunkLevel.fullStatus(this.oldTicketLevel); +- FullChunkStatus fullchunkstatus1 = ChunkLevel.fullStatus(this.ticketLevel); +- boolean flag = fullchunkstatus.isOrAfter(FullChunkStatus.FULL); +- boolean flag1 = fullchunkstatus1.isOrAfter(FullChunkStatus.FULL); +- // CraftBukkit start +- // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins. +- if (flag && !flag1) { +- this.getFullChunkFuture().thenAccept((either) -> { +- LevelChunk chunk = (LevelChunk) either.orElse(null); +- if (chunk != null) { +- chunkLoadingManager.callbackExecutor.execute(() -> { +- // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick +- // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag. +- // These actions may however happen deferred, so we manually set the needsSaving flag already here. +- chunk.setUnsaved(true); +- chunk.unloadCallback(); +- }); +- } +- }).exceptionally((throwable) -> { +- // ensure exceptions are printed, by default this is not the case +- MinecraftServer.LOGGER.error("Failed to schedule unload callback for chunk " + ChunkHolder.this.pos, throwable); +- return null; +- }); +- +- // Run callback right away if the future was already done +- chunkLoadingManager.callbackExecutor.run(); +- } +- // CraftBukkit end +- +- this.wasAccessibleSinceLastSave |= flag1; +- if (!flag && flag1) { +- int expectCreateCount = ++this.fullChunkCreateCount; // Paper +- this.fullChunkFuture = chunkLoadingManager.prepareAccessibleChunk(this); +- this.scheduleFullChunkPromotion(chunkLoadingManager, this.fullChunkFuture, executor, FullChunkStatus.FULL); +- // Paper start - cache ticking ready status +- this.fullChunkFuture.thenAccept(chunkResult -> { +- chunkResult.ifSuccess(chunk -> { +- if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) { +- ChunkHolder.this.isFullChunkReady = true; +- io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(chunk, this); +- } +- }); +- }); +- // Paper end - cache ticking ready status +- this.addSaveDependency(this.fullChunkFuture); +- } +- +- if (flag && !flag1) { +- // Paper start +- if (this.isFullChunkReady) { +- io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper +- } +- // Paper end +- this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); +- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- } +- +- boolean flag2 = fullchunkstatus.isOrAfter(FullChunkStatus.BLOCK_TICKING); +- boolean flag3 = fullchunkstatus1.isOrAfter(FullChunkStatus.BLOCK_TICKING); +- +- if (!flag2 && flag3) { +- this.tickingChunkFuture = chunkLoadingManager.prepareTickingChunk(this); +- this.scheduleFullChunkPromotion(chunkLoadingManager, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING); +- // Paper start - cache ticking ready status +- this.tickingChunkFuture.thenAccept(chunkResult -> { +- chunkResult.ifSuccess(chunk -> { +- // note: Here is a very good place to add callbacks to logic waiting on this. +- ChunkHolder.this.isTickingReady = true; +- io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(chunk, this); +- }); +- }); +- // Paper end +- this.addSaveDependency(this.tickingChunkFuture); +- } +- +- if (flag2 && !flag3) { +- // Paper start +- if (this.isTickingReady) { +- io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper +- } +- // Paper end +- this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage +- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- } +- +- boolean flag4 = fullchunkstatus.isOrAfter(FullChunkStatus.ENTITY_TICKING); +- boolean flag5 = fullchunkstatus1.isOrAfter(FullChunkStatus.ENTITY_TICKING); +- +- if (!flag4 && flag5) { +- if (this.entityTickingChunkFuture != ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE) { +- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException()); +- } +- +- this.entityTickingChunkFuture = chunkLoadingManager.prepareEntityTickingChunk(this); +- this.scheduleFullChunkPromotion(chunkLoadingManager, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING); +- // Paper start - cache ticking ready status +- this.entityTickingChunkFuture.thenAccept(chunkResult -> { +- chunkResult.ifSuccess(chunk -> { +- ChunkHolder.this.isEntityTickingReady = true; +- io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(chunk, this); +- }); +- }); +- // Paper end +- this.addSaveDependency(this.entityTickingChunkFuture); +- } +- +- if (flag4 && !flag5) { +- // Paper start +- if (this.isEntityTickingReady) { +- io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); +- } +- // Paper end +- this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage +- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- } +- +- if (!fullchunkstatus1.isOrAfter(fullchunkstatus)) { +- this.demoteFullChunk(chunkLoadingManager, fullchunkstatus1); +- } +- +- this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel); +- this.oldTicketLevel = this.ticketLevel; +- // CraftBukkit start +- // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins. +- if (!fullchunkstatus.isOrAfter(FullChunkStatus.FULL) && fullchunkstatus1.isOrAfter(FullChunkStatus.FULL)) { +- this.getFullChunkFuture().thenAccept((either) -> { +- LevelChunk chunk = (LevelChunk) either.orElse(null); +- if (chunk != null) { +- chunkLoadingManager.callbackExecutor.execute(() -> { +- chunk.loadCallback(); +- }); +- } +- }).exceptionally((throwable) -> { +- // ensure exceptions are printed, by default this is not the case +- MinecraftServer.LOGGER.error("Failed to schedule load callback for chunk " + ChunkHolder.this.pos, throwable); +- return null; +- }); +- +- // Run callback right away if the future was already done +- chunkLoadingManager.callbackExecutor.run(); +- } +- // CraftBukkit end ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public boolean wasAccessibleSinceLastSave() { +- return this.wasAccessibleSinceLastSave; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public void refreshAccessibility() { +- this.wasAccessibleSinceLastSave = ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @FunctionalInterface +@@ -511,15 +430,15 @@ public class ChunkHolder extends GenerationChunkHolder { + + // Paper start + public final boolean isEntityTickingReady() { +- return this.isEntityTickingReady; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public final boolean isTickingReady() { +- return this.isTickingReady; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public final boolean isFullChunkReady() { +- return this.isFullChunkReady; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + // Paper end + } +diff --git a/src/main/java/net/minecraft/server/level/ChunkLevel.java b/src/main/java/net/minecraft/server/level/ChunkLevel.java +index d9ad32acdf46a43a649334a3b736aeb7b3af21d1..fae17a075d7efaf24d916877dd5968eb9652bb66 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkLevel.java ++++ b/src/main/java/net/minecraft/server/level/ChunkLevel.java +@@ -7,9 +7,9 @@ import net.minecraft.world.level.chunk.status.ChunkStep; + import org.jetbrains.annotations.Contract; + + public class ChunkLevel { +- private static final int FULL_CHUNK_LEVEL = 33; +- private static final int BLOCK_TICKING_LEVEL = 32; +- private static final int ENTITY_TICKING_LEVEL = 31; ++ public static final int FULL_CHUNK_LEVEL = 33; ++ public static final int BLOCK_TICKING_LEVEL = 32; ++ public static final int ENTITY_TICKING_LEVEL = 31; + private static final ChunkStep FULL_CHUNK_STEP = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL); + public static final int RADIUS_AROUND_FULL_CHUNK = FULL_CHUNK_STEP.accumulatedDependencies().getRadius(); + public static final int MAX_LEVEL = 33 + RADIUS_AROUND_FULL_CHUNK; +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index a03385b1b0a2f9b98319137b87d917856d3c632c..d843bc04ae93d11d7820cab5ed18617193568f0d 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -122,10 +122,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + public static final int MIN_VIEW_DISTANCE = 2; + public static final int MAX_VIEW_DISTANCE = 32; + public static final int FORCED_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING); +- public final Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap = new Long2ObjectLinkedOpenHashMap(); +- public volatile Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunkMap; +- private final Long2ObjectLinkedOpenHashMap<ChunkHolder> pendingUnloads; +- private final List<ChunkGenerationTask> pendingGenerationTasks; ++ // Paper - rewrite chunk system + public final ServerLevel level; + private final ThreadedLevelLightEngine lightEngine; + private final BlockableEventLoop<Runnable> mainThreadExecutor; +@@ -135,21 +132,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + private final PoiManager poiManager; + public final LongSet toDrop; + private boolean modified; +- private final ChunkTaskPriorityQueueSorter queueSorter; +- private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> worldgenMailbox; +- private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> mainThreadMailbox; ++ // Paper - rewrite chunk system + public final ChunkProgressListener progressListener; + private final ChunkStatusUpdateListener chunkStatusListener; + public final ChunkMap.ChunkDistanceManager distanceManager; +- private final AtomicInteger tickingGenerated; ++ public final AtomicInteger tickingGenerated; // Paper - public + private final String storageName; + private final PlayerMap playerMap; + public final Int2ObjectMap<ChunkMap.TrackedEntity> entityMap; + private final Long2ByteMap chunkTypeCache; + private final Long2LongMap chunkSaveCooldowns; +- private final Queue<Runnable> unloadQueue; ++ // Paper - rewrite chunk system + public int serverViewDistance; +- private final WorldGenContext worldGenContext; ++ public final WorldGenContext worldGenContext; // Paper - public + + // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback() + public final CallbackExecutor callbackExecutor = new CallbackExecutor(); +@@ -198,23 +193,21 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper end + // Paper start + public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) { +- return this.pendingUnloads.get(io.papermc.paper.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ return null; // Paper - rewrite chunk system + } + public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers; + // Paper end + + public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) { + super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync); +- this.visibleChunkMap = this.updatingChunkMap.clone(); +- this.pendingUnloads = new Long2ObjectLinkedOpenHashMap(); +- this.pendingGenerationTasks = new ArrayList(); ++ // Paper - rewrite chunk system + this.toDrop = new LongOpenHashSet(); + this.tickingGenerated = new AtomicInteger(); + this.playerMap = new PlayerMap(); + this.entityMap = new Int2ObjectOpenHashMap(); + this.chunkTypeCache = new Long2ByteOpenHashMap(); + this.chunkSaveCooldowns = new Long2LongOpenHashMap(); +- this.unloadQueue = Queues.newConcurrentLinkedQueue(); ++ // Paper - rewrite chunk system + Path path = session.getDimensionPath(world.dimension()); + + this.storageName = path.getFileName().toString(); +@@ -245,15 +238,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.chunkStatusListener = chunkStatusChangeListener; + ProcessorMailbox<Runnable> threadedmailbox1 = ProcessorMailbox.create(executor, "light"); + +- this.queueSorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(threadedmailbox, mailbox, threadedmailbox1), executor, Integer.MAX_VALUE); +- this.worldgenMailbox = this.queueSorter.getProcessor(threadedmailbox, false); +- this.mainThreadMailbox = this.queueSorter.getProcessor(mailbox, false); +- this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), threadedmailbox1, this.queueSorter.getProcessor(threadedmailbox1, false)); ++ // Paper - rewrite chunk system ++ this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), threadedmailbox1, null); // Paper - rewrite chunk system + this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor); + this.overworldDataStorage = persistentStateManagerFactory; + this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world.getServer(), world); + this.setServerViewDistance(viewDistance); +- this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, this.mainThreadMailbox); ++ this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, null); // Paper - rewrite chunk system + // Paper start + this.nearbyPlayers = new io.papermc.paper.util.player.NearbyPlayers(this.level); + // Paper end +@@ -292,23 +283,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + boolean isChunkTracked(ServerPlayer player, int chunkX, int chunkZ) { +- return player.getChunkTrackingView().contains(chunkX, chunkZ) && !player.connection.chunkSender.isPending(ChunkPos.asLong(chunkX, chunkZ)); ++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ); // Paper - rewrite chunk system + } + + private boolean isChunkOnTrackedBorder(ServerPlayer player, int chunkX, int chunkZ) { +- if (!this.isChunkTracked(player, chunkX, chunkZ)) { +- return false; +- } else { +- for (int k = -1; k <= 1; ++k) { +- for (int l = -1; l <= 1; ++l) { +- if ((k != 0 || l != 0) && !this.isChunkTracked(player, chunkX + k, chunkZ + l)) { +- return true; +- } +- } +- } +- +- return false; +- } ++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().isChunkSent(player, chunkX, chunkZ, true); // Paper - rewrite chunk system + } + + protected ThreadedLevelLightEngine getLightEngine() { +@@ -317,20 +296,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + @Nullable + protected ChunkHolder getUpdatingChunkIfPresent(long pos) { +- return (ChunkHolder) this.updatingChunkMap.get(pos); ++ // Paper start - rewrite chunk system ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos); ++ return holder == null ? null : holder.vanillaChunkHolder; ++ // Paper end - rewrite chunk system + } + + @Nullable + public ChunkHolder getVisibleChunkIfPresent(long pos) { +- return (ChunkHolder) this.visibleChunkMap.get(pos); ++ // Paper start - rewrite chunk system ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos); ++ return holder == null ? null : holder.vanillaChunkHolder; ++ // Paper end - rewrite chunk system + } + + protected IntSupplier getChunkQueueLevel(long pos) { +- return () -> { +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); +- +- return playerchunk == null ? ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1 : Math.min(playerchunk.getQueueLevel(), ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1); +- }; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public String getChunkDebugData(ChunkPos chunkPos) { +@@ -359,55 +340,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + private CompletableFuture<ChunkResult<List<ChunkAccess>>> getChunkRangeFuture(ChunkHolder centerChunk, int margin, IntFunction<ChunkStatus> distanceToStatus) { +- if (margin == 0) { +- ChunkStatus chunkstatus = (ChunkStatus) distanceToStatus.apply(0); +- +- return centerChunk.scheduleChunkGenerationTask(chunkstatus, this).thenApply((chunkresult) -> { +- return chunkresult.map(List::of); +- }); +- } else { +- List<CompletableFuture<ChunkResult<ChunkAccess>>> list = new ArrayList(); +- ChunkPos chunkcoordintpair = centerChunk.getPos(); +- +- for (int j = -margin; j <= margin; ++j) { +- for (int k = -margin; k <= margin; ++k) { +- int l = Math.max(Math.abs(k), Math.abs(j)); +- long i1 = ChunkPos.asLong(chunkcoordintpair.x + k, chunkcoordintpair.z + j); +- ChunkHolder playerchunk1 = this.getUpdatingChunkIfPresent(i1); +- +- if (playerchunk1 == null) { +- return ChunkMap.UNLOADED_CHUNK_LIST_FUTURE; +- } +- +- ChunkStatus chunkstatus1 = (ChunkStatus) distanceToStatus.apply(l); +- +- list.add(playerchunk1.scheduleChunkGenerationTask(chunkstatus1, this)); +- } +- } +- +- return Util.sequence(list).thenApply((list1) -> { +- List<ChunkAccess> list2 = Lists.newArrayList(); +- Iterator iterator = list1.iterator(); +- +- while (iterator.hasNext()) { +- ChunkResult<ChunkAccess> chunkresult = (ChunkResult) iterator.next(); +- +- if (chunkresult == null) { +- throw this.debugFuturesAndCreateReportedException(new IllegalStateException("At least one of the chunk futures were null"), "n/a"); +- } +- +- ChunkAccess ichunkaccess = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error +- +- if (ichunkaccess == null) { +- return ChunkMap.UNLOADED_CHUNK_LIST_RESULT; +- } +- +- list2.add(ichunkaccess); +- } +- +- return ChunkResult.of(list2); +- }); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public ReportedException debugFuturesAndCreateReportedException(IllegalStateException exception, String details) { +@@ -437,93 +370,23 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public CompletableFuture<ChunkResult<LevelChunk>> prepareEntityTickingChunk(ChunkHolder holder) { +- return this.getChunkRangeFuture(holder, 2, (i) -> { +- return ChunkStatus.FULL; +- }).thenApplyAsync((chunkresult) -> { +- return chunkresult.map((list) -> { +- return (LevelChunk) list.get(list.size() / 2); +- }); +- }, this.mainThreadExecutor); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Nullable + ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) { +- if (!ChunkLevel.isLoaded(k) && !ChunkLevel.isLoaded(level)) { +- return holder; +- } else { +- if (holder != null) { +- holder.setTicketLevel(level); +- } +- +- if (holder != null) { +- if (!ChunkLevel.isLoaded(level)) { +- this.toDrop.add(pos); +- } else { +- this.toDrop.remove(pos); +- } +- } +- +- if (ChunkLevel.isLoaded(level) && holder == null) { +- holder = (ChunkHolder) this.pendingUnloads.remove(pos); +- if (holder != null) { +- holder.setTicketLevel(level); +- } else { +- holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this); +- // Paper start +- io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderCreate(this.level, holder); +- // Paper end +- } +- +- // Paper start +- holder.onChunkAdd(); +- // Paper end +- this.updatingChunkMap.put(pos, holder); +- this.modified = true; +- } +- +- return holder; +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Override + public void close() throws IOException { +- try { +- this.queueSorter.close(); +- this.poiManager.close(); +- } finally { +- super.close(); +- } +- ++ throw new UnsupportedOperationException("Use ServerChunkCache#close"); // Paper - rewrite chunk system + } + + protected void saveAllChunks(boolean flush) { +- if (flush) { +- List<ChunkHolder> list = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper +- MutableBoolean mutableboolean = new MutableBoolean(); +- +- do { +- mutableboolean.setFalse(); +- list.stream().map((playerchunk) -> { +- BlockableEventLoop iasynctaskhandler = this.mainThreadExecutor; +- +- Objects.requireNonNull(playerchunk); +- iasynctaskhandler.managedBlock(playerchunk::isReadyForSaving); +- return playerchunk.getLatestChunk(); +- }).filter((ichunkaccess) -> { +- return ichunkaccess instanceof ImposterProtoChunk || ichunkaccess instanceof LevelChunk; +- }).filter(this::save).forEach((ichunkaccess) -> { +- mutableboolean.setTrue(); +- }); +- } while (mutableboolean.isTrue()); +- +- this.processUnloads(() -> { +- return true; +- }); +- this.flushWorker(); +- } else { +- io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded); +- } +- ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.saveAllChunks( ++ flush, false, false ++ ); + } + + protected void tick(BooleanSupplier shouldKeepTicking) { +@@ -540,134 +403,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public boolean hasWork() { +- return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || io.papermc.paper.chunk.system.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void processUnloads(BooleanSupplier shouldKeepTicking) { +- LongIterator longiterator = this.toDrop.iterator(); +- int i = 0; +- +- while (longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000)) { +- long j = longiterator.nextLong(); +- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(j); +- +- if (playerchunk != null) { +- if (playerchunk.getGenerationRefCount() != 0) { +- continue; +- } +- +- this.updatingChunkMap.remove(j); +- playerchunk.onChunkRemove(); // Paper +- this.pendingUnloads.put(j, playerchunk); +- this.modified = true; +- ++i; +- this.scheduleUnload(j, playerchunk); +- } +- +- longiterator.remove(); +- } +- +- int k = Math.max(0, this.unloadQueue.size() - 2000); +- +- Runnable runnable; +- +- while ((shouldKeepTicking.getAsBoolean() || k > 0) && (runnable = (Runnable) this.unloadQueue.poll()) != null) { +- --k; +- runnable.run(); +- } +- +- int l = 0; +- Iterator<ChunkHolder> objectiterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper +- +- while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) { +- if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) { +- ++l; +- } +- } ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processUnloads(); // Paper - rewrite chunk system ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.autoSave(); // Paper - rewrite chunk system + + } + + private void scheduleUnload(long pos, ChunkHolder holder) { +- CompletableFuture completablefuture = holder.getSaveSyncFuture(); +- Runnable runnable = () -> { +- if (!holder.isReadyForSaving()) { +- this.scheduleUnload(pos, holder); +- } else { +- ChunkAccess ichunkaccess = holder.getLatestChunk(); +- +- // Paper start +- boolean removed; +- if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) { +- io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder); +- // Paper end +- LevelChunk chunk; +- +- if (ichunkaccess instanceof LevelChunk) { +- chunk = (LevelChunk) ichunkaccess; +- chunk.setLoaded(false); +- } +- +- this.save(ichunkaccess); +- if (ichunkaccess instanceof LevelChunk) { +- chunk = (LevelChunk) ichunkaccess; +- this.level.unload(chunk); +- } +- +- this.lightEngine.updateChunkStatus(ichunkaccess.getPos()); +- this.lightEngine.tryScheduleUpdate(); +- this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null); +- this.chunkSaveCooldowns.remove(ichunkaccess.getPos().toLong()); +- } else if (removed) { // Paper start +- io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder); +- } // Paper end +- +- } +- }; +- Queue queue = this.unloadQueue; +- +- Objects.requireNonNull(this.unloadQueue); +- completablefuture.thenRunAsync(runnable, queue::add).whenComplete((ovoid, throwable) -> { +- if (throwable != null) { +- ChunkMap.LOGGER.error("Failed to save chunk {}", holder.getPos(), throwable); +- } +- +- }); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + protected boolean promoteChunkMap() { +- if (!this.modified) { +- return false; +- } else { +- this.visibleChunkMap = this.updatingChunkMap.clone(); +- this.modified = false; +- return true; +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private CompletableFuture<ChunkAccess> scheduleChunkLoad(ChunkPos pos) { +- return this.readChunk(pos).thenApply((optional) -> { +- return optional.filter((nbttagcompound) -> { +- boolean flag = ChunkMap.isChunkDataValid(nbttagcompound); +- +- if (!flag) { +- ChunkMap.LOGGER.error("Chunk file at {} is missing level data, skipping", pos); +- } +- +- return flag; +- }); +- }).thenApplyAsync((optional) -> { +- this.level.getProfiler().incrementCounter("chunkLoad"); +- if (optional.isPresent()) { +- ProtoChunk protochunk = ChunkSerializer.read(this.level, this.poiManager, this.storageInfo(), pos, (CompoundTag) optional.get()); +- +- this.markPosition(pos, protochunk.getPersistedStatus().getChunkType()); +- return protochunk; +- } else { +- return this.createEmptyChunk(pos); +- } +- }, this.mainThreadExecutor).exceptionallyAsync((throwable) -> { +- return this.handleChunkLoadFailure(throwable, pos); +- }, this.mainThreadExecutor); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private static boolean isChunkDataValid(CompoundTag nbt) { +@@ -727,137 +481,44 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + @Override + public GenerationChunkHolder acquireGeneration(long pos) { +- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.get(pos); +- +- playerchunk.increaseGenerationRefCount(); +- return playerchunk; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Override + public void releaseGeneration(GenerationChunkHolder chunkHolder) { +- chunkHolder.decreaseGenerationRefCount(); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Override + public CompletableFuture<ChunkAccess> applyStep(GenerationChunkHolder chunkHolder, ChunkStep step, StaticCache2D<GenerationChunkHolder> chunks) { +- ChunkPos chunkcoordintpair = chunkHolder.getPos(); +- +- if (step.targetStatus() == ChunkStatus.EMPTY) { +- return this.scheduleChunkLoad(chunkcoordintpair); +- } else { +- try { +- GenerationChunkHolder generationchunkholder1 = (GenerationChunkHolder) chunks.get(chunkcoordintpair.x, chunkcoordintpair.z); +- ChunkAccess ichunkaccess = generationchunkholder1.getChunkIfPresentUnchecked(step.targetStatus().getParent()); +- +- if (ichunkaccess == null) { +- throw new IllegalStateException("Parent chunk missing"); +- } else { +- CompletableFuture<ChunkAccess> completablefuture = step.apply(this.worldGenContext, chunks, ichunkaccess); +- +- this.progressListener.onStatusChange(chunkcoordintpair, step.targetStatus()); +- return completablefuture; +- } +- } catch (Exception exception) { +- exception.getStackTrace(); +- CrashReport crashreport = CrashReport.forThrowable(exception, "Exception generating new chunk"); +- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk to be generated"); +- +- crashreportsystemdetails.setDetail("Status being generated", () -> { +- return step.targetStatus().getName(); +- }); +- crashreportsystemdetails.setDetail("Location", (Object) String.format(Locale.ROOT, "%d,%d", chunkcoordintpair.x, chunkcoordintpair.z)); +- crashreportsystemdetails.setDetail("Position hash", (Object) ChunkPos.asLong(chunkcoordintpair.x, chunkcoordintpair.z)); +- crashreportsystemdetails.setDetail("Generator", (Object) this.generator()); +- this.mainThreadExecutor.execute(() -> { +- throw new ReportedException(crashreport); +- }); +- throw new ReportedException(crashreport); +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Override + public ChunkGenerationTask scheduleGenerationTask(ChunkStatus requestedStatus, ChunkPos pos) { +- ChunkGenerationTask chunkgenerationtask = ChunkGenerationTask.create(this, requestedStatus, pos); +- +- this.pendingGenerationTasks.add(chunkgenerationtask); +- return chunkgenerationtask; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void runGenerationTask(ChunkGenerationTask chunkLoader) { +- this.worldgenMailbox.tell(ChunkTaskPriorityQueueSorter.message(chunkLoader.getCenter(), () -> { +- CompletableFuture<?> completablefuture = chunkLoader.runUntilWait(); +- +- if (completablefuture != null) { +- completablefuture.thenRun(() -> { +- this.runGenerationTask(chunkLoader); +- }); +- } +- })); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Override + public void runGenerationTasks() { +- this.pendingGenerationTasks.forEach(this::runGenerationTask); +- this.pendingGenerationTasks.clear(); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public CompletableFuture<ChunkResult<LevelChunk>> prepareTickingChunk(ChunkHolder holder) { +- CompletableFuture<ChunkResult<List<ChunkAccess>>> completablefuture = this.getChunkRangeFuture(holder, 1, (i) -> { +- return ChunkStatus.FULL; +- }); +- CompletableFuture<ChunkResult<LevelChunk>> completablefuture1 = completablefuture.thenApplyAsync((chunkresult) -> { +- return chunkresult.map((list) -> { +- return (LevelChunk) list.get(list.size() / 2); +- }); +- }, (runnable) -> { +- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable)); +- }).thenApplyAsync((chunkresult) -> { +- return chunkresult.ifSuccess((chunk) -> { +- chunk.postProcessGeneration(); +- this.level.startTickingChunk(chunk); +- CompletableFuture<?> completablefuture2 = holder.getSendSyncFuture(); +- +- if (completablefuture2.isDone()) { +- this.onChunkReadyToSend(chunk); +- } else { +- completablefuture2.thenAcceptAsync((object) -> { +- this.onChunkReadyToSend(chunk); +- }, this.mainThreadExecutor); +- } +- +- }); +- }, this.mainThreadExecutor); +- +- completablefuture1.handle((chunkresult, throwable) -> { +- this.tickingGenerated.getAndIncrement(); +- return null; +- }); +- return completablefuture1; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void onChunkReadyToSend(LevelChunk chunk) { +- ChunkPos chunkcoordintpair = chunk.getPos(); +- Iterator iterator = this.playerMap.getAllPlayers().iterator(); +- +- while (iterator.hasNext()) { +- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); +- +- if (entityplayer.getChunkTrackingView().contains(chunkcoordintpair)) { +- ChunkMap.markChunkPendingToSend(entityplayer, chunk); +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + + } + + public CompletableFuture<ChunkResult<LevelChunk>> prepareAccessibleChunk(ChunkHolder holder) { +- return this.getChunkRangeFuture(holder, 1, ChunkLevel::getStatusAroundFullChunk).thenApplyAsync((chunkresult) -> { +- return chunkresult.map((list) -> { +- return (LevelChunk) list.get(list.size() / 2); +- }); +- }, (runnable) -> { +- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable)); +- }); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public int getTickingGenerated() { +@@ -865,135 +526,84 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + private boolean saveChunkIfNeeded(ChunkHolder chunkHolder) { +- if (chunkHolder.wasAccessibleSinceLastSave() && chunkHolder.isReadyForSaving()) { +- ChunkAccess ichunkaccess = chunkHolder.getLatestChunk(); +- +- if (!(ichunkaccess instanceof ImposterProtoChunk) && !(ichunkaccess instanceof LevelChunk)) { +- return false; +- } else { +- long i = ichunkaccess.getPos().toLong(); +- long j = this.chunkSaveCooldowns.getOrDefault(i, -1L); +- long k = System.currentTimeMillis(); +- +- if (k < j) { +- return false; +- } else { +- boolean flag = this.save(ichunkaccess); +- +- chunkHolder.refreshAccessibility(); +- if (flag) { +- this.chunkSaveCooldowns.put(i, k + 10000L); +- } +- +- return flag; +- } +- } +- } else { +- return false; +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public boolean save(ChunkAccess chunk) { +- this.poiManager.flush(chunk.getPos()); +- if (!chunk.isUnsaved()) { +- return false; +- } else { +- chunk.setUnsaved(false); +- ChunkPos chunkcoordintpair = chunk.getPos(); +- +- try { +- ChunkStatus chunkstatus = chunk.getPersistedStatus(); +- +- if (chunkstatus.getChunkType() != ChunkType.LEVELCHUNK) { +- if (this.isExistingChunkFull(chunkcoordintpair)) { +- return false; +- } +- +- if (chunkstatus == ChunkStatus.EMPTY && chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) { +- return false; +- } +- } +- +- this.level.getProfiler().incrementCounter("chunkSave"); +- CompoundTag nbttagcompound = ChunkSerializer.write(this.level, chunk); +- +- this.write(chunkcoordintpair, nbttagcompound).exceptionally((throwable) -> { +- this.level.getServer().reportChunkSaveFailure(throwable, this.storageInfo(), chunkcoordintpair); +- return null; +- }); +- this.markPosition(chunkcoordintpair, chunkstatus.getChunkType()); +- return true; +- } catch (Exception exception) { +- this.level.getServer().reportChunkSaveFailure(exception, this.storageInfo(), chunkcoordintpair); +- return false; +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private boolean isExistingChunkFull(ChunkPos pos) { +- byte b0 = this.chunkTypeCache.get(pos.toLong()); +- +- if (b0 != 0) { +- return b0 == 1; +- } else { +- CompoundTag nbttagcompound; +- +- try { +- nbttagcompound = (CompoundTag) ((Optional) this.readChunk(pos).join()).orElse((Object) null); +- if (nbttagcompound == null) { +- this.markPositionReplaceable(pos); +- return false; +- } +- } catch (Exception exception) { +- ChunkMap.LOGGER.error("Failed to read chunk {}", pos, exception); +- this.markPositionReplaceable(pos); +- return false; +- } +- +- ChunkType chunktype = ChunkSerializer.getChunkTypeFromTag(nbttagcompound); +- +- return this.markPosition(pos, chunktype) == 1; +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public void setServerViewDistance(int watchDistance) { // Paper - public +- int j = Mth.clamp(watchDistance, 2, 32); +- +- if (j != this.serverViewDistance) { +- this.serverViewDistance = j; +- this.distanceManager.updatePlayerTickets(this.serverViewDistance); +- Iterator iterator = this.playerMap.getAllPlayers().iterator(); +- +- while (iterator.hasNext()) { +- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); +- +- this.updateChunkTracking(entityplayer); +- } ++ // Paper start - rewrite chunk system ++ final int clamped = Mth.clamp(watchDistance, 2, ca.spottedleaf.moonrise.common.util.MoonriseConstants.MAX_VIEW_DISTANCE); ++ if (clamped == this.serverViewDistance) { ++ return; + } + ++ this.serverViewDistance = clamped; ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().setLoadDistance(this.serverViewDistance + 1); ++ // Paper end - rewrite chunk system + } + + public int getPlayerViewDistance(ServerPlayer player) { // Paper - public +- return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance); ++ return ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.getSendViewDistance(player); // Paper - rewrite chunk system + } + + private void markChunkPendingToSend(ServerPlayer player, ChunkPos pos) { +- LevelChunk chunk = this.getChunkToSend(pos.toLong()); +- +- if (chunk != null) { +- ChunkMap.markChunkPendingToSend(player, chunk); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + + } + + private static void markChunkPendingToSend(ServerPlayer player, LevelChunk chunk) { +- player.connection.chunkSender.markChunkPendingToSend(chunk); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private static void dropChunk(ServerPlayer player, ChunkPos pos) { +- player.connection.chunkSender.dropChunk(player, pos); ++ // Paper - rewrite chunk system + } + ++ // Paper start - rewrite chunk system ++ @Override ++ public CompletableFuture<Optional<CompoundTag>> read(final ChunkPos pos) { ++ if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) { ++ try { ++ return CompletableFuture.completedFuture( ++ Optional.ofNullable( ++ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.loadData( ++ this.level, pos.x, pos.z, ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA, ++ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread() ++ ) ++ ) ++ ); ++ } catch (final Throwable thr) { ++ return CompletableFuture.failedFuture(thr); ++ } ++ } ++ return super.read(pos); ++ } ++ ++ @Override ++ public CompletableFuture<Void> write(final ChunkPos pos, final CompoundTag tag) { ++ if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) { ++ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.scheduleSave( ++ this.level, pos.x, pos.z, tag, ++ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA); ++ return null; ++ } ++ super.write(pos, tag); ++ return null; ++ } ++ ++ @Override ++ public void flushWorker() { ++ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.flush(); ++ } ++ // Paper end - rewrite chunk system ++ + @Nullable + public LevelChunk getChunkToSend(long pos) { + ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); +@@ -1059,7 +669,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + // CraftBukkit start +- private CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) { ++ public CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) { // Paper - public + return this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, this.generator().getTypeNameForDataFixer(), chunkcoordintpair, this.level); + // CraftBukkit end + } +@@ -1153,7 +763,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + player.setChunkTrackingView(ChunkTrackingView.EMPTY); +- this.updateChunkTracking(player); ++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system + this.addPlayerToDistanceMaps(player); // Paper - distance maps + } else { + SectionPos sectionposition = player.getLastSectionPos(); +@@ -1164,7 +774,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + this.removePlayerFromDistanceMaps(player); // Paper - distance maps +- this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY); ++ ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem.removePlayerFromDistanceMaps(this.level, player); // Paper - rewrite chunk system + } + + } +@@ -1176,17 +786,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public void move(ServerPlayer player) { +- ObjectIterator objectiterator = this.entityMap.values().iterator(); +- +- while (objectiterator.hasNext()) { +- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); +- +- if (playerchunkmap_entitytracker.entity == player) { +- playerchunkmap_entitytracker.updatePlayers(this.level.players()); +- } else { +- playerchunkmap_entitytracker.updatePlayer(player); +- } +- } ++ // Paper - optimise entity tracker + + SectionPos sectionposition = player.getLastSectionPos(); + SectionPos sectionposition1 = SectionPos.of((EntityAccess) player); +@@ -1212,71 +812,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) { +@@ -1304,6 +864,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + entity.tracker = playerchunkmap_entitytracker; // Paper - Fast access to tracker + this.entityMap.put(entity.getId(), playerchunkmap_entitytracker); ++ // Paper start - optimise entity tracker ++ if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity() != null) { ++ throw new IllegalStateException("Entity is already tracked"); ++ } ++ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(playerchunkmap_entitytracker); ++ // Paper end - optimise entity tracker + playerchunkmap_entitytracker.updatePlayers(this.level.players()); + if (entity instanceof ServerPlayer) { + ServerPlayer entityplayer = (ServerPlayer) entity; +@@ -1344,16 +910,49 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + playerchunkmap_entitytracker1.broadcastRemoved(); + } + entity.tracker = null; // Paper - We're no longer tracked ++ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(null); // Paper - optimise entity tracker + } + +- protected void tick() { +- Iterator iterator = this.playerMap.getAllPlayers().iterator(); ++ // Paper start - optimise entity tracker ++ private void newTrackerTick() { ++ final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers(); ++ final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup entityLookup = (ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup)((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getEntityLookup();; + +- while (iterator.hasNext()) { +- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); ++ final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.world.entity.Entity> trackerEntities = entityLookup.trackerEntities; ++ final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked(); ++ for (int i = 0, len = trackerEntities.size(); i < len; ++i) { ++ final Entity entity = trackerEntitiesRaw[i]; ++ final ChunkMap.TrackedEntity tracker = ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity(); ++ if (tracker == null) { ++ continue; ++ } ++ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition())); ++ tracker.serverEntity.sendChanges(); ++ } ++ ++ // process unloads ++ final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.world.entity.Entity> unloadedEntities = entityLookup.trackerUnloadedEntities; ++ final Entity[] unloadedEntitiesRaw = java.util.Arrays.copyOf(unloadedEntities.getRawDataUnchecked(), unloadedEntities.size()); ++ unloadedEntities.clear(); + +- this.updateChunkTracking(entityplayer); ++ for (final Entity entity : unloadedEntitiesRaw) { ++ final ChunkMap.TrackedEntity tracker = ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity(); ++ if (tracker == null) { ++ continue; ++ } ++ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$clearPlayers(); ++ } ++ } ++ // Paper end - optimise entity tracker ++ ++ protected void tick() { ++ // Paper start - optimise entity tracker ++ if (true) { ++ this.newTrackerTick(); ++ return; + } ++ // Paper end - optimise entity tracker ++ // Paper - rewrite chunk system + + List<ServerPlayer> list = Lists.newArrayList(); + List<ServerPlayer> list1 = this.level.players(); +@@ -1460,27 +1059,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 +@@ -1496,7 +1093,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + } + +- public class TrackedEntity { ++ public class TrackedEntity implements ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity { // Paper - optimise entity tracker + + public final ServerEntity serverEntity; + final Entity entity; +@@ -1504,6 +1101,84 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + SectionPos lastSectionPos; + public final Set<ServerPlayerConnection> seenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl + ++ // Paper start - optimise entity tracker ++ private long lastChunkUpdate = -1L; ++ private ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk lastTrackedChunk; ++ ++ @Override ++ public final void moonrise$tick(final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk chunk) { ++ if (chunk == null) { ++ this.moonrise$clearPlayers(); ++ return; ++ } ++ ++ final ca.spottedleaf.moonrise.common.list.ReferenceList<ServerPlayer> players = chunk.getPlayers(ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.VIEW_DISTANCE); ++ ++ if (players == null) { ++ this.moonrise$clearPlayers(); ++ return; ++ } ++ ++ final long lastChunkUpdate = this.lastChunkUpdate; ++ final long currChunkUpdate = chunk.getUpdateCount(); ++ final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk lastTrackedChunk = this.lastTrackedChunk; ++ this.lastChunkUpdate = currChunkUpdate; ++ this.lastTrackedChunk = chunk; ++ ++ final ServerPlayer[] playersRaw = players.getRawDataUnchecked(); ++ ++ for (int i = 0, len = players.size(); i < len; ++i) { ++ final ServerPlayer player = playersRaw[i]; ++ this.updatePlayer(player); ++ } ++ ++ if (lastChunkUpdate != currChunkUpdate || lastTrackedChunk != chunk) { ++ // need to purge any players possible not in the chunk list ++ for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) { ++ final ServerPlayer player = conn.getPlayer(); ++ if (!players.contains(player)) { ++ this.removePlayer(player); ++ } ++ } ++ } ++ } ++ ++ @Override ++ public final void moonrise$removeNonTickThreadPlayers() { ++ boolean foundToRemove = false; ++ for (final ServerPlayerConnection conn : this.seenBy) { ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(conn.getPlayer())) { ++ foundToRemove = true; ++ break; ++ } ++ } ++ ++ if (!foundToRemove) { ++ return; ++ } ++ ++ for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) { ++ ServerPlayer player = conn.getPlayer(); ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(player)) { ++ this.removePlayer(player); ++ } ++ } ++ } ++ ++ @Override ++ public final void moonrise$clearPlayers() { ++ this.lastChunkUpdate = -1; ++ this.lastTrackedChunk = null; ++ if (this.seenBy.isEmpty()) { ++ return; ++ } ++ for (final ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) { ++ ServerPlayer player = conn.getPlayer(); ++ this.removePlayer(player); ++ } ++ } ++ // Paper end - optimise entity tracker ++ + public TrackedEntity(final Entity entity, final int i, final int j, final boolean flag) { + this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit + this.entity = entity; +diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java +index cbabbfbb9967ddf9a56f3be24a88e0fcd4415aa2..71abe25cfb73af3857cbc85980aa32d0201aab62 100644 +--- a/src/main/java/net/minecraft/server/level/DistanceManager.java ++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java +@@ -36,66 +36,36 @@ import net.minecraft.world.level.ChunkPos; + import net.minecraft.world.level.chunk.LevelChunk; + import org.slf4j.Logger; + +-public abstract class DistanceManager { ++public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemDistanceManager { // Paper - rewrite chunk system + + static final Logger LOGGER = LogUtils.getLogger(); + static final int PLAYER_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING); + private static final int INITIAL_TICKET_LIST_CAPACITY = 4; + final Long2ObjectMap<ObjectSet<ServerPlayer>> playersPerChunk = new Long2ObjectOpenHashMap(); +- public final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> tickets = new Long2ObjectOpenHashMap(); +- private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); ++ // Paper - rewrite chunk system + private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8); +- private final TickingTracker tickingTicketsTracker = new TickingTracker(); +- private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(32); +- final Set<ChunkHolder> chunksToUpdateFutures = Sets.newHashSet(); +- final ChunkTaskPriorityQueueSorter ticketThrottler; +- final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> ticketThrottlerInput; +- final ProcessorHandle<ChunkTaskPriorityQueueSorter.Release> ticketThrottlerReleaser; +- final LongSet ticketsToRelease = new LongOpenHashSet(); +- final Executor mainThreadExecutor; ++ // Paper - rewrite chunk system + private long ticketTickCounter; +- public int simulationDistance = 10; ++ // Paper - rewrite chunk system + private final ChunkMap chunkMap; // Paper + ++ // Paper start - rewrite chunk system ++ public ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager getChunkHolderManager() { ++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getChunkTaskScheduler().chunkHolderManager; ++ } ++ // Paper end - rewrite chunk system ++ + protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor, ChunkMap chunkMap) { + Objects.requireNonNull(mainThreadExecutor); + ProcessorHandle<Runnable> mailbox = ProcessorHandle.of("player ticket throttler", mainThreadExecutor::execute); + ChunkTaskPriorityQueueSorter chunktaskqueuesorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(mailbox), workerExecutor, 4); + +- this.ticketThrottler = chunktaskqueuesorter; +- this.ticketThrottlerInput = chunktaskqueuesorter.getProcessor(mailbox, true); +- this.ticketThrottlerReleaser = chunktaskqueuesorter.getReleaseProcessor(mailbox); +- this.mainThreadExecutor = mainThreadExecutor; ++ // Paper - rewrite chunk system + this.chunkMap = chunkMap; // Paper + } + + protected void purgeStaleTickets() { +- ++this.ticketTickCounter; +- ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); +- +- while (objectiterator.hasNext()) { +- Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next(); +- Iterator<Ticket<?>> iterator = ((SortedArraySet) entry.getValue()).iterator(); +- boolean flag = false; +- +- while (iterator.hasNext()) { +- Ticket<?> ticket = (Ticket) iterator.next(); +- +- if (ticket.timedOut(this.ticketTickCounter)) { +- iterator.remove(); +- flag = true; +- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); +- } +- } +- +- if (flag) { +- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); +- } +- +- if (((SortedArraySet) entry.getValue()).isEmpty()) { +- objectiterator.remove(); +- } +- } ++ this.getChunkHolderManager().tick(); // Paper - rewrite chunk system + + } + +@@ -112,86 +82,15 @@ public abstract class DistanceManager { + protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k); + + public boolean runAllUpdates(ChunkMap chunkLoadingManager) { +- this.naturalSpawnChunkCounter.runAllUpdates(); +- this.tickingTicketsTracker.runAllUpdates(); +- this.playerTicketManager.runAllUpdates(); +- int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE); +- boolean flag = i != 0; +- +- if (flag) { +- ; +- } +- +- if (!this.chunksToUpdateFutures.isEmpty()) { +- this.chunksToUpdateFutures.forEach((playerchunk) -> { +- playerchunk.updateHighestAllowedStatus(chunkLoadingManager); +- }); +- this.chunksToUpdateFutures.forEach((playerchunk) -> { +- playerchunk.updateFutures(chunkLoadingManager, this.mainThreadExecutor); +- }); +- this.chunksToUpdateFutures.clear(); +- return true; +- } else { +- if (!this.ticketsToRelease.isEmpty()) { +- LongIterator longiterator = this.ticketsToRelease.iterator(); +- +- while (longiterator.hasNext()) { +- long j = longiterator.nextLong(); +- +- if (this.getTickets(j).stream().anyMatch((ticket) -> { +- return ticket.getType() == TicketType.PLAYER; +- })) { +- ChunkHolder playerchunk = chunkLoadingManager.getUpdatingChunkIfPresent(j); +- +- if (playerchunk == null) { +- throw new IllegalStateException(); +- } +- +- CompletableFuture<ChunkResult<LevelChunk>> completablefuture = playerchunk.getEntityTickingChunkFuture(); +- +- completablefuture.thenAccept((chunkresult) -> { +- this.mainThreadExecutor.execute(() -> { +- this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> { +- }, j, false)); +- }); +- }); +- } +- } +- +- this.ticketsToRelease.clear(); +- } +- +- return flag; +- } ++ return this.getChunkHolderManager().processTicketUpdates(); // Paper - rewrite chunk system + } + + boolean addTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean +- SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i); +- int j = DistanceManager.getTicketLevelAt(arraysetsorted); +- Ticket<?> ticket1 = (Ticket) arraysetsorted.addOrGet(ticket); +- +- ticket1.setCreatedTick(this.ticketTickCounter); +- if (ticket.getTicketLevel() < j) { +- this.ticketTracker.update(i, ticket.getTicketLevel(), true); +- } +- +- return ticket == ticket1; // CraftBukkit ++ return this.getChunkHolderManager().addTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system + } + + boolean removeTicket(long i, Ticket<?> ticket) { // CraftBukkit - void -> boolean +- SortedArraySet<Ticket<?>> arraysetsorted = this.getTickets(i); +- +- boolean removed = false; // CraftBukkit +- if (arraysetsorted.remove(ticket)) { +- removed = true; // CraftBukkit +- } +- +- if (arraysetsorted.isEmpty()) { +- this.tickets.remove(i); +- } +- +- this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false); +- return removed; // CraftBukkit ++ return this.getChunkHolderManager().removeTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system + } + + public <T> void addTicket(TicketType<T> type, ChunkPos pos, int level, T argument) { +@@ -210,13 +109,7 @@ public abstract class DistanceManager { + } + + public <T> boolean addRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) { +- // CraftBukkit end +- Ticket<T> ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); +- long j = chunkcoordintpair.toLong(); +- +- boolean added = this.addTicket(j, ticket); // CraftBukkit +- this.tickingTicketsTracker.addTicket(j, ticket); +- return added; // CraftBukkit ++ return this.getChunkHolderManager().addTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system + } + + public <T> void removeRegionTicket(TicketType<T> type, ChunkPos pos, int radius, T argument) { +@@ -225,32 +118,21 @@ public abstract class DistanceManager { + } + + public <T> boolean removeRegionTicketAtDistance(TicketType<T> tickettype, ChunkPos chunkcoordintpair, int i, T t0) { +- // CraftBukkit end +- Ticket<T> ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); +- long j = chunkcoordintpair.toLong(); +- +- boolean removed = this.removeTicket(j, ticket); // CraftBukkit +- this.tickingTicketsTracker.removeTicket(j, ticket); +- return removed; // CraftBukkit ++ return this.getChunkHolderManager().removeTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system + } + + private SortedArraySet<Ticket<?>> getTickets(long position) { +- return (SortedArraySet) this.tickets.computeIfAbsent(position, (j) -> { +- return SortedArraySet.create(4); +- }); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + protected void updateChunkForced(ChunkPos pos, boolean forced) { +- Ticket<ChunkPos> ticket = new Ticket<>(TicketType.FORCED, ChunkMap.FORCED_TICKET_LEVEL, pos); +- long i = pos.toLong(); +- ++ // Paper start - rewrite chunk system + if (forced) { +- this.addTicket(i, ticket); +- this.tickingTicketsTracker.addTicket(i, ticket); ++ this.getChunkHolderManager().addTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos); + } else { +- this.removeTicket(i, ticket); +- this.tickingTicketsTracker.removeTicket(i, ticket); ++ this.getChunkHolderManager().removeTicketAtLevel(TicketType.FORCED, pos, ChunkMap.FORCED_TICKET_LEVEL, pos); + } ++ // Paper end - rewrite chunk system + + } + +@@ -262,8 +144,7 @@ public abstract class DistanceManager { + return new ObjectOpenHashSet(); + })).add(player); + this.naturalSpawnChunkCounter.update(i, 0, true); +- this.playerTicketManager.update(i, 0, true); +- this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); ++ // Paper - rewrite chunk system + } + + public void removePlayer(SectionPos pos, ServerPlayer player) { +@@ -276,39 +157,39 @@ public abstract class DistanceManager { + if (objectset == null || objectset.isEmpty()) { // Paper + this.playersPerChunk.remove(i); + this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false); +- this.playerTicketManager.update(i, Integer.MAX_VALUE, false); +- this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); ++ // Paper - rewrite chunk system + } + + } + + private int getPlayerTicketLevel() { +- return Math.max(0, ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING) - this.simulationDistance); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public boolean inEntityTickingRange(long chunkPos) { +- return ChunkLevel.isEntityTicking(this.tickingTicketsTracker.getLevel(chunkPos)); ++ // Paper start - rewrite chunk system ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.getChunkHolderManager().getChunkHolder(chunkPos); ++ return chunkHolder != null && chunkHolder.isEntityTickingReady(); ++ // Paper end - rewrite chunk system + } + + public boolean inBlockTickingRange(long chunkPos) { +- return ChunkLevel.isBlockTicking(this.tickingTicketsTracker.getLevel(chunkPos)); ++ // Paper start - rewrite chunk system ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.getChunkHolderManager().getChunkHolder(chunkPos); ++ return chunkHolder != null && chunkHolder.isTickingReady(); ++ // Paper end - rewrite chunk system + } + + protected String getTicketDebugString(long pos) { +- SortedArraySet<Ticket<?>> arraysetsorted = (SortedArraySet) this.tickets.get(pos); +- +- return arraysetsorted != null && !arraysetsorted.isEmpty() ? ((Ticket) arraysetsorted.first()).toString() : "no_ticket"; ++ return this.getChunkHolderManager().getTicketDebugString(pos); // Paper - rewrite chunk system + } + + protected void updatePlayerTickets(int viewDistance) { +- this.playerTicketManager.updateViewDistance(viewDistance); ++ this.moonrise$getChunkMap().setServerViewDistance(viewDistance); // Paper - rewrite chunk system + } + + public void updateSimulationDistance(int simulationDistance) { +- if (simulationDistance != this.simulationDistance) { +- this.simulationDistance = simulationDistance; +- this.tickingTicketsTracker.replacePlayerTicketsLevel(this.getPlayerTicketLevel()); +- } ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.moonrise$getChunkMap().level).moonrise$getPlayerChunkLoader().setTickDistance(simulationDistance); // Paper - rewrite chunk system + + } + +@@ -323,103 +204,35 @@ public abstract class DistanceManager { + } + + public String getDebugStatus() { +- return this.ticketThrottler.getDebugStatus(); ++ return "No DistanceManager stats available"; // Paper - rewrite chunk system + } + + private void dumpTickets(String path) { +- try { +- FileOutputStream fileoutputstream = new FileOutputStream(new File(path)); +- +- try { +- ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().iterator(); +- +- while (objectiterator.hasNext()) { +- Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next(); +- ChunkPos chunkcoordintpair = new ChunkPos(entry.getLongKey()); +- Iterator iterator = ((SortedArraySet) entry.getValue()).iterator(); +- +- while (iterator.hasNext()) { +- Ticket<?> ticket = (Ticket) iterator.next(); +- +- fileoutputstream.write((chunkcoordintpair.x + "\t" + chunkcoordintpair.z + "\t" + String.valueOf(ticket.getType()) + "\t" + ticket.getTicketLevel() + "\t\n").getBytes(StandardCharsets.UTF_8)); +- } +- } +- } catch (Throwable throwable) { +- try { +- fileoutputstream.close(); +- } catch (Throwable throwable1) { +- throwable.addSuppressed(throwable1); +- } +- +- throw throwable; +- } +- +- fileoutputstream.close(); +- } catch (IOException ioexception) { +- DistanceManager.LOGGER.error("Failed to dump tickets to {}", path, ioexception); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + + } + + @VisibleForTesting + TickingTracker tickingTracker() { +- return this.tickingTicketsTracker; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public void removeTicketsOnClosing() { +- ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve +- ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); +- +- while (objectiterator.hasNext()) { +- Entry<SortedArraySet<Ticket<?>>> entry = (Entry) objectiterator.next(); +- Iterator<Ticket<?>> iterator = ((SortedArraySet) entry.getValue()).iterator(); +- boolean flag = false; +- +- while (iterator.hasNext()) { +- Ticket<?> ticket = (Ticket) iterator.next(); +- +- if (!immutableset.contains(ticket.getType())) { +- iterator.remove(); +- flag = true; +- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); +- } +- } +- +- if (flag) { +- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); +- } +- +- if (((SortedArraySet) entry.getValue()).isEmpty()) { +- objectiterator.remove(); +- } +- } ++ // Paper - rewrite chunk system + + } + + public boolean hasTickets() { +- return !this.tickets.isEmpty(); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + // CraftBukkit start + public <T> void removeAllTicketsFor(TicketType<T> ticketType, int ticketLevel, T ticketIdentifier) { +- Ticket<T> target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier); +- +- for (java.util.Iterator<Entry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) { +- Entry<SortedArraySet<Ticket<?>>> entry = iterator.next(); +- SortedArraySet<Ticket<?>> tickets = entry.getValue(); +- if (tickets.remove(target)) { +- // copied from removeTicket +- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false); +- +- // can't use entry after it's removed +- if (tickets.isEmpty()) { +- iterator.remove(); +- } +- } +- } ++ this.getChunkHolderManager().removeAllTicketsFor(ticketType, ticketLevel, ticketIdentifier); // Paper - rewrite chunk system + } + // CraftBukkit end + ++ /* // Paper - rewrite chunk system + private class ChunkTicketTracker extends ChunkTracker { + + private static final int MAX_LEVEL = ChunkLevel.MAX_LEVEL + 1; +@@ -465,7 +278,7 @@ public abstract class DistanceManager { + public int runDistanceUpdates(int distance) { + return this.runUpdates(distance); + } +- } ++ }*/ // Paper - rewrite chunk system + + private class FixedPlayerDistanceChunkTracker extends ChunkTracker { + +@@ -545,6 +358,7 @@ public abstract class DistanceManager { + } + } + ++ /* // Paper - rewrite chunk system + private class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker { + + private int viewDistance = 0; +@@ -639,5 +453,5 @@ public abstract class DistanceManager { + private boolean haveTicketFor(int distance) { + return distance <= this.viewDistance; + } +- } ++ }*/ // Paper - rewrite chunk system + } +diff --git a/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java b/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java +index 3dc1daa3c6a04d3ff1a2353773b465fc380994a2..3575782f13a7f3c52e64dc5046803305d5c8ce12 100644 +--- a/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java ++++ b/src/main/java/net/minecraft/server/level/GenerationChunkHolder.java +@@ -27,249 +27,105 @@ public abstract class GenerationChunkHolder { + public static final ChunkResult<ChunkAccess> UNLOADED_CHUNK = ChunkResult.error("Unloaded chunk"); + public static final CompletableFuture<ChunkResult<ChunkAccess>> UNLOADED_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_CHUNK); + protected final ChunkPos pos; +- @Nullable +- private volatile ChunkStatus highestAllowedStatus; +- private final AtomicReference<ChunkStatus> startedWork = new AtomicReference<>(); +- private final AtomicReferenceArray<CompletableFuture<ChunkResult<ChunkAccess>>> futures = new AtomicReferenceArray<>(CHUNK_STATUSES.size()); +- private final AtomicReference<ChunkGenerationTask> task = new AtomicReference<>(); +- private final AtomicInteger generationRefCount = new AtomicInteger(); ++ // Paper - rewrite chunk system + + public GenerationChunkHolder(ChunkPos pos) { + this.pos = pos; + } + + public CompletableFuture<ChunkResult<ChunkAccess>> scheduleChunkGenerationTask(ChunkStatus requestedStatus, ChunkMap chunkLoadingManager) { +- if (this.isStatusDisallowed(requestedStatus)) { +- return UNLOADED_CHUNK_FUTURE; +- } else { +- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.getOrCreateFuture(requestedStatus); +- if (completableFuture.isDone()) { +- return completableFuture; +- } else { +- ChunkGenerationTask chunkGenerationTask = this.task.get(); +- if (chunkGenerationTask == null || requestedStatus.isAfter(chunkGenerationTask.targetStatus)) { +- this.rescheduleChunkTask(chunkLoadingManager, requestedStatus); +- } +- +- return completableFuture; +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + CompletableFuture<ChunkResult<ChunkAccess>> applyStep(ChunkStep step, GeneratingChunkMap chunkLoadingManager, StaticCache2D<GenerationChunkHolder> chunks) { +- if (this.isStatusDisallowed(step.targetStatus())) { +- return UNLOADED_CHUNK_FUTURE; +- } else { +- return this.acquireStatusBump(step.targetStatus()) ? chunkLoadingManager.applyStep(this, step, chunks).handle((chunk, throwable) -> { +- if (throwable != null) { +- CrashReport crashReport = CrashReport.forThrowable(throwable, "Exception chunk generation/loading"); +- MinecraftServer.setFatalException(new ReportedException(crashReport)); +- } else { +- this.completeFuture(step.targetStatus(), chunk); +- } +- +- return ChunkResult.of(chunk); +- }) : this.getOrCreateFuture(step.targetStatus()); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + protected void updateHighestAllowedStatus(ChunkMap chunkLoadingManager) { +- ChunkStatus chunkStatus = this.highestAllowedStatus; +- ChunkStatus chunkStatus2 = ChunkLevel.generationStatus(this.getTicketLevel()); +- this.highestAllowedStatus = chunkStatus2; +- boolean bl = chunkStatus != null && (chunkStatus2 == null || chunkStatus2.isBefore(chunkStatus)); +- if (bl) { +- this.failAndClearPendingFuturesBetween(chunkStatus2, chunkStatus); +- if (this.task.get() != null) { +- this.rescheduleChunkTask(chunkLoadingManager, this.findHighestStatusWithPendingFuture(chunkStatus2)); +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public void replaceProtoChunk(ImposterProtoChunk chunk) { +- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = CompletableFuture.completedFuture(ChunkResult.of(chunk)); +- +- for (int i = 0; i < this.futures.length() - 1; i++) { +- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture2 = this.futures.get(i); +- Objects.requireNonNull(completableFuture2); +- ChunkAccess chunkAccess = completableFuture2.getNow(NOT_DONE_YET).orElse(null); +- if (!(chunkAccess instanceof ProtoChunk)) { +- throw new IllegalStateException("Trying to replace a ProtoChunk, but found " + chunkAccess); +- } +- +- if (!this.futures.compareAndSet(i, completableFuture2, completableFuture)) { +- throw new IllegalStateException("Future changed by other thread while trying to replace it"); +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + void removeTask(ChunkGenerationTask loader) { +- this.task.compareAndSet(loader, null); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void rescheduleChunkTask(ChunkMap chunkLoadingManager, @Nullable ChunkStatus requestedStatus) { +- ChunkGenerationTask chunkGenerationTask; +- if (requestedStatus != null) { +- chunkGenerationTask = chunkLoadingManager.scheduleGenerationTask(requestedStatus, this.getPos()); +- } else { +- chunkGenerationTask = null; +- } +- +- ChunkGenerationTask chunkGenerationTask3 = this.task.getAndSet(chunkGenerationTask); +- if (chunkGenerationTask3 != null) { +- chunkGenerationTask3.markForCancellation(); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private CompletableFuture<ChunkResult<ChunkAccess>> getOrCreateFuture(ChunkStatus status) { +- if (this.isStatusDisallowed(status)) { +- return UNLOADED_CHUNK_FUTURE; +- } else { +- int i = status.getIndex(); +- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i); +- +- while (completableFuture == null) { +- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture2 = new CompletableFuture<>(); +- completableFuture = this.futures.compareAndExchange(i, null, completableFuture2); +- if (completableFuture == null) { +- if (this.isStatusDisallowed(status)) { +- this.failAndClearPendingFuture(i, completableFuture2); +- return UNLOADED_CHUNK_FUTURE; +- } +- +- return completableFuture2; +- } +- } +- +- return completableFuture; +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void failAndClearPendingFuturesBetween(@Nullable ChunkStatus from, ChunkStatus to) { +- int i = from == null ? 0 : from.getIndex() + 1; +- int j = to.getIndex(); +- +- for (int k = i; k <= j; k++) { +- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(k); +- if (completableFuture != null) { +- this.failAndClearPendingFuture(k, completableFuture); +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void failAndClearPendingFuture(int statusIndex, CompletableFuture<ChunkResult<ChunkAccess>> previousFuture) { +- if (previousFuture.complete(UNLOADED_CHUNK) && !this.futures.compareAndSet(statusIndex, previousFuture, null)) { +- throw new IllegalStateException("Nothing else should replace the future here"); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void completeFuture(ChunkStatus status, ChunkAccess chunk) { +- ChunkResult<ChunkAccess> chunkResult = ChunkResult.of(chunk); +- int i = status.getIndex(); +- +- while (true) { +- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(i); +- if (completableFuture == null) { +- if (this.futures.compareAndSet(i, null, CompletableFuture.completedFuture(chunkResult))) { +- return; +- } +- } else { +- if (completableFuture.complete(chunkResult)) { +- return; +- } +- +- if (completableFuture.getNow(NOT_DONE_YET).isSuccess()) { +- throw new IllegalStateException("Trying to complete a future but found it to be completed successfully already"); +- } +- +- Thread.yield(); +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Nullable + private ChunkStatus findHighestStatusWithPendingFuture(@Nullable ChunkStatus checkUpperBound) { +- if (checkUpperBound == null) { +- return null; +- } else { +- ChunkStatus chunkStatus = checkUpperBound; +- +- for (ChunkStatus chunkStatus2 = this.startedWork.get(); +- chunkStatus2 == null || chunkStatus.isAfter(chunkStatus2); +- chunkStatus = chunkStatus.getParent() +- ) { +- if (this.futures.get(chunkStatus.getIndex()) != null) { +- return chunkStatus; +- } +- +- if (chunkStatus == ChunkStatus.EMPTY) { +- break; +- } +- } +- +- return null; +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private boolean acquireStatusBump(ChunkStatus nextStatus) { +- ChunkStatus chunkStatus = nextStatus == ChunkStatus.EMPTY ? null : nextStatus.getParent(); +- ChunkStatus chunkStatus2 = this.startedWork.compareAndExchange(chunkStatus, nextStatus); +- if (chunkStatus2 == chunkStatus) { +- return true; +- } else if (chunkStatus2 != null && !nextStatus.isAfter(chunkStatus2)) { +- return false; +- } else { +- throw new IllegalStateException("Unexpected last startedWork status: " + chunkStatus2 + " while trying to start: " + nextStatus); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private boolean isStatusDisallowed(ChunkStatus status) { +- ChunkStatus chunkStatus = this.highestAllowedStatus; +- return chunkStatus == null || status.isAfter(chunkStatus); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public void increaseGenerationRefCount() { +- this.generationRefCount.incrementAndGet(); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public void decreaseGenerationRefCount() { +- int i = this.generationRefCount.decrementAndGet(); +- if (i < 0) { +- throw new IllegalStateException("More releases than claims. Count: " + i); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public int getGenerationRefCount() { +- return this.generationRefCount.get(); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Nullable + public ChunkAccess getChunkIfPresentUnchecked(ChunkStatus requestedStatus) { +- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(requestedStatus.getIndex()); +- return completableFuture == null ? null : completableFuture.getNow(NOT_DONE_YET).orElse(null); ++ // Paper start - rewrite chunk system ++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresentUnchecked(requestedStatus); ++ // Paper end - rewrite chunk system + } + + @Nullable + public ChunkAccess getChunkIfPresent(ChunkStatus requestedStatus) { +- return this.isStatusDisallowed(requestedStatus) ? null : this.getChunkIfPresentUnchecked(requestedStatus); ++ // Paper start - rewrite chunk system ++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkIfPresent(requestedStatus); ++ // Paper end - rewrite chunk system + } + + @Nullable + public ChunkAccess getLatestChunk() { +- ChunkStatus chunkStatus = this.startedWork.get(); +- if (chunkStatus == null) { +- return null; +- } else { +- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(chunkStatus); +- return chunkAccess != null ? chunkAccess : this.getChunkIfPresentUnchecked(chunkStatus.getParent()); +- } ++ // Paper start - rewrite chunk system ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion(); ++ return lastCompletion == null ? null : lastCompletion.chunk(); ++ // Paper end - rewrite chunk system + } + + @Nullable + public ChunkStatus getPersistedStatus() { +- CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = this.futures.get(ChunkStatus.EMPTY.getIndex()); +- ChunkAccess chunkAccess = completableFuture == null ? null : completableFuture.getNow(NOT_DONE_YET).orElse(null); +- return chunkAccess == null ? null : chunkAccess.getPersistedStatus(); ++ // Paper start - rewrite chunk system ++ final ChunkAccess chunk = this.getLatestChunk(); ++ return chunk == null ? null : chunk.getPersistedStatus(); ++ // Paper end - rewrite chunk system + } + + public ChunkPos getPos() { +@@ -277,7 +133,7 @@ public abstract class GenerationChunkHolder { + } + + public FullChunkStatus getFullStatus() { +- return ChunkLevel.fullStatus(this.getTicketLevel()); ++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getChunkStatus(); // Paper - rewrite chunk system + } + + public abstract int getTicketLevel(); +@@ -286,26 +142,15 @@ public abstract class GenerationChunkHolder { + + @VisibleForDebug + public List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> getAllFutures() { +- List<Pair<ChunkStatus, CompletableFuture<ChunkResult<ChunkAccess>>>> list = new ArrayList<>(); +- +- for (int i = 0; i < CHUNK_STATUSES.size(); i++) { +- list.add(Pair.of(CHUNK_STATUSES.get(i), this.futures.get(i))); +- } +- +- return list; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Nullable + @VisibleForDebug + public ChunkStatus getLatestStatus() { +- for (int i = CHUNK_STATUSES.size() - 1; i >= 0; i--) { +- ChunkStatus chunkStatus = CHUNK_STATUSES.get(i); +- ChunkAccess chunkAccess = this.getChunkIfPresentUnchecked(chunkStatus); +- if (chunkAccess != null) { +- return chunkStatus; +- } +- } +- +- return null; ++ // Paper start - rewrite chunk system ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)(Object)this).moonrise$getRealChunkHolder().getLastChunkCompletion(); ++ return lastCompletion == null ? null : lastCompletion.genStatus(); ++ // Paper end - rewrite chunk system + } + } +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index be9604a0f267558c95125852d86761a2f175732a..d2cb358c340bcf7532fd25eccdd33c6945d16de4 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -46,7 +46,7 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp + import net.minecraft.world.level.storage.DimensionDataStorage; + import net.minecraft.world.level.storage.LevelStorageSource; + +-public class ServerChunkCache extends ChunkSource { ++public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache { // Paper - rewrite chunk system + + public static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); // Paper + private static final List<ChunkStatus> CHUNK_STATUSES = ChunkStatus.getStatusList(); +@@ -73,6 +73,61 @@ public class ServerChunkCache extends ChunkSource { + private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<net.minecraft.world.level.chunk.LevelChunk> fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>(); + long chunkFutureAwaitCounter; + // Paper end ++ // Paper start - rewrite chunk system ++ ++ @Override ++ public final void moonrise$setFullChunk(final int chunkX, final int chunkZ, final LevelChunk chunk) { ++ final long key = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ if (chunk == null) { ++ this.fullChunks.remove(key); ++ } else { ++ this.fullChunks.put(key, chunk); ++ } ++ } ++ ++ @Override ++ public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) { ++ return this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ private ChunkAccess syncLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus) { ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler(); ++ final CompletableFuture<ChunkAccess> completable = new CompletableFuture<>(); ++ chunkTaskScheduler.scheduleChunkLoad( ++ chunkX, chunkZ, toStatus, true, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING, ++ completable::complete ++ ); ++ ++ if (io.papermc.paper.util.TickThread.isTickThreadFor(this.level, chunkX, chunkZ)) { ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.pushChunkWait(this.level, chunkX, chunkZ); ++ this.mainThreadProcessor.managedBlock(completable::isDone); ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.popChunkWait(); ++ } ++ ++ final ChunkAccess ret = completable.join(); ++ if (ret == null) { ++ throw new IllegalStateException("Chunk not loaded when requested"); ++ } ++ ++ return ret; ++ } ++ ++ private ChunkAccess getChunkFallback(final int chunkX, final int chunkZ, final ChunkStatus toStatus, ++ final boolean load) { ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler(); ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager; ++ ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder currentChunk = chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ ++ final ChunkAccess ifPresent = currentChunk == null ? null : currentChunk.getChunkIfPresent(toStatus); ++ ++ if (ifPresent != null && (toStatus != ChunkStatus.FULL || currentChunk.isFullChunkReady())) { ++ return ifPresent; ++ } ++ ++ return load ? this.syncLoad(chunkX, chunkZ, toStatus) : null; ++ } ++ // Paper end - rewrite chunk system + + public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) { + this.level = world; +@@ -99,13 +154,7 @@ public class ServerChunkCache extends ChunkSource { + } + // CraftBukkit end + // Paper start +- public void addLoadedChunk(LevelChunk chunk) { +- this.fullChunks.put(chunk.coordinateKey, chunk); +- } +- +- public void removeLoadedChunk(LevelChunk chunk) { +- this.fullChunks.remove(chunk.coordinateKey); +- } ++ // Paper - rewrite chunk system + + @Nullable + public ChunkAccess getChunkAtImmediately(int x, int z) { +@@ -176,63 +225,25 @@ public class ServerChunkCache extends ChunkSource { + @Nullable + @Override + public ChunkAccess getChunk(int x, int z, ChunkStatus leastStatus, boolean create) { +- if (Thread.currentThread() != this.mainThread) { +- return (ChunkAccess) CompletableFuture.supplyAsync(() -> { +- return this.getChunk(x, z, leastStatus, create); +- }, this.mainThreadProcessor).join(); +- } else { +- // Paper start - Perf: Optimise getChunkAt calls for loaded chunks +- LevelChunk ifLoaded = this.getChunkAtIfLoadedMainThread(x, z); +- if (ifLoaded != null) { +- return ifLoaded; +- } +- // Paper end - Perf: Optimise getChunkAt calls for loaded chunks +- ProfilerFiller gameprofilerfiller = this.level.getProfiler(); ++ // Paper start - rewrite chunk system ++ if (leastStatus == ChunkStatus.FULL) { ++ final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x, z)); + +- gameprofilerfiller.incrementCounter("getChunk"); +- long k = ChunkPos.asLong(x, z); +- +- for (int l = 0; l < 4; ++l) { +- if (k == this.lastChunkPos[l] && leastStatus == this.lastChunkStatus[l]) { +- ChunkAccess ichunkaccess = this.lastChunk[l]; +- +- if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime +- return ichunkaccess; +- } +- } ++ if (ret != null) { ++ return ret; + } + +- gameprofilerfiller.incrementCounter("getChunkCacheMiss"); +- CompletableFuture<ChunkResult<ChunkAccess>> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create); +- ServerChunkCache.MainThreadExecutor chunkproviderserver_b = this.mainThreadProcessor; +- +- Objects.requireNonNull(completablefuture); +- if (!completablefuture.isDone()) { // Paper +- com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads +- this.level.timings.syncChunkLoad.startTiming(); // Paper +- chunkproviderserver_b.managedBlock(completablefuture::isDone); +- this.level.timings.syncChunkLoad.stopTiming(); // Paper +- } // Paper +- ChunkResult<ChunkAccess> chunkresult = (ChunkResult) completablefuture.join(); +- ChunkAccess ichunkaccess1 = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error +- +- if (ichunkaccess1 == null && create) { +- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Chunk not there when requested: " + chunkresult.getError())); +- } else { +- this.storeInCache(k, ichunkaccess1, leastStatus); +- return ichunkaccess1; +- } ++ return create ? this.getChunkFallback(x, z, leastStatus, create) : null; + } ++ ++ return this.getChunkFallback(x, z, leastStatus, create); ++ // Paper end - rewrite chunk system + } + + @Nullable + @Override + public LevelChunk getChunkNow(int chunkX, int chunkZ) { +- if (Thread.currentThread() != this.mainThread) { +- return null; +- } else { +- return this.getChunkAtIfLoadedMainThread(chunkX, chunkZ); // Paper - Perf: Optimise getChunkAt calls for loaded chunks +- } ++ return this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); // Paper - rewrite chunk system + } + + private void clearCache() { +@@ -263,56 +274,59 @@ public class ServerChunkCache extends ChunkSource { + } + + private CompletableFuture<ChunkResult<ChunkAccess>> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { +- ChunkPos chunkcoordintpair = new ChunkPos(chunkX, chunkZ); +- long k = chunkcoordintpair.toLong(); +- int l = ChunkLevel.byStatus(leastStatus); +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k); ++ // Paper start - rewrite chunk system ++ io.papermc.paper.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Scheduling chunk load off-main"); + +- // CraftBukkit start - don't add new ticket for currently unloading chunk +- boolean currentlyUnloading = false; +- if (playerchunk != null) { +- FullChunkStatus oldChunkState = ChunkLevel.fullStatus(playerchunk.oldTicketLevel); +- FullChunkStatus currentChunkState = ChunkLevel.fullStatus(playerchunk.getTicketLevel()); +- currentlyUnloading = (oldChunkState.isOrAfter(FullChunkStatus.FULL) && !currentChunkState.isOrAfter(FullChunkStatus.FULL)); ++ final int minLevel = ChunkLevel.byStatus(leastStatus); ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ); ++ ++ final boolean needsFullScheduling = leastStatus == ChunkStatus.FULL && (chunkHolder == null || !chunkHolder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)); ++ ++ if ((chunkHolder == null || chunkHolder.getTicketLevel() > minLevel || needsFullScheduling) && !create) { ++ return ChunkHolder.UNLOADED_CHUNK_FUTURE; + } +- if (create && !currentlyUnloading) { +- // CraftBukkit end +- this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair); +- if (this.chunkAbsent(playerchunk, l)) { +- ProfilerFiller gameprofilerfiller = this.level.getProfiler(); +- +- gameprofilerfiller.push("chunkLoad"); +- this.runDistanceManagerUpdates(); +- playerchunk = this.getVisibleChunkIfPresent(k); +- gameprofilerfiller.pop(); +- if (this.chunkAbsent(playerchunk, l)) { +- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("No chunk holder after ticket has been added")); ++ ++ final ChunkAccess ifPresent = chunkHolder == null ? null : chunkHolder.getChunkIfPresent(leastStatus); ++ if (needsFullScheduling || ifPresent == null) { ++ // schedule ++ final CompletableFuture<ChunkResult<ChunkAccess>> ret = new CompletableFuture<>(); ++ final Consumer<ChunkAccess> complete = (ChunkAccess chunk) -> { ++ if (chunk == null) { ++ ret.complete(ChunkHolder.UNLOADED_CHUNK); ++ } else { ++ ret.complete(ChunkResult.of(chunk)); + } +- } +- } ++ }; + +- return this.chunkAbsent(playerchunk, l) ? GenerationChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.scheduleChunkGenerationTask(leastStatus, this.chunkMap); +- } ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().scheduleChunkLoad( ++ chunkX, chunkZ, leastStatus, true, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, ++ complete ++ ); + +- private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) { +- return holder == null || holder.oldTicketLevel > maxLevel; // CraftBukkit using oldTicketLevel for isLoaded checks ++ return ret; ++ } else { ++ // can return now ++ return CompletableFuture.completedFuture(ChunkResult.of(ifPresent)); ++ } ++ // Paper end - rewrite chunk system + } + + @Override + public boolean hasChunk(int x, int z) { +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent((new ChunkPos(x, z)).toLong()); +- int k = ChunkLevel.byStatus(ChunkStatus.FULL); +- +- return !this.chunkAbsent(playerchunk, k); ++ return this.getChunkNow(x, z) != null; // Paper - rewrite chunk system + } + + @Nullable + @Override + public LightChunk getChunkForLighting(int chunkX, int chunkZ) { +- long k = ChunkPos.asLong(chunkX, chunkZ); +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k); +- +- return playerchunk == null ? null : playerchunk.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent()); ++ // Paper start - rewrite chunk system ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ); ++ if (newChunkHolder == null) { ++ return null; ++ } ++ return newChunkHolder.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent()); ++ // Paper end - rewrite chunk system + } + + @Override +@@ -325,16 +339,7 @@ public class ServerChunkCache extends ChunkSource { + } + + public boolean runDistanceManagerUpdates() { // Paper - public +- boolean flag = this.distanceManager.runAllUpdates(this.chunkMap); +- boolean flag1 = this.chunkMap.promoteChunkMap(); +- +- this.chunkMap.runGenerationTasks(); +- if (!flag && !flag1) { +- return false; +- } else { +- this.clearCache(); +- return true; +- } ++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // Paper - rewrite chunk system + } + + // Paper start +@@ -344,13 +349,14 @@ public class ServerChunkCache extends ChunkSource { + // Paper end + + public boolean isPositionTicking(long pos) { +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); +- +- return playerchunk == null ? false : (!this.level.shouldTickBlocksAt(pos) ? false : ((ChunkResult) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).isSuccess()); ++ // Paper start - rewrite chunk system ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(pos); ++ return newChunkHolder != null && newChunkHolder.isTickingReady(); ++ // Paper end - rewrite chunk system + } + + public void save(boolean flush) { +- this.runDistanceManagerUpdates(); ++ // Paper - rewrite chunk system + try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings + this.chunkMap.saveAllChunks(flush); + } // Paper - Timings +@@ -363,12 +369,7 @@ public class ServerChunkCache extends ChunkSource { + } + + public void close(boolean save) throws IOException { +- if (save) { +- this.save(true); +- } +- // CraftBukkit end +- this.lightEngine.close(); +- this.chunkMap.close(); ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.close(save, true); // Paper - rewrite chunk system + } + + // CraftBukkit start - modelled on below +@@ -396,6 +397,7 @@ public class ServerChunkCache extends ChunkSource { + this.level.getProfiler().popPush("chunks"); + if (tickChunks) { + this.level.timings.chunks.startTiming(); // Paper - timings ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().tick(); // Paper - rewrite chunk system + this.tickChunks(); + this.level.timings.chunks.stopTiming(); // Paper - timings + this.chunkMap.tick(); +@@ -410,6 +412,7 @@ public class ServerChunkCache extends ChunkSource { + } + + private void tickChunks() { ++ long chunksTicked = 0; // Paper - rewrite chunk system + long i = this.level.getGameTime(); + long j = i - this.lastInhabitedUpdate; + +@@ -470,6 +473,11 @@ public class ServerChunkCache extends ChunkSource { + + if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { + this.level.tickChunk(chunk1, l); ++ // Paper start - rewrite chunk system ++ if ((++chunksTicked & 7L) == 0L) { ++ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.level.getServer()).moonrise$executeMidTickTasks(); ++ } ++ // Paper end - rewrite chunk system + } + } + } +@@ -495,11 +503,12 @@ public class ServerChunkCache extends ChunkSource { + } + + private void getFullChunk(long pos, Consumer<LevelChunk> chunkConsumer) { +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); +- +- if (playerchunk != null) { +- ((ChunkResult) playerchunk.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).ifSuccess(chunkConsumer); ++ // Paper start - rewrite chunk system ++ final LevelChunk fullChunk = this.getChunkNow(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos)); ++ if (fullChunk != null) { ++ chunkConsumer.accept(fullChunk); + } ++ // Paper end - rewrite chunk system + + } + +@@ -593,6 +602,12 @@ public class ServerChunkCache extends ChunkSource { + this.chunkMap.setServerViewDistance(watchDistance); + } + ++ // Paper start - rewrite chunk system ++ public void setSendViewDistance(int viewDistance) { ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getPlayerChunkLoader().setSendDistance(viewDistance); ++ } ++ // Paper end - rewrite chunk system ++ + public void setSimulationDistance(int simulationDistance) { + this.distanceManager.updateSimulationDistance(simulationDistance); + } +@@ -671,16 +686,14 @@ public class ServerChunkCache extends ChunkSource { + @Override + // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task + public boolean pollTask() { +- try { +- if (ServerChunkCache.this.runDistanceManagerUpdates()) { ++ // Paper start - rewrite chunk system ++ final ServerChunkCache serverChunkCache = ServerChunkCache.this; ++ if (serverChunkCache.runDistanceManagerUpdates()) { + return true; + } else { +- ServerChunkCache.this.lightEngine.tryScheduleUpdate(); +- return super.pollTask(); ++ return super.pollTask() | ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)serverChunkCache.level).moonrise$getChunkTaskScheduler().executeMainThreadTask(); + } +- } finally { +- ServerChunkCache.this.chunkMap.callbackExecutor.run(); +- } ++ // Paper end - rewrite chunk system + // CraftBukkit end + } + } +diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java +index 1d849ce4e2c85f149af25318b8ffb6dcef6c6788..12d86f27d04bffed8c3844e36b42fbc2f84dacff 100644 +--- a/src/main/java/net/minecraft/server/level/ServerEntity.java ++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java +@@ -97,6 +97,11 @@ public class ServerEntity { + } + + public void sendChanges() { ++ // Paper start - optimise collisions ++ if (((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)this.entity).moonrise$isHardColliding()) { ++ this.teleportDelay = 9999; ++ } ++ // Paper end - optimise collisions + List<Entity> list = this.entity.getPassengers(); + + if (!list.equals(this.lastPassengers)) { +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 4d7e234d379a451c4bb53bc2fcdf22cb191f8d1a..bb3826481d6165991f4201ed39d56fa5824a841f 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,176 @@ public class ServerLevel extends Level implements WorldGenLevel { + return player != null && player.level() == this ? player : null; + } + // Paper end - optimise getPlayerByUUID ++ // Paper start - rewrite chunk system ++ private boolean markedClosing; ++ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder(); ++ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader chunkLoader = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader((ServerLevel)(Object)this); ++ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController entityDataController; ++ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController poiDataController; ++ private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController chunkDataController; ++ private final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler; ++ private long lastMidTickFailure; ++ private long tickedBlocksOrFluids; ++ private final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = new ca.spottedleaf.moonrise.common.misc.NearbyPlayers((ServerLevel)(Object)this); ++ ++ @Override ++ public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) { ++ return this.chunkSource.getChunkNow(chunkX, chunkZ); ++ } ++ ++ @Override ++ public final ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) { ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ if (newChunkHolder == null) { ++ return null; ++ } ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion(); ++ return lastCompletion == null ? null : lastCompletion.chunk(); ++ } ++ ++ @Override ++ public final ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus leastStatus) { ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ); ++ if (newChunkHolder == null) { ++ return null; ++ } ++ return newChunkHolder.getChunkIfPresentUnchecked(leastStatus); ++ } ++ ++ @Override ++ public final void moonrise$midTickTasks() { ++ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks(); ++ } ++ ++ @Override ++ public final ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus status) { ++ return this.moonrise$getChunkTaskScheduler().syncLoadNonFull(chunkX, chunkZ, status); ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler moonrise$getChunkTaskScheduler() { ++ return this.chunkTaskScheduler; ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.ChunkDataController moonrise$getChunkDataController() { ++ return this.chunkDataController; ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.ChunkDataController moonrise$getPoiChunkDataController() { ++ return this.poiDataController; ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.ChunkDataController moonrise$getEntityChunkDataController() { ++ return this.entityDataController; ++ } ++ ++ @Override ++ public final int moonrise$getRegionChunkShift() { ++ return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(); ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader() { ++ return this.chunkLoader; ++ } ++ ++ @Override ++ public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, ++ final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) { ++ this.moonrise$loadChunksAsync( ++ (pos.getX() - radiusBlocks) >> 4, ++ (pos.getX() + radiusBlocks) >> 4, ++ (pos.getZ() - radiusBlocks) >> 4, ++ (pos.getZ() + radiusBlocks) >> 4, ++ priority, onLoad ++ ); ++ } ++ ++ @Override ++ public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks, ++ final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) { ++ this.moonrise$loadChunksAsync( ++ (pos.getX() - radiusBlocks) >> 4, ++ (pos.getX() + radiusBlocks) >> 4, ++ (pos.getZ() - radiusBlocks) >> 4, ++ (pos.getZ() + radiusBlocks) >> 4, ++ chunkStatus, priority, onLoad ++ ); ++ } ++ ++ @Override ++ public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, ++ final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) { ++ this.moonrise$loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, priority, onLoad); ++ } ++ ++ @Override ++ public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ, ++ final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) { ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = this.moonrise$getChunkTaskScheduler(); ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager; ++ ++ final int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); ++ final java.util.concurrent.atomic.AtomicInteger loadedChunks = new java.util.concurrent.atomic.AtomicInteger(); ++ final Long holderIdentifier = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getNextChunkLoadId(); ++ final int ticketLevel = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getTicketLevel(chunkStatus); ++ ++ final List<ChunkAccess> ret = new ArrayList<>(requiredChunks); ++ ++ final java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (final ChunkAccess chunk) -> { ++ if (chunk != null) { ++ synchronized (ret) { ++ ret.add(chunk); ++ } ++ chunkHolderManager.addTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunk.getPos(), ticketLevel, holderIdentifier); ++ } ++ if (loadedChunks.incrementAndGet() == requiredChunks) { ++ try { ++ onLoad.accept(java.util.Collections.unmodifiableList(ret)); ++ } finally { ++ for (int i = 0, len = ret.size(); i < len; ++i) { ++ final ChunkPos chunkPos = ret.get(i).getPos(); ++ ++ chunkHolderManager.removeTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunkPos, ticketLevel, holderIdentifier); ++ } ++ } ++ } ++ }; ++ ++ for (int cx = minChunkX; cx <= maxChunkX; ++cx) { ++ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { ++ chunkTaskScheduler.scheduleChunkLoad(cx, cz, chunkStatus, true, priority, consumer); ++ } ++ } ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() { ++ return this.viewDistanceHolder; ++ } ++ ++ @Override ++ public final long moonrise$getLastMidTickFailure() { ++ return this.lastMidTickFailure; ++ } ++ ++ @Override ++ public final void moonrise$setLastMidTickFailure(final long time) { ++ this.lastMidTickFailure = time; ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.common.misc.NearbyPlayers moonrise$getNearbyPlayers() { ++ return this.nearbyPlayers; ++ } ++ // 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 +555,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 +589,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 +735,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 +760,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() { +@@ -626,6 +811,63 @@ public class ServerLevel extends Level implements WorldGenLevel { + }); + } + ++ // Paper start - optimise random ticking ++ private void optimiseRandomTick(final LevelChunk chunk, final int tickSpeed) { ++ final LevelChunkSection[] sections = chunk.getSections(); ++ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((ServerLevel)(Object)this); ++ final RandomSource random = this.random; ++ final boolean tickFluids = false; // Paper - not configurable - MC-224294 ++ ++ final ChunkPos cpos = chunk.getPos(); ++ final int offsetX = cpos.x << 4; ++ final int offsetZ = cpos.z << 4; ++ ++ for (int sectionIndex = 0, sectionsLen = sections.length; sectionIndex < sectionsLen; sectionIndex++) { ++ final int offsetY = (sectionIndex + minSection) << 4; ++ final LevelChunkSection section = sections[sectionIndex]; ++ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> states = section.states; ++ if (section == null || !section.isRandomlyTickingBlocks()) { ++ continue; ++ } ++ ++ final ca.spottedleaf.moonrise.common.list.IBlockDataList tickList = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getTickingBlockList(); ++ if (tickList.size() == 0) { ++ continue; ++ } ++ ++ for (int i = 0; i < tickSpeed; ++i) { ++ final int tickingBlocks = tickList.size(); ++ final int index = random.nextInt() & ((16 * 16 * 16) - 1); ++ ++ if (index >= tickingBlocks) { ++ // most of the time we fall here ++ continue; ++ } ++ ++ final long raw = tickList.getRaw(index); ++ final int location = ca.spottedleaf.moonrise.common.list.IBlockDataList.getLocationFromRaw(raw); ++ final int randomX = (location & 15); ++ final int randomY = ((location >>> (4 + 4)) & 255); ++ final int randomZ = ((location >>> 4) & 15); ++ final BlockState state = states.get(randomX | (randomZ << 4) | (randomY << 8)); ++ ++ // do not use a mutable pos, as some random tick implementations store the input without calling immutable()! ++ final BlockPos pos = new BlockPos(randomX | offsetX, randomY | offsetY, randomZ | offsetZ); ++ ++ state.randomTick((ServerLevel)(Object)this, pos, random); ++ if (tickFluids) { ++ final FluidState fluidState = state.getFluidState(); ++ if (fluidState.isRandomlyTicking()) { ++ fluidState.randomTick((ServerLevel)(Object)this, pos, random); ++ } ++ } ++ } ++ } ++ ++ return; ++ } ++ // Paper end - optimise random ticking ++ + public void tickChunk(LevelChunk chunk, int randomTickSpeed) { + ChunkPos chunkcoordintpair = chunk.getPos(); + boolean flag = this.isRaining(); +@@ -675,35 +917,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + gameprofilerfiller.popPush("tickBlocks"); + timings.chunkTicksBlocks.startTiming(); // Paper + if (randomTickSpeed > 0) { +- LevelChunkSection[] achunksection = chunk.getSections(); +- +- for (int i1 = 0; i1 < achunksection.length; ++i1) { +- LevelChunkSection chunksection = achunksection[i1]; +- +- if (chunksection.isRandomlyTicking()) { +- int j1 = chunk.getSectionYFromSectionIndex(i1); +- int k1 = SectionPos.sectionToBlockCoord(j1); +- +- for (int l1 = 0; l1 < randomTickSpeed; ++l1) { +- BlockPos blockposition1 = this.getBlockRandomPos(j, k1, k, 15); +- +- gameprofilerfiller.push("randomTick"); +- BlockState iblockdata = chunksection.getBlockState(blockposition1.getX() - j, blockposition1.getY() - k1, blockposition1.getZ() - k); +- +- if (iblockdata.isRandomlyTicking()) { +- iblockdata.randomTick(this, blockposition1, this.random); +- } +- +- FluidState fluid = iblockdata.getFluidState(); +- +- if (fluid.isRandomlyTicking()) { +- fluid.randomTick(this, blockposition1, this.random); +- } +- +- gameprofilerfiller.pop(); +- } +- } +- } ++ this.optimiseRandomTick(chunk, randomTickSpeed); // Paper - optimise random ticking + } + + timings.chunkTicksBlocks.stopTiming(); // Paper +@@ -976,6 +1190,11 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (fluid1.is(fluid)) { + fluid1.tick(this, pos); + } ++ // Paper start - rewrite chunk system ++ if ((++this.tickedBlocksOrFluids & 7L) != 0L) { ++ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks(); ++ } ++ // Paper end - rewrite chunk system + + } + +@@ -985,6 +1204,11 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (iblockdata.is(block)) { + iblockdata.tick(this, pos, this.random); + } ++ // Paper start - rewrite chunk system ++ if ((++this.tickedBlocksOrFluids & 7L) != 0L) { ++ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks(); ++ } ++ // Paper end - rewrite chunk system + + } + +@@ -1061,6 +1285,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 +1305,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 +1452,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 +1483,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 +1494,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 +2080,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 +2129,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 +2150,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 +2292,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 +2322,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 +2360,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 +2426,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + CrashReportCategory crashreportsystemdetails = super.fillReportDetails(report); + + crashreportsystemdetails.setDetail("Loaded entity count", () -> { +- return String.valueOf(this.entityManager.count()); ++ return String.valueOf(this.moonrise$getEntityLookup().getEntityCount()); // Paper - rewrite chunk system + }); + return crashreportsystemdetails; + } +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 191dfbd0f15c3a21278f3c4f9ce29f1698e0836c..ba64e42a58b4b760815f54228ebf7a46fd14734e 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -199,7 +199,7 @@ import org.bukkit.event.player.PlayerToggleSneakEvent; + import org.bukkit.inventory.MainHand; + // CraftBukkit end + +-public class ServerPlayer extends net.minecraft.world.entity.player.Player { ++public class ServerPlayer extends net.minecraft.world.entity.player.Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system + + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32; +@@ -297,6 +297,36 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player { + public @Nullable String clientBrandName = null; // Paper - Brand support + public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event + ++ // Paper start - rewrite chunk system ++ private ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader; ++ private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder(); ++ ++ @Override ++ public final boolean moonrise$isRealPlayer() { ++ return this.isRealPlayer; ++ } ++ ++ @Override ++ public final void moonrise$setRealPlayer(final boolean real) { ++ this.isRealPlayer = real; ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData moonrise$getChunkLoader() { ++ return this.chunkLoader; ++ } ++ ++ @Override ++ public final void moonrise$setChunkLoader(final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData loader) { ++ this.chunkLoader = loader; ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() { ++ return this.viewDistanceHolder; ++ } ++ // Paper end - rewrite chunk system ++ + public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) { + super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile); + this.chatVisibility = ChatVisiblity.FULL; +diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +index 63fae619e9b4ed49585f88ea7c167b0ee5efd859..cc779de06773451d51f54040fc899e4f45110bc1 100644 +--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java ++++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +@@ -23,15 +23,128 @@ import net.minecraft.world.level.chunk.LightChunkGetter; + import net.minecraft.world.level.lighting.LevelLightEngine; + import org.slf4j.Logger; + +-public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable { ++public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable, ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider { // Paper - rewrite chunk system + public static final int DEFAULT_BATCH_SIZE = 1000; + private static final Logger LOGGER = LogUtils.getLogger(); +- private final ProcessorMailbox<Runnable> taskMailbox; +- private final ObjectList<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> lightTasks = new ObjectArrayList<>(); ++ // Paper - rewrite chunk sytem + private final ChunkMap chunkMap; +- private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> sorterMailbox; ++ // Paper - rewrite chunk sytem + private final int taskPerBatch = 1000; +- private final AtomicBoolean scheduled = new AtomicBoolean(); ++ // Paper - rewrite chunk sytem ++ ++ // Paper start - rewrite chunk system ++ private final java.util.concurrent.atomic.AtomicLong chunkWorkCounter = new java.util.concurrent.atomic.AtomicLong(); ++ private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, ++ final java.util.function.Supplier<ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.LightQueue.ChunkTasks> supplier) { ++ final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld(); ++ ++ final ChunkAccess center = this.starlight$getLightEngine().getAnyChunkNow(chunkX, chunkZ); ++ if (center == null || !center.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { ++ // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing ++ // chunk scheduling, we could be lighting and generating a chunk at the same time ++ return; ++ } ++ ++ final ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.ServerLightQueue.ServerChunkTasks scheduledTask = (ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.ServerLightQueue.ServerChunkTasks)supplier.get(); ++ ++ if (scheduledTask == null) { ++ // not scheduled ++ return; ++ } ++ ++ if (!scheduledTask.markTicketAdded()) { ++ // ticket already added ++ return; ++ } ++ ++ final Long ticketId = Long.valueOf(this.chunkWorkCounter.getAndIncrement()); ++ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); ++ world.getChunkSource().addRegionTicket(ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId); ++ ++ scheduledTask.queueOrRunTask(() -> { ++ world.getChunkSource().removeRegionTicket(ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, ticketId); ++ }); ++ } ++ ++ @Override ++ public final int starlight$serverRelightChunks(final java.util.Collection<net.minecraft.world.level.ChunkPos> chunks0, ++ final java.util.function.Consumer<net.minecraft.world.level.ChunkPos> chunkLightCallback, ++ final java.util.function.IntConsumer onComplete) { ++ final java.util.Set<net.minecraft.world.level.ChunkPos> chunks = new java.util.LinkedHashSet<>(chunks0); ++ final java.util.Map<net.minecraft.world.level.ChunkPos, Long> ticketIds = new java.util.HashMap<>(); ++ final ServerLevel world = (ServerLevel)this.starlight$getLightEngine().getWorld(); ++ ++ for (final java.util.Iterator<net.minecraft.world.level.ChunkPos> iterator = chunks.iterator(); iterator.hasNext();) { ++ final ChunkPos pos = iterator.next(); ++ ++ final Long id = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getNextChunkRelightId(); ++ world.getChunkSource().addRegionTicket(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, id); ++ ticketIds.put(pos, id); ++ ++ final ChunkAccess chunk = (ChunkAccess)world.getChunkSource().getChunkForLighting(pos.x, pos.z); ++ if (chunk == null || !chunk.isLightCorrect() || !chunk.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { ++ // cannot relight this chunk ++ iterator.remove(); ++ ticketIds.remove(pos); ++ world.getChunkSource().removeRegionTicket(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, pos, ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, id); ++ continue; ++ } ++ } ++ ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().radiusAwareScheduler.queueInfiniteRadiusTask(() -> { ++ ThreadedLevelLightEngine.this.starlight$getLightEngine().relightChunks( ++ chunks, ++ (final ChunkPos pos) -> { ++ if (chunkLightCallback != null) { ++ chunkLightCallback.accept(pos); ++ } ++ ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().scheduleChunkTask(pos.x, pos.z, () -> { ++ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder( ++ pos.x, pos.z ++ ); ++ ++ if (chunkHolder == null) { ++ return; ++ } ++ ++ final java.util.List<ServerPlayer> players = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)chunkHolder.vanillaChunkHolder).moonrise$getPlayers(false); ++ ++ if (players.isEmpty()) { ++ return; ++ } ++ ++ final net.minecraft.network.protocol.Packet<?> relightPacket = new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket( ++ pos, (ThreadedLevelLightEngine)(Object)ThreadedLevelLightEngine.this, ++ null, null ++ ); ++ ++ for (final ServerPlayer player : players) { ++ final net.minecraft.server.network.ServerGamePacketListenerImpl conn = player.connection; ++ if (conn != null) { ++ conn.send(relightPacket); ++ } ++ } ++ }); ++ }, ++ (final int relight) -> { ++ if (onComplete != null) { ++ onComplete.accept(relight); ++ } ++ ++ for (final java.util.Map.Entry<ChunkPos, Long> entry : ticketIds.entrySet()) { ++ world.getChunkSource().removeRegionTicket( ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_RELIGHT, entry.getKey(), ++ ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface.REGION_LIGHT_TICKET_LEVEL, entry.getValue() ++ ); ++ } ++ } ++ ); ++ }); ++ ++ return chunks.size(); ++ } ++ // Paper end - rewrite chunk system + + public ThreadedLevelLightEngine( + LightChunkGetter chunkProvider, +@@ -42,8 +155,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + ) { + super(chunkProvider, true, hasBlockLight); + this.chunkMap = chunkLoadingManager; +- this.sorterMailbox = executor; +- this.taskMailbox = processor; ++ // Paper - rewrite chunk sytem + } + + @Override +@@ -57,164 +169,73 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + + @Override + public void checkBlock(BlockPos pos) { +- BlockPos blockPos = pos.immutable(); +- this.addTask( +- SectionPos.blockToSectionCoord(pos.getX()), +- SectionPos.blockToSectionCoord(pos.getZ()), +- ThreadedLevelLightEngine.TaskType.PRE_UPDATE, +- Util.name(() -> super.checkBlock(blockPos), () -> "checkBlock " + blockPos) +- ); ++ // Paper start - rewrite chunk system ++ final BlockPos posCopy = pos.immutable(); ++ this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> { ++ return ThreadedLevelLightEngine.this.starlight$getLightEngine().blockChange(posCopy); ++ }); ++ // Paper end - rewrite chunk system + } + + protected void updateChunkStatus(ChunkPos pos) { +- this.addTask(pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { +- super.retainData(pos, false); +- super.setLightEnabled(pos, false); +- +- for (int i = this.getMinLightSection(); i < this.getMaxLightSection(); i++) { +- super.queueSectionData(LightLayer.BLOCK, SectionPos.of(pos, i), null); +- super.queueSectionData(LightLayer.SKY, SectionPos.of(pos, i), null); +- } +- +- for (int j = this.levelHeightAccessor.getMinSection(); j < this.levelHeightAccessor.getMaxSection(); j++) { +- super.updateSectionStatus(SectionPos.of(pos, j), true); +- } +- }, () -> "updateChunkStatus " + pos + " true")); ++ // Paper - rewrite chunk system + } + + @Override + public void updateSectionStatus(SectionPos pos, boolean notReady) { +- this.addTask( +- pos.x(), +- pos.z(), +- () -> 0, +- ThreadedLevelLightEngine.TaskType.PRE_UPDATE, +- Util.name(() -> super.updateSectionStatus(pos, notReady), () -> "updateSectionStatus " + pos + " " + notReady) +- ); ++ // Paper start - rewrite chunk system ++ this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> { ++ return ThreadedLevelLightEngine.this.starlight$getLightEngine().sectionChange(pos, notReady); ++ }); ++ // Paper end - rewrite chunk system + } + + @Override + public void propagateLightSources(ChunkPos chunkPos) { +- this.addTask( +- chunkPos.x, +- chunkPos.z, +- ThreadedLevelLightEngine.TaskType.PRE_UPDATE, +- Util.name(() -> super.propagateLightSources(chunkPos), () -> "propagateLight " + chunkPos) +- ); ++ // Paper - rewrite chunk system + } + + @Override + public void setLightEnabled(ChunkPos pos, boolean retainData) { +- this.addTask( +- pos.x, +- pos.z, +- ThreadedLevelLightEngine.TaskType.PRE_UPDATE, +- Util.name(() -> super.setLightEnabled(pos, retainData), () -> "enableLight " + pos + " " + retainData) +- ); ++ // Paper start - rewrite chunk system + } + + @Override + public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) { +- this.addTask( +- pos.x(), +- pos.z(), +- () -> 0, +- ThreadedLevelLightEngine.TaskType.PRE_UPDATE, +- Util.name(() -> super.queueSectionData(lightType, pos, nibbles), () -> "queueData " + pos) +- ); ++ // Paper start - rewrite chunk system + } + + private void addTask(int x, int z, ThreadedLevelLightEngine.TaskType stage, Runnable task) { +- this.addTask(x, z, this.chunkMap.getChunkQueueLevel(ChunkPos.asLong(x, z)), stage, task); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void addTask(int x, int z, IntSupplier completedLevelSupplier, ThreadedLevelLightEngine.TaskType stage, Runnable task) { +- this.sorterMailbox.tell(ChunkTaskPriorityQueueSorter.message(() -> { +- this.lightTasks.add(Pair.of(stage, task)); +- if (this.lightTasks.size() >= 1000) { +- this.runUpdate(); +- } +- }, ChunkPos.asLong(x, z), completedLevelSupplier)); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Override + public void retainData(ChunkPos pos, boolean retainData) { +- this.addTask( +- pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> super.retainData(pos, retainData), () -> "retainData " + pos) +- ); ++ // Paper start - rewrite chunk system + } + + public CompletableFuture<ChunkAccess> initializeLight(ChunkAccess chunk, boolean bl) { +- ChunkPos chunkPos = chunk.getPos(); +- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { +- LevelChunkSection[] levelChunkSections = chunk.getSections(); +- +- for (int i = 0; i < chunk.getSectionsCount(); i++) { +- LevelChunkSection levelChunkSection = levelChunkSections[i]; +- if (!levelChunkSection.hasOnlyAir()) { +- int j = this.levelHeightAccessor.getSectionYFromSectionIndex(i); +- super.updateSectionStatus(SectionPos.of(chunkPos, j), false); +- } +- } +- }, () -> "initializeLight: " + chunkPos)); +- return CompletableFuture.supplyAsync(() -> { +- super.setLightEnabled(chunkPos, bl); +- super.retainData(chunkPos, false); +- return chunk; +- }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task)); ++ return CompletableFuture.completedFuture(chunk); // Paper start - rewrite chunk system + } + + public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean excludeBlocks) { +- ChunkPos chunkPos = chunk.getPos(); +- chunk.setLightCorrect(false); +- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { +- if (!excludeBlocks) { +- super.propagateLightSources(chunkPos); +- } +- }, () -> "lightChunk " + chunkPos + " " + excludeBlocks)); +- return CompletableFuture.supplyAsync(() -> { +- chunk.setLightCorrect(true); +- return chunk; +- }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task)); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public void tryScheduleUpdate() { +- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { +- this.taskMailbox.tell(() -> { +- this.runUpdate(); +- this.scheduled.set(false); +- }); +- } ++ // Paper - rewrite chunk system + } + + private void runUpdate() { +- int i = Math.min(this.lightTasks.size(), 1000); +- ObjectListIterator<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> objectListIterator = this.lightTasks.iterator(); +- +- int j; +- for (j = 0; objectListIterator.hasNext() && j < i; j++) { +- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair = objectListIterator.next(); +- if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.PRE_UPDATE) { +- pair.getSecond().run(); +- } +- } +- +- objectListIterator.back(j); +- super.runLightUpdates(); +- +- for (int var5 = 0; objectListIterator.hasNext() && var5 < i; var5++) { +- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair2 = objectListIterator.next(); +- if (pair2.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) { +- pair2.getSecond().run(); +- } +- +- objectListIterator.remove(); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public CompletableFuture<?> waitForPendingTasks(int x, int z) { +- return CompletableFuture.runAsync(() -> { +- }, callback -> this.addTask(x, z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, callback)); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + static enum TaskType { +diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java +index eba83b085435150e5954fd5d41dda9ce1d0601ad..daf543b51d8875b374688957ae4bc466f5512bcd 100644 +--- a/src/main/java/net/minecraft/server/level/Ticket.java ++++ b/src/main/java/net/minecraft/server/level/Ticket.java +@@ -2,13 +2,25 @@ package net.minecraft.server.level; + + import java.util.Objects; + +-public final class Ticket<T> implements Comparable<Ticket<?>> { ++public final class Ticket<T> implements Comparable<Ticket<?>>, ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicket<T> { // Paper - rewrite chunk system + private final TicketType<T> type; + private final int ticketLevel; + public final T key; +- private long createdTick; ++ // Paper start - rewrite chunk system ++ private long removeDelay; + +- protected Ticket(TicketType<T> type, int level, T argument) { ++ @Override ++ public final long moonrise$getRemoveDelay() { ++ return this.removeDelay; ++ } ++ ++ @Override ++ public final void moonrise$setRemoveDelay(final long removeDelay) { ++ this.removeDelay = removeDelay; ++ } ++ // Paper end - rewerite chunk system ++ ++ public Ticket(TicketType<T> type, int level, T argument) { // Paper - public + this.type = type; + this.ticketLevel = level; + this.key = argument; +@@ -41,7 +53,7 @@ public final class Ticket<T> implements Comparable<Ticket<?>> { + + @Override + public String toString() { +- return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] at " + this.createdTick; ++ return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] to die in " + this.removeDelay; // Paper - rewrite chunk system + } + + public TicketType<T> getType() { +@@ -53,11 +65,10 @@ public final class Ticket<T> implements Comparable<Ticket<?>> { + } + + protected void setCreatedTick(long tickCreated) { +- this.createdTick = tickCreated; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + protected boolean timedOut(long currentTick) { +- long l = this.type.timeout(); +- return l != 0L && currentTick - this.createdTick > l; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + } +diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +index b26a4a38144ec1b171db911bbf949b53ed35708f..5a8a33638ceb1d980ffc3e6dd86e7eb11dfd9375 100644 +--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java ++++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +@@ -85,6 +85,36 @@ public class WorldGenRegion implements WorldGenLevel { + private final AtomicLong subTickCount = new AtomicLong(); + private static final ResourceLocation WORLDGEN_REGION_RANDOM = ResourceLocation.withDefaultNamespace("worldgen_region_random"); + ++ // Paper start - rewrite chunk system ++ /** ++ * During feature generation, light data is not initialised and will always return 15 in Starlight. Vanilla ++ * can possibly return 0 if partially initialised, which allows some mushroom blocks to generate. ++ * In general, the brightness value from the light engine should not be used until the chunk is ready. To emulate ++ * Vanilla behavior better, we return 0 as the brightness during world gen unless the target chunk is finished ++ * lighting. ++ */ ++ @Override ++ public int getBrightness(final net.minecraft.world.level.LightLayer lightLayer, final BlockPos blockPos) { ++ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4); ++ if (!chunk.isLightCorrect()) { ++ return 0; ++ } ++ return this.getLightEngine().getLayerListener(lightLayer).getLightValue(blockPos); ++ } ++ ++ /** ++ * See above ++ */ ++ @Override ++ public int getRawBrightness(final BlockPos blockPos, final int subtract) { ++ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4); ++ if (!chunk.isLightCorrect()) { ++ return 0; ++ } ++ return this.getLightEngine().getRawBrightness(blockPos, subtract); ++ } ++ // Paper end - rewrite chunk system ++ + public WorldGenRegion(ServerLevel world, StaticCache2D<GenerationChunkHolder> chunks, ChunkStep generationStep, ChunkAccess centerPos) { + this.generatingStep = generationStep; + this.cache = chunks; +diff --git a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java +index cdd66e6ce96e2613afe7f06ca8da3cfaa6704b2d..32634e45ac8433648e49e47e20081e15ad41ff15 100644 +--- a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java ++++ b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java +@@ -78,7 +78,7 @@ public class PlayerChunkSender { + } + } + +- private static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) { ++ public static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) { // Paper - public + handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.getLightEngine(), null, null)); + // Paper start - PlayerChunkLoadEvent + if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) { +diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java +index 68648c5a5e3ff079f832092af0f2f801c42d1ede..19661e106612b8e4e152085fb398db7bd06acc23 100644 +--- a/src/main/java/net/minecraft/util/BitStorage.java ++++ b/src/main/java/net/minecraft/util/BitStorage.java +@@ -2,7 +2,7 @@ package net.minecraft.util; + + import java.util.function.IntConsumer; + +-public interface BitStorage { ++public interface BitStorage extends ca.spottedleaf.moonrise.patches.block_counting.BlockCountingBitStorage { // Paper - block counting + int getAndSet(int index, int value); + + void set(int index, int value); +@@ -20,4 +20,22 @@ public interface BitStorage { + void unpack(int[] out); + + BitStorage copy(); ++ ++ // Paper start - block counting ++ // provide default impl in case mods implement this... ++ @Override ++ public default it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> moonrise$countEntries() { ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(); ++ ++ final int size = this.getSize(); ++ for (int index = 0; index < size; ++index) { ++ final int paletteIdx = this.get(index); ++ ret.computeIfAbsent(paletteIdx, (final int key) -> { ++ return new it.unimi.dsi.fastutil.ints.IntArrayList(); ++ }).add(index); ++ } ++ ++ return ret; ++ } ++ // Paper end - block counting + } +diff --git a/src/main/java/net/minecraft/util/SimpleBitStorage.java b/src/main/java/net/minecraft/util/SimpleBitStorage.java +index 9f438d9c6eb05e43d24e4af68188a3d4c46a938c..8acf2f2491a8d9d13392c5e89b2bd5c9918285e1 100644 +--- a/src/main/java/net/minecraft/util/SimpleBitStorage.java ++++ b/src/main/java/net/minecraft/util/SimpleBitStorage.java +@@ -362,6 +362,40 @@ public class SimpleBitStorage implements BitStorage { + return new SimpleBitStorage(this.bits, this.size, (long[])this.data.clone()); + } + ++ // Paper start - block counting ++ @Override ++ public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> moonrise$countEntries() { ++ final int valuesPerLong = this.valuesPerLong; ++ final int bits = this.bits; ++ final long mask = this.mask; ++ final int size = this.size; ++ ++ // we may be backed by global palette, so limit bits for init capacity ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>( ++ 1 << Math.min(6, bits) ++ ); ++ ++ int index = 0; ++ ++ for (long value : this.data) { ++ int li = 0; ++ do { ++ final int paletteIdx = (int)(value & mask); ++ value >>= bits; ++ ++ ret.computeIfAbsent(paletteIdx, (final int key) -> { ++ return new it.unimi.dsi.fastutil.ints.IntArrayList(); ++ }).add(index); ++ ++ ++li; ++ ++index; ++ } while (li < valuesPerLong && index < size); ++ } ++ ++ return ret; ++ } ++ // Paper end - block counting ++ + public static class InitializationException extends RuntimeException { + InitializationException(String message) { + super(message); +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/util/ZeroBitStorage.java b/src/main/java/net/minecraft/util/ZeroBitStorage.java +index 50040c497a819cd1229042ab3cb057d34a32cacc..15c5164d0ef41a978c16ee317fa73e97f2480207 100644 +--- a/src/main/java/net/minecraft/util/ZeroBitStorage.java ++++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java +@@ -62,4 +62,22 @@ public class ZeroBitStorage implements BitStorage { + public BitStorage copy() { + return this; + } ++ ++ // Paper start - block counting ++ @Override ++ public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> moonrise$countEntries() { ++ final int size = this.size; ++ ++ final int[] raw = new int[size]; ++ for (int i = 0; i < size; ++i) { ++ raw[i] = i; ++ } ++ ++ final it.unimi.dsi.fastutil.ints.IntArrayList coordinates = it.unimi.dsi.fastutil.ints.IntArrayList.wrap(raw, size); ++ ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1); ++ ret.put(0, coordinates); ++ return ret; ++ } ++ // Paper end - block counting + } +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 2e2101274f3afebbae783fa119f5cae8104de45d..4f4b7e738fe604808d837a38d23bf437bc1d5329 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, ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity { // Paper - rewrite chunk system // Paper - optimise entity tracker + + // CraftBukkit start + private static final int CURRENT_LEVEL = 2; +@@ -456,6 +456,97 @@ 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 ++ // Paper start - optimise entity tracker ++ private net.minecraft.server.level.ChunkMap.TrackedEntity trackedEntity; ++ ++ @Override ++ public final net.minecraft.server.level.ChunkMap.TrackedEntity moonrise$getTrackedEntity() { ++ return this.trackedEntity; ++ } ++ ++ @Override ++ public final void moonrise$setTrackedEntity(final net.minecraft.server.level.ChunkMap.TrackedEntity trackedEntity) { ++ this.trackedEntity = trackedEntity; ++ } ++ ++ private static void collectIndirectPassengers(final List<Entity> into, final List<Entity> from) { ++ for (final Entity passenger : from) { ++ into.add(passenger); ++ collectIndirectPassengers(into, ((Entity)(Object)passenger).passengers); ++ } ++ } ++ // Paper end - optimise entity tracker + + public Entity(EntityType<?> type, Level world) { + this.id = Entity.ENTITY_COUNTER.incrementAndGet(); +@@ -1279,41 +1370,82 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + private Vec3 collide(Vec3 movement) { +- AABB axisalignedbb = this.getBoundingBox(); +- List<VoxelShape> list = this.level().getEntityCollisions(this, axisalignedbb.expandTowards(movement)); +- Vec3 vec3d1 = movement.lengthSqr() == 0.0D ? movement : Entity.collideBoundingBox(this, movement, axisalignedbb, this.level(), list); +- boolean flag = movement.x != vec3d1.x; +- boolean flag1 = movement.y != vec3d1.y; +- boolean flag2 = movement.z != vec3d1.z; +- boolean flag3 = flag1 && movement.y < 0.0D; +- +- if (this.maxUpStep() > 0.0F && (flag3 || this.onGround()) && (flag || flag2)) { +- AABB axisalignedbb1 = flag3 ? axisalignedbb.move(0.0D, vec3d1.y, 0.0D) : axisalignedbb; +- AABB axisalignedbb2 = axisalignedbb1.expandTowards(movement.x, (double) this.maxUpStep(), movement.z); +- +- if (!flag3) { +- axisalignedbb2 = axisalignedbb2.expandTowards(0.0D, -9.999999747378752E-6D, 0.0D); +- } ++ // Paper start - optimise collisions ++ final boolean xZero = movement.x == 0.0; ++ final boolean yZero = movement.y == 0.0; ++ final boolean zZero = movement.z == 0.0; ++ if (xZero & yZero & zZero) { ++ return movement; ++ } + +- List<VoxelShape> list1 = Entity.collectColliders(this, this.level, list, axisalignedbb2); +- float f = (float) vec3d1.y; +- float[] afloat = Entity.collectCandidateStepUpHeights(axisalignedbb1, list1, this.maxUpStep(), f); +- float[] afloat1 = afloat; +- int i = afloat.length; ++ final Level world = this.level; ++ final AABB currBoundingBox = this.getBoundingBox(); + +- for (int j = 0; j < i; ++j) { +- float f1 = afloat1[j]; +- Vec3 vec3d2 = Entity.collideWithShapes(new Vec3(movement.x, (double) f1, movement.z), axisalignedbb1, list1); ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(currBoundingBox)) { ++ return movement; ++ } + +- if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) { +- double d0 = axisalignedbb.minY - axisalignedbb1.minY; ++ final List<AABB> potentialCollisionsBB = new ArrayList<>(); ++ final List<VoxelShape> potentialCollisionsVoxel = new ArrayList<>(); ++ final double stepHeight = (double)this.maxUpStep(); ++ final AABB collisionBox; ++ final boolean onGround = this.onGround; + +- return vec3d2.add(0.0D, -d0, 0.0D); ++ if (xZero & zZero) { ++ if (movement.y > 0.0) { ++ collisionBox = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutUpwards(currBoundingBox, movement.y); ++ } else { ++ collisionBox = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutDownwards(currBoundingBox, movement.y); ++ } ++ } else { ++ // note: xZero == false or zZero == false ++ if (stepHeight > 0.0 && (onGround || (movement.y < 0.0))) { ++ // don't bother getting the collisions if we don't need them. ++ if (movement.y <= 0.0) { ++ collisionBox = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.expandUpwards(currBoundingBox.expandTowards(movement.x, movement.y, movement.z), stepHeight); ++ } else { ++ collisionBox = currBoundingBox.expandTowards(movement.x, Math.max(stepHeight, movement.y), movement.z); + } ++ } else { ++ collisionBox = currBoundingBox.expandTowards(movement.x, movement.y, movement.z); + } + } + +- return vec3d1; ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisions( ++ world, (Entity)(Object)this, collisionBox, potentialCollisionsVoxel, potentialCollisionsBB, ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, ++ null, null ++ ); ++ ++ if (potentialCollisionsVoxel.isEmpty() && potentialCollisionsBB.isEmpty()) { ++ return movement; ++ } ++ ++ final Vec3 limitedMoveVector = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(movement, currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB); ++ ++ if (stepHeight > 0.0 ++ && (onGround || (limitedMoveVector.y != movement.y && movement.y < 0.0)) ++ && (limitedMoveVector.x != movement.x || limitedMoveVector.z != movement.z)) { ++ Vec3 vec3d2 = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(movement.x, stepHeight, movement.z), currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB); ++ final Vec3 vec3d3 = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(0.0, stepHeight, 0.0), currBoundingBox.expandTowards(movement.x, 0.0, movement.z), potentialCollisionsVoxel, potentialCollisionsBB); ++ ++ if (vec3d3.y < stepHeight) { ++ final Vec3 vec3d4 = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(movement.x, 0.0D, movement.z), currBoundingBox.move(vec3d3), potentialCollisionsVoxel, potentialCollisionsBB).add(vec3d3); ++ ++ if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) { ++ vec3d2 = vec3d4; ++ } ++ } ++ ++ if (vec3d2.horizontalDistanceSqr() > limitedMoveVector.horizontalDistanceSqr()) { ++ return vec3d2.add(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), currBoundingBox.move(vec3d2), potentialCollisionsVoxel, potentialCollisionsBB)); ++ } ++ ++ return limitedMoveVector; ++ } else { ++ return limitedMoveVector; ++ } ++ // Paper end - optimise collisions + } + + private static float[] collectCandidateStepUpHeights(AABB collisionBox, List<VoxelShape> collisions, float f, float stepHeight) { +@@ -2629,18 +2761,75 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + public boolean isInWall() { ++ // Paper start - optimise collisions + if (this.noPhysics) { + return false; +- } else { +- float f = this.dimensions.width() * 0.8F; +- AABB axisalignedbb = AABB.ofSize(this.getEyePosition(), (double) f, 1.0E-6D, (double) f); ++ } + +- return BlockPos.betweenClosedStream(axisalignedbb).anyMatch((blockposition) -> { +- BlockState iblockdata = this.level().getBlockState(blockposition); ++ final float reducedWith = this.dimensions.width() * 0.8F; ++ final AABB box = AABB.ofSize(this.getEyePosition(), reducedWith, 1.0E-6D, reducedWith); ++ ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(box)) { ++ return false; ++ } + +- return !iblockdata.isAir() && iblockdata.isSuffocating(this.level(), blockposition) && Shapes.joinIsNotEmpty(iblockdata.getCollisionShape(this.level(), blockposition).move((double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ()), Shapes.create(axisalignedbb), BooleanOp.AND); +- }); ++ final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos(); ++ ++ final int minX = Mth.floor(box.minX); ++ final int minY = Mth.floor(box.minY); ++ final int minZ = Mth.floor(box.minZ); ++ final int maxX = Mth.floor(box.maxX); ++ final int maxY = Mth.floor(box.maxY); ++ final int maxZ = Mth.floor(box.maxZ); ++ ++ final net.minecraft.world.level.chunk.ChunkSource chunkProvider = this.level.getChunkSource(); ++ ++ long lastChunkKey = ChunkPos.INVALID_CHUNK_POS; ++ net.minecraft.world.level.chunk.LevelChunk lastChunk = null; ++ for (int fz = minZ; fz <= maxZ; ++fz) { ++ tempPos.setZ(fz); ++ for (int fx = minX; fx <= maxX; ++fx) { ++ final int newChunkX = fx >> 4; ++ final int newChunkZ = fz >> 4; ++ final net.minecraft.world.level.chunk.LevelChunk chunk = lastChunkKey == (lastChunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(newChunkX, newChunkZ)) ? ++ lastChunk : (lastChunk = (net.minecraft.world.level.chunk.LevelChunk)chunkProvider.getChunk(newChunkX, newChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true)); ++ tempPos.setX(fx); ++ for (int fy = minY; fy <= maxY; ++fy) { ++ tempPos.setY(fy); ++ ++ final BlockState state = chunk.getBlockState(tempPos); ++ ++ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)state).moonrise$emptyCollisionShape() || !state.isSuffocating(this.level, tempPos)) { ++ continue; ++ } ++ ++ // Yes, it does not use the Entity context stuff. ++ final VoxelShape collisionShape = state.getCollisionShape(this.level, tempPos); ++ ++ if (collisionShape.isEmpty()) { ++ continue; ++ } ++ ++ final AABB toCollide = box.move(-(double)fx, -(double)fy, -(double)fz); ++ ++ final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)collisionShape).moonrise$getSingleAABBRepresentation(); ++ if (singleAABB != null) { ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(singleAABB, toCollide)) { ++ return true; ++ } ++ continue; ++ } ++ ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, toCollide)) { ++ return true; ++ } ++ continue; ++ } ++ } + } ++ ++ return false; ++ // Paper end - optimise collisions + } + + public InteractionResult interact(Player player, InteractionHand hand) { +@@ -4025,14 +4214,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + public Iterable<Entity> getIndirectPassengers() { +- // Paper start - Optimize indirect passenger iteration +- if (this.passengers.isEmpty()) { return ImmutableList.of(); } +- ImmutableList.Builder<Entity> indirectPassengers = ImmutableList.builder(); +- for (Entity passenger : this.passengers) { +- indirectPassengers.add(passenger); +- indirectPassengers.addAll(passenger.getIndirectPassengers()); ++ // Paper start - optimise entity tracker ++ final List<Entity> ret = new ArrayList<>(); ++ ++ if (this.passengers.isEmpty()) { ++ return ret; + } +- return indirectPassengers.build(); ++ ++ collectIndirectPassengers(ret, this.passengers); ++ ++ return ret; ++ // Paper end - optimise entity tracker + } + private Iterable<Entity> getIndirectPassengers_old() { + // Paper end - Optimize indirect passenger iteration +@@ -4397,6 +4589,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.setPosRaw(x, y, z, false); + } + public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) { ++ // Paper start - rewrite chunk system ++ if (this.updatingSectionStatus) { ++ LOGGER.error( ++ "Refusing to update position for entity " + this + " to position " + new Vec3(x, y, z) ++ + " since it is processing a section status update", new Throwable() ++ ); ++ return; ++ } ++ // Paper end - rewrite chunk system + if (!checkPosition(this, x, y, z)) { + return; + } +@@ -4528,6 +4729,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + @Override + public final void setRemoved(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) { ++ // Paper start - rewrite chunk system ++ if (!((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this.level).moonrise$getEntityLookup().canRemoveEntity((Entity)(Object)this)) { ++ LOGGER.warn("Entity " + this + " is currently prevented from being removed from the world since it is processing section status updates", new Throwable()); ++ return; ++ } ++ // Paper end - rewrite chunk system + CraftEventFactory.callEntityRemoveEvent(this, cause); + // CraftBukkit end + final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers +@@ -4539,7 +4746,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.stopRiding(); + } + +- this.getPassengers().forEach(Entity::stopRiding); ++ if (this.removalReason != Entity.RemovalReason.UNLOADED_TO_CHUNK) { this.getPassengers().forEach(Entity::stopRiding); } // Paper - rewrite chunk system + this.levelCallback.onRemove(entity_removalreason); + // Paper start - Folia schedulers + if (!(this instanceof ServerPlayer) && entity_removalreason != RemovalReason.CHANGED_DIMENSION && !alreadyRemoved) { +@@ -4570,7 +4777,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/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java +index 2f398750bfee5758ad8b1367b6fc14364e4de776..c292f58ba4b29395484dbbf8591e455f449581d8 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java +@@ -359,7 +359,7 @@ public class ArmorStand extends LivingEntity { + @Override + protected void pushEntities() { + if (!this.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return; // Paper - Option to prevent armor stands from doing entity lookups +- List<Entity> list = this.level().getEntities((Entity) this, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS); ++ List<AbstractMinecart> list = this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), RIDABLE_MINECARTS); // Paper - optimise collisions + Iterator iterator = list.iterator(); + + while (iterator.hasNext()) { +diff --git a/src/main/java/net/minecraft/world/level/ClipContext.java b/src/main/java/net/minecraft/world/level/ClipContext.java +index 3fa2964b979053ecbefc946c7fe76828de86d8f1..28bf0518f7d17099d7e4990defbeda6757b4477c 100644 +--- a/src/main/java/net/minecraft/world/level/ClipContext.java ++++ b/src/main/java/net/minecraft/world/level/ClipContext.java +@@ -18,7 +18,7 @@ public class ClipContext { + private final Vec3 from; + private final Vec3 to; + private final ClipContext.Block block; +- private final ClipContext.Fluid fluid; ++ public final ClipContext.Fluid fluid; // Paper - optimise collisions - public + private final CollisionContext collisionContext; + + public ClipContext(Vec3 start, Vec3 end, ClipContext.Block shapeType, ClipContext.Fluid fluidHandling, Entity entity) { +diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java +index bd20bea7f76a7307f1698fb2dfef37125032d166..141b748abe80402731cdaf14a3d36aa7cef4f4bd 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,21 +33,44 @@ public interface EntityGetter { + return this.getEntities(except, box, EntitySelector.NO_SPECTATORS); + } + +- default boolean isUnobstructed(@Nullable Entity except, VoxelShape shape) { +- if (shape.isEmpty()) { +- return true; +- } else { +- for (Entity entity : this.getEntities(except, shape.bounds())) { +- if (!entity.isRemoved() +- && entity.blocksBuilding +- && (except == null || !entity.isPassengerOfSameVehicle(except)) +- && Shapes.joinIsNotEmpty(shape, Shapes.create(entity.getBoundingBox()), BooleanOp.AND)) { +- return false; ++ // 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 ++ ++ // Paper start - optimise collisions ++ default boolean isUnobstructed(@Nullable Entity entity, VoxelShape voxel) { ++ if (voxel.isEmpty()) { ++ return false; ++ } ++ ++ final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation(); ++ final List<Entity> entities = this.getEntities( ++ entity, ++ singleAABB == null ? voxel.bounds() : singleAABB.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) ++ ); ++ ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ final Entity otherEntity = entities.get(i); ++ ++ if (otherEntity.isRemoved() || !otherEntity.blocksBuilding || (entity != null && otherEntity.isPassengerOfSameVehicle(entity))) { ++ continue; ++ } ++ ++ if (singleAABB == null) { ++ final AABB entityBB = otherEntity.getBoundingBox(); ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(entityBB) || !ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(voxel, entityBB)) { ++ continue; + } + } + +- return true; ++ return false; + } ++ ++ return true; ++ // Paper end - optimise collisions + } + + default <T extends Entity> List<T> getEntitiesOfClass(Class<T> entityClass, AABB box) { +@@ -55,23 +78,41 @@ public interface EntityGetter { + } + + default List<VoxelShape> getEntityCollisions(@Nullable Entity entity, AABB box) { +- if (box.getSize() < 1.0E-7) { +- return List.of(); ++ // Paper start - optimise collisions ++ // first behavior change is to correctly check for empty AABB ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(box)) { ++ // reduce indirection by always returning type with same class ++ return new java.util.ArrayList<>(); ++ } ++ ++ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with. ++ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems ++ // specifically with boat collisions. ++ box = box.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON); ++ ++ final List<Entity> entities; ++ if (entity != null && ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$isHardColliding()) { ++ entities = this.getEntities(entity, box, null); + } else { +- Predicate<Entity> predicate = entity == null ? EntitySelector.CAN_BE_COLLIDED_WITH : EntitySelector.NO_SPECTATORS.and(entity::canCollideWith); +- List<Entity> list = this.getEntities(entity, box.inflate(1.0E-7), predicate); +- if (list.isEmpty()) { +- return List.of(); +- } else { +- Builder<VoxelShape> builder = ImmutableList.builderWithExpectedSize(list.size()); +- +- for (Entity entity2 : list) { +- builder.add(Shapes.create(entity2.getBoundingBox())); +- } ++ entities = ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter)this).moonrise$getHardCollidingEntities(entity, box, null); ++ } + +- return builder.build(); ++ final List<VoxelShape> ret = new java.util.ArrayList<>(Math.min(25, entities.size())); ++ ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ final Entity otherEntity = entities.get(i); ++ ++ if (otherEntity.isSpectator()) { ++ continue; ++ } ++ ++ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) { ++ ret.add(Shapes.create(otherEntity.getBoundingBox())); + } + } ++ ++ return ret; ++ // Paper end - optimise collisions + } + + // Paper start - Affects Spawning API +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index bff83fe413c7baef4ba56a3270ea4463a58c792f..a248d859cbce48f4a34c4771a7acffc17d7edc84 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -75,6 +75,247 @@ public class Explosion { + public float yield; + // CraftBukkit end + ++ // Paper start - optimise collisions ++ private static final double[] CACHED_RAYS; ++ static { ++ final it.unimi.dsi.fastutil.doubles.DoubleArrayList rayCoords = new it.unimi.dsi.fastutil.doubles.DoubleArrayList(); ++ ++ for (int x = 0; x <= 15; ++x) { ++ for (int y = 0; y <= 15; ++y) { ++ for (int z = 0; z <= 15; ++z) { ++ if ((x == 0 || x == 15) || (y == 0 || y == 15) || (z == 0 || z == 15)) { ++ double xDir = (double)((float)x / 15.0F * 2.0F - 1.0F); ++ double yDir = (double)((float)y / 15.0F * 2.0F - 1.0F); ++ double zDir = (double)((float)z / 15.0F * 2.0F - 1.0F); ++ ++ double mag = Math.sqrt( ++ xDir * xDir + yDir * yDir + zDir * zDir ++ ); ++ ++ rayCoords.add((xDir / mag) * (double)0.3F); ++ rayCoords.add((yDir / mag) * (double)0.3F); ++ rayCoords.add((zDir / mag) * (double)0.3F); ++ } ++ } ++ } ++ } ++ ++ CACHED_RAYS = rayCoords.toDoubleArray(); ++ } ++ ++ private static final int CHUNK_CACHE_SHIFT = 2; ++ private static final int CHUNK_CACHE_MASK = (1 << CHUNK_CACHE_SHIFT) - 1; ++ private static final int CHUNK_CACHE_WIDTH = 1 << CHUNK_CACHE_SHIFT; ++ ++ private static final int BLOCK_EXPLOSION_CACHE_SHIFT = 3; ++ private static final int BLOCK_EXPLOSION_CACHE_MASK = (1 << BLOCK_EXPLOSION_CACHE_SHIFT) - 1; ++ private static final int BLOCK_EXPLOSION_CACHE_WIDTH = 1 << BLOCK_EXPLOSION_CACHE_SHIFT; ++ ++ // resistance = (res + 0.3F) * 0.3F; ++ // so for resistance = 0, we need res = -0.3F ++ private static final Float ZERO_RESISTANCE = Float.valueOf(-0.3f); ++ private it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache> blockCache = null; ++ private long[] chunkPosCache = null; ++ private net.minecraft.world.level.chunk.LevelChunk[] chunkCache = null; ++ private ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache getOrCacheExplosionBlock(final int x, final int y, final int z, ++ final long key, final boolean calculateResistance) { ++ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache ret = this.blockCache.get(key); ++ if (ret != null) { ++ return ret; ++ } ++ ++ BlockPos pos = new BlockPos(x, y, z); ++ ++ if (!this.level.isInWorldBounds(pos)) { ++ ret = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache(key, pos, null, null, 0.0f, true); ++ } else { ++ net.minecraft.world.level.chunk.LevelChunk chunk; ++ long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(x >> 4, z >> 4); ++ int chunkCacheKey = ((x >> 4) & CHUNK_CACHE_MASK) | (((z >> 4) << CHUNK_CACHE_SHIFT) & (CHUNK_CACHE_MASK << CHUNK_CACHE_SHIFT)); ++ if (this.chunkPosCache[chunkCacheKey] == chunkKey) { ++ chunk = this.chunkCache[chunkCacheKey]; ++ } else { ++ this.chunkPosCache[chunkCacheKey] = chunkKey; ++ this.chunkCache[chunkCacheKey] = chunk = this.level.getChunk(x >> 4, z >> 4); ++ } ++ ++ BlockState blockState = ((ca.spottedleaf.moonrise.patches.chunk_getblock.GetBlockChunk)chunk).moonrise$getBlock(x, y, z); ++ FluidState fluidState = blockState.getFluidState(); ++ ++ Optional<Float> resistance = !calculateResistance ? Optional.empty() : this.damageCalculator.getBlockExplosionResistance((Explosion)(Object)this, this.level, pos, blockState, fluidState); ++ ++ ret = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache( ++ key, pos, blockState, fluidState, ++ (resistance.orElse(ZERO_RESISTANCE).floatValue() + 0.3f) * 0.3f, ++ false ++ ); ++ } ++ ++ this.blockCache.put(key, ret); ++ ++ return ret; ++ } ++ ++ private boolean clipsAnything(final Vec3 from, final Vec3 to, ++ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext context, ++ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache, ++ final BlockPos.MutableBlockPos currPos) { ++ // assume that context.delegated = false ++ final double adjX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.x - to.x); ++ final double adjY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.y - to.y); ++ final double adjZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.z - to.z); ++ ++ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) { ++ return false; ++ } ++ ++ final double toXAdj = to.x - adjX; ++ final double toYAdj = to.y - adjY; ++ final double toZAdj = to.z - adjZ; ++ final double fromXAdj = from.x + adjX; ++ final double fromYAdj = from.y + adjY; ++ final double fromZAdj = from.z + adjZ; ++ ++ int currX = Mth.floor(fromXAdj); ++ int currY = Mth.floor(fromYAdj); ++ int currZ = Mth.floor(fromZAdj); ++ ++ final double diffX = toXAdj - fromXAdj; ++ final double diffY = toYAdj - fromYAdj; ++ final double diffZ = toZAdj - fromZAdj; ++ ++ final double dxDouble = Math.signum(diffX); ++ final double dyDouble = Math.signum(diffY); ++ final double dzDouble = Math.signum(diffZ); ++ ++ final int dx = (int)dxDouble; ++ final int dy = (int)dyDouble; ++ final int dz = (int)dzDouble; ++ ++ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX; ++ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY; ++ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ; ++ ++ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj)); ++ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj)); ++ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj)); ++ ++ for (;;) { ++ currPos.set(currX, currY, currZ); ++ ++ // ClipContext.Block.COLLIDER -> BlockBehaviour.BlockStateBase::getCollisionShape ++ // ClipContext.Fluid.NONE -> ignore fluids ++ ++ // read block from cache ++ final long key = BlockPos.asLong(currX, currY, currZ); ++ ++ final int cacheKey = ++ (currX & BLOCK_EXPLOSION_CACHE_MASK) | ++ (currY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | ++ (currZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); ++ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache cachedBlock = blockCache[cacheKey]; ++ if (cachedBlock == null || cachedBlock.key != key) { ++ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(currX, currY, currZ, key, false); ++ } ++ ++ final BlockState blockState = cachedBlock.blockState; ++ if (blockState != null && !((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$emptyCollisionShape()) { ++ net.minecraft.world.phys.shapes.VoxelShape collision = cachedBlock.cachedCollisionShape; ++ if (collision == null) { ++ collision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockState).moonrise$getConstantCollisionShape(); ++ if (collision == null) { ++ collision = blockState.getCollisionShape(this.level, currPos, context); ++ if (!context.isDelegated()) { ++ // if it was not delegated during this call, assume that for any future ones it will not be delegated ++ // again, and cache the result ++ cachedBlock.cachedCollisionShape = collision; ++ } ++ } else { ++ cachedBlock.cachedCollisionShape = collision; ++ } ++ } ++ ++ if (!collision.isEmpty() && collision.clip(from, to, currPos) != null) { ++ return true; ++ } ++ } ++ ++ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { ++ return false; ++ } ++ ++ // inc the smallest normalized coordinate ++ ++ if (normalizedCurrX < normalizedCurrY) { ++ if (normalizedCurrX < normalizedCurrZ) { ++ currX += dx; ++ normalizedCurrX += normalizedDiffX; ++ } else { ++ // x < y && x >= z <--> z < y && z <= x ++ currZ += dz; ++ normalizedCurrZ += normalizedDiffZ; ++ } ++ } else if (normalizedCurrY < normalizedCurrZ) { ++ // y <= x && y < z ++ currY += dy; ++ normalizedCurrY += normalizedDiffY; ++ } else { ++ // y <= x && z <= y <--> z <= y && z <= x ++ currZ += dz; ++ normalizedCurrZ += normalizedDiffZ; ++ } ++ } ++ } ++ ++ private float getSeenFraction(final Vec3 source, final Entity target, ++ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache, ++ final BlockPos.MutableBlockPos blockPos) { ++ final AABB boundingBox = target.getBoundingBox(); ++ final double diffX = boundingBox.maxX - boundingBox.minX; ++ final double diffY = boundingBox.maxY - boundingBox.minY; ++ final double diffZ = boundingBox.maxZ - boundingBox.minZ; ++ ++ final double incX = 1.0 / (diffX * 2.0 + 1.0); ++ final double incY = 1.0 / (diffY * 2.0 + 1.0); ++ final double incZ = 1.0 / (diffZ * 2.0 + 1.0); ++ ++ if (incX < 0.0 || incY < 0.0 || incZ < 0.0) { ++ return 0.0f; ++ } ++ ++ final double offX = (1.0 - Math.floor(1.0 / incX) * incX) * 0.5 + boundingBox.minX; ++ final double offY = boundingBox.minY; ++ final double offZ = (1.0 - Math.floor(1.0 / incZ) * incZ) * 0.5 + boundingBox.minZ; ++ ++ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext context = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(target); ++ ++ int totalRays = 0; ++ int missedRays = 0; ++ ++ for (double dx = 0.0; dx <= 1.0; dx += incX) { ++ final double fromX = Math.fma(dx, diffX, offX); ++ for (double dy = 0.0; dy <= 1.0; dy += incY) { ++ final double fromY = Math.fma(dy, diffY, offY); ++ for (double dz = 0.0; dz <= 1.0; dz += incZ) { ++ ++totalRays; ++ ++ final Vec3 from = new Vec3( ++ fromX, ++ fromY, ++ Math.fma(dz, diffZ, offZ) ++ ); ++ ++ if (!this.clipsAnything(from, source, context, blockCache, blockPos)) { ++ ++missedRays; ++ } ++ } ++ } ++ } ++ ++ return (float)missedRays / (float)totalRays; ++ } ++ // Paper end - optimise collisions ++ + public static DamageSource getDefaultDamageSource(Level world, @Nullable Entity source) { + return world.damageSources().explosion(source, Explosion.getIndirectSourceEntityInternal(source)); + } +@@ -167,68 +408,107 @@ public class Explosion { + } + // CraftBukkit end + this.level.gameEvent(this.source, (Holder) GameEvent.EXPLODE, new Vec3(this.x, this.y, this.z)); +- Set<BlockPos> set = Sets.newHashSet(); ++ ++ // Paper start - collision optimisations ++ this.blockCache = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(); ++ ++ this.chunkPosCache = new long[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; ++ java.util.Arrays.fill(this.chunkPosCache, ChunkPos.INVALID_CHUNK_POS); ++ ++ this.chunkCache = new net.minecraft.world.level.chunk.LevelChunk[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; ++ ++ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache = new ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH]; ++ ++ // use initial cache value that is most likely to be used: the source position ++ final ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache initialCache; ++ { ++ final int blockX = Mth.floor(this.x); ++ final int blockY = Mth.floor(this.y); ++ final int blockZ = Mth.floor(this.z); ++ ++ final long key = BlockPos.asLong(blockX, blockY, blockZ); ++ ++ initialCache = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); ++ } ++ // Paper end - collision optimisations ++ + boolean flag = true; + + int i; + int j; + +- for (int k = 0; k < 16; ++k) { +- for (i = 0; i < 16; ++i) { +- for (j = 0; j < 16; ++j) { +- if (k == 0 || k == 15 || i == 0 || i == 15 || j == 0 || j == 15) { +- double d0 = (double) ((float) k / 15.0F * 2.0F - 1.0F); +- double d1 = (double) ((float) i / 15.0F * 2.0F - 1.0F); +- double d2 = (double) ((float) j / 15.0F * 2.0F - 1.0F); +- double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2); +- +- d0 /= d3; +- d1 /= d3; +- d2 /= d3; +- float f = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F); +- double d4 = this.x; +- double d5 = this.y; +- double d6 = this.z; +- +- for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) { +- BlockPos blockposition = BlockPos.containing(d4, d5, d6); +- BlockState iblockdata = this.level.getBlockState(blockposition); +- if (!iblockdata.isDestroyable()) continue; // Paper - Protect Bedrock and End Portal/Frames from being destroyed +- FluidState fluid = iblockdata.getFluidState(); // Paper - Perf: Optimize call to getFluid for explosions +- +- if (!this.level.isInWorldBounds(blockposition)) { +- break; ++ // Paper start - collision optimisations ++ for (int ray = 0, len = CACHED_RAYS.length; ray < len;) { ++ ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache cachedBlock = initialCache; ++ ++ double currX = this.x; ++ double currY = this.y; ++ double currZ = this.z; ++ ++ final double incX = CACHED_RAYS[ray]; ++ final double incY = CACHED_RAYS[ray + 1]; ++ final double incZ = CACHED_RAYS[ray + 2]; ++ ++ ray += 3; ++ float power = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F); ++ do { ++ final int blockX = Mth.floor(currX); ++ final int blockY = Mth.floor(currY); ++ final int blockZ = Mth.floor(currZ); ++ ++ final long key = BlockPos.asLong(blockX, blockY, blockZ); ++ ++ if (cachedBlock.key != key) { ++ final int cacheKey = ++ (blockX & BLOCK_EXPLOSION_CACHE_MASK) | ++ (blockY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | ++ (blockZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); ++ cachedBlock = blockCache[cacheKey]; ++ if (cachedBlock == null || cachedBlock.key != key) { ++ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); ++ } + } + +- Optional<Float> optional = this.damageCalculator.getBlockExplosionResistance(this, this.level, blockposition, iblockdata, fluid); +- +- if (optional.isPresent()) { +- f -= ((Float) optional.get() + 0.3F) * 0.3F; ++ if (cachedBlock.outOfWorld) { ++ break; + } +- +- if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockposition, iblockdata, f)) { +- set.add(blockposition); +- // Paper start - prevent headless pistons from forming +- if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) { +- net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(blockposition); +- if (extension instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity blockEntity && blockEntity.isSourcePiston()) { +- net.minecraft.core.Direction direction = iblockdata.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING); +- set.add(blockposition.relative(direction.getOpposite())); ++ // Paper end - collision optimisations ++ BlockState iblockdata = cachedBlock.blockState; // Paper - optimise collisions ++ // Paper - collision optimisations ++ ++ // Paper start - collision optimisations ++ power -= cachedBlock.resistance; ++ ++ if (power > 0.0f && cachedBlock.shouldExplode == null && iblockdata.isDestroyable()) { // Paper - Protect Bedrock and End Portal/Frames from being destroyed ++ // note: we expect shouldBlockExplode to be pure with respect to power, as Vanilla currently is. ++ // basically, it is unused, which allows us to cache the result ++ final boolean shouldExplode = this.damageCalculator.shouldBlockExplode((Explosion)(Object)this, this.level, cachedBlock.immutablePos, cachedBlock.blockState, power); ++ cachedBlock.shouldExplode = shouldExplode ? Boolean.TRUE : Boolean.FALSE; ++ if (shouldExplode) { ++ if (this.fire || !cachedBlock.blockState.isAir()) { ++ this.toBlow.add(cachedBlock.immutablePos); ++ // Paper start - prevent headless pistons from forming ++ if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) { ++ net.minecraft.world.level.block.entity.BlockEntity extension = this.level.getBlockEntity(cachedBlock.immutablePos); // Paper - optimise collisions ++ if (extension instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity blockEntity && blockEntity.isSourcePiston()) { ++ net.minecraft.core.Direction direction = iblockdata.getValue(net.minecraft.world.level.block.piston.PistonHeadBlock.FACING); ++ this.toBlow.add(cachedBlock.immutablePos.relative(direction.getOpposite())); // Paper - optimise collisions ++ } ++ } ++ // Paper end - prevent headless pistons from forming + } + } +- // Paper end - prevent headless pistons from forming + } + +- d4 += d0 * 0.30000001192092896D; +- d5 += d1 * 0.30000001192092896D; +- d6 += d2 * 0.30000001192092896D; +- } ++ power -= 0.22500001F; ++ currX += incX; ++ currY += incY; ++ currZ += incZ; ++ } while (power > 0.0f); ++ // Paper end - collision optimisations + } +- } +- } +- } + +- this.toBlow.addAll(set); ++ // Paper - optimise collisions + float f2 = this.radius * 2.0F; + + i = Mth.floor(this.x - (double) f2 - 1.0D); +@@ -241,6 +521,10 @@ public class Explosion { + Vec3 vec3d = new Vec3(this.x, this.y, this.z); + Iterator iterator = list.iterator(); + ++ // Paper start - optimise collisions ++ final BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(); ++ // Paper end - optimise collisions ++ + while (iterator.hasNext()) { + Entity entity = (Entity) iterator.next(); + +@@ -257,6 +541,7 @@ public class Explosion { + d8 /= d11; + d9 /= d11; + d10 /= d11; ++ final double seenFraction; // Paper - optimise collisions + if (this.damageCalculator.shouldDamageEntity(this, entity)) { + // CraftBukkit start + +@@ -272,6 +557,8 @@ public class Explosion { + + entity.lastDamageCancelled = false; + ++ seenFraction = (double)this.getBlockDensity(vec3d, entity, blockCache, blockPos); // Paper - optimise collisions ++ + if (entity instanceof EnderDragon) { + for (EnderDragonPart entityComplexPart : ((EnderDragon) entity).subEntities) { + // Calculate damage separately for each EntityComplexPart +@@ -280,16 +567,21 @@ public class Explosion { + } + } + } else { +- entity.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity)); ++ // Paper start - optimise collisions ++ // inline getEntityDamageAmount so that we can avoid double calling getSeenPercent, which is the MOST ++ // expensive part of this loop!!!! ++ final double factor = (1.0 - d7) * seenFraction; ++ entity.hurt(this.damageSource, (float)((factor * factor + factor) / 2.0 * 7.0 * (double)f2 + 1.0)); ++ // Paper end - optimise collisions + } + + if (entity.lastDamageCancelled) { // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Skip entity if damage event was cancelled + continue; + } + // CraftBukkit end +- } ++ } else { seenFraction = (double)this.getBlockDensity(vec3d, entity, blockCache, blockPos); } // Paper - optimise collisions + +- double d12 = (1.0D - d7) * this.getBlockDensity(vec3d, entity) * (double) this.damageCalculator.getKnockbackMultiplier(entity); // Paper - Optimize explosions ++ double d12 = (1.0D - d7) * seenFraction * (double) this.damageCalculator.getKnockbackMultiplier(entity); // Paper - Optimize explosions // Paper - optimise collisions + double d13; + + if (entity instanceof LivingEntity) { +@@ -327,7 +619,11 @@ public class Explosion { + } + } + } +- ++ // Paper start - optimise collisions ++ this.blockCache = null; ++ this.chunkPosCache = null; ++ this.chunkCache = null; ++ // Paper end - optimise collisions + } + + public void finalizeExplosion(boolean particles) { +@@ -547,14 +843,14 @@ public class Explosion { + private BlockInteraction() {} + } + // Paper start - Optimize explosions +- private float getBlockDensity(Vec3 vec3d, Entity entity) { ++ private float getBlockDensity(Vec3 vec3d, Entity entity, ca.spottedleaf.moonrise.patches.collisions.ExplosionBlockCache[] blockCache, BlockPos.MutableBlockPos blockPos) { // Paper - optimise collisions + if (!this.level.paperConfig().environment.optimizeExplosions) { +- return getSeenPercent(vec3d, entity); ++ return this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise collisions + } + CacheKey key = new CacheKey(this, entity.getBoundingBox()); + Float blockDensity = this.level.explosionDensityCache.get(key); + if (blockDensity == null) { +- blockDensity = getSeenPercent(vec3d, entity); ++ blockDensity = this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise collisions + this.level.explosionDensityCache.put(key, blockDensity); + } + +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index e27d3547d1e19c137e05e6b8d075127a8bafb237..c4c2dc649e69d6097fbf6884ce82a34efb3e0e4d 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -81,6 +81,7 @@ import net.minecraft.world.level.storage.LevelData; + import net.minecraft.world.level.storage.WritableLevelData; + import net.minecraft.world.phys.AABB; + import net.minecraft.world.phys.Vec3; ++import net.minecraft.world.phys.shapes.VoxelShape; + import net.minecraft.world.scores.Scoreboard; + + // CraftBukkit start +@@ -102,7 +103,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, ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel { // Paper - rewrite chunk system // Paper - optimise collisions + + 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 +200,483 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + public abstract ResourceKey<LevelStem> getTypeKey(); + ++ // Paper start - rewrite chunk system ++ private ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup; ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup moonrise$getEntityLookup() { ++ return this.entityLookup; ++ } ++ ++ @Override ++ public void moonrise$setEntityLookup(final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup entityLookup) { ++ if (this.entityLookup != null && !(this.entityLookup instanceof ca.spottedleaf.moonrise.patches.chunk_system.level.entity.dfl.DefaultEntityLookup)) { ++ throw new IllegalStateException("Entity lookup already initialised"); ++ } ++ this.entityLookup = entityLookup; ++ } ++ ++ @Override ++ public final <T extends Entity> List<T> getEntitiesOfClass(final Class<T> entityClass, final AABB boundingBox, final Predicate<? super T> predicate) { ++ this.getProfiler().incrementCounter("getEntities"); ++ final List<T> ret = new java.util.ArrayList<>(); ++ ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(entityClass, null, boundingBox, ret, predicate); ++ ++ return ret; ++ } ++ ++ @Override ++ public final List<Entity> moonrise$getHardCollidingEntities(final Entity entity, final AABB box, final Predicate<? super Entity> predicate) { ++ this.getProfiler().incrementCounter("getEntities"); ++ final List<Entity> ret = new java.util.ArrayList<>(); ++ ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getHardCollidingEntities(entity, box, ret, predicate); ++ ++ return ret; ++ } ++ ++ @Override ++ public LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) { ++ return this.getChunkSource().getChunk(chunkX, chunkZ, false); ++ } ++ ++ @Override ++ public ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) { ++ return this.getChunkSource().getChunk(chunkX, chunkZ, ChunkStatus.EMPTY, false); ++ } ++ ++ @Override ++ public ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final ChunkStatus leastStatus) { ++ return this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, false); ++ } ++ ++ @Override ++ public void moonrise$midTickTasks() { ++ // no-op on ClientLevel ++ } ++ /** ++ * @reason Turn all getChunk(x, z, status) calls into virtual invokes, instead of interface invokes: ++ * 1. The interface invoke is expensive ++ * 2. The method makes other interface invokes (again, expensive) ++ * Instead, we just directly call getChunk(x, z, status, true) which avoids the interface invokes entirely. ++ * @author Spottedleaf ++ */ ++ @Override ++ public ChunkAccess getChunk(final int x, final int z, final ChunkStatus status) { ++ return ((Level)(Object)this).getChunk(x, z, status, true); ++ } ++ ++ @Override ++ public BlockPos getHeightmapPos(Heightmap.Types types, BlockPos blockPos) { ++ return new BlockPos(blockPos.getX(), this.getHeight(types, blockPos.getX(), blockPos.getZ()), blockPos.getZ()); ++ } ++ // Paper end - rewrite chunk system ++ // Paper start - optimise collisions ++ private final int minSection; ++ private final int maxSection; ++ ++ @Override ++ public final int moonrise$getMinSection() { ++ return this.minSection; ++ } ++ ++ @Override ++ public final int moonrise$getMaxSection() { ++ return this.maxSection; ++ } ++ ++ /** ++ * Route to faster lookup. ++ * See {@link EntityGetter#isUnobstructed(Entity, VoxelShape)} for expected behavior ++ * @author Spottedleaf ++ */ ++ @Override ++ public final boolean isUnobstructed(final Entity entity) { ++ final AABB boundingBox = entity.getBoundingBox(); ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(boundingBox)) { ++ return false; ++ } ++ ++ final List<Entity> entities = this.getEntities( ++ entity, ++ boundingBox.inflate(-ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON, -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON), ++ null ++ ); ++ ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ final Entity otherEntity = entities.get(i); ++ ++ if (otherEntity.isSpectator() || otherEntity.isRemoved() || !otherEntity.blocksBuilding || otherEntity.isPassengerOfSameVehicle(entity)) { ++ continue; ++ } ++ ++ return false; ++ } ++ ++ return true; ++ } ++ ++ ++ private static net.minecraft.world.phys.BlockHitResult miss(final ClipContext clipContext) { ++ final Vec3 to = clipContext.getTo(); ++ final Vec3 from = clipContext.getFrom(); ++ ++ return net.minecraft.world.phys.BlockHitResult.miss(to, Direction.getNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z)); ++ } ++ ++ private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState(); ++ ++ private static net.minecraft.world.phys.BlockHitResult fastClip(final Vec3 from, final Vec3 to, final Level level, ++ final ClipContext clipContext) { ++ final double adjX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.x - to.x); ++ final double adjY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.y - to.y); ++ final double adjZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON * (from.z - to.z); ++ ++ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) { ++ return miss(clipContext); ++ } ++ ++ final double toXAdj = to.x - adjX; ++ final double toYAdj = to.y - adjY; ++ final double toZAdj = to.z - adjZ; ++ final double fromXAdj = from.x + adjX; ++ final double fromYAdj = from.y + adjY; ++ final double fromZAdj = from.z + adjZ; ++ ++ int currX = Mth.floor(fromXAdj); ++ int currY = Mth.floor(fromYAdj); ++ int currZ = Mth.floor(fromZAdj); ++ ++ final BlockPos.MutableBlockPos currPos = new BlockPos.MutableBlockPos(); ++ ++ final double diffX = toXAdj - fromXAdj; ++ final double diffY = toYAdj - fromYAdj; ++ final double diffZ = toZAdj - fromZAdj; ++ ++ final double dxDouble = Math.signum(diffX); ++ final double dyDouble = Math.signum(diffY); ++ final double dzDouble = Math.signum(diffZ); ++ ++ final int dx = (int)dxDouble; ++ final int dy = (int)dyDouble; ++ final int dz = (int)dzDouble; ++ ++ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX; ++ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY; ++ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ; ++ ++ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj)); ++ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj)); ++ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj)); ++ ++ net.minecraft.world.level.chunk.LevelChunkSection[] lastChunk = null; ++ net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> lastSection = null; ++ int lastChunkX = Integer.MIN_VALUE; ++ int lastChunkY = Integer.MIN_VALUE; ++ int lastChunkZ = Integer.MIN_VALUE; ++ ++ final int minSection = ((ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel)level).moonrise$getMinSection(); ++ ++ for (;;) { ++ currPos.set(currX, currY, currZ); ++ ++ final int newChunkX = currX >> 4; ++ final int newChunkY = currY >> 4; ++ final int newChunkZ = currZ >> 4; ++ ++ final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); ++ final int chunkYDiff = newChunkY ^ lastChunkY; ++ ++ if ((chunkDiff | chunkYDiff) != 0) { ++ if (chunkDiff != 0) { ++ lastChunk = level.getChunk(newChunkX, newChunkZ).getSections(); ++ } ++ final int sectionY = newChunkY - minSection; ++ lastSection = sectionY >= 0 && sectionY < lastChunk.length ? lastChunk[sectionY].states : null; ++ ++ lastChunkX = newChunkX; ++ lastChunkY = newChunkY; ++ lastChunkZ = newChunkZ; ++ } ++ ++ final BlockState blockState; ++ if (lastSection != null && !(blockState = lastSection.get((currX & 15) | ((currZ & 15) << 4) | ((currY & 15) << (4+4)))).isAir()) { ++ final VoxelShape blockCollision = clipContext.getBlockShape(blockState, level, currPos); ++ ++ final net.minecraft.world.phys.BlockHitResult blockHit = blockCollision.isEmpty() ? null : level.clipWithInteractionOverride(from, to, currPos, blockCollision, blockState); ++ ++ final VoxelShape fluidCollision; ++ final FluidState fluidState; ++ if (clipContext.fluid != ClipContext.Fluid.NONE && (fluidState = blockState.getFluidState()) != AIR_FLUIDSTATE) { ++ fluidCollision = clipContext.getFluidShape(fluidState, level, currPos); ++ ++ final net.minecraft.world.phys.BlockHitResult fluidHit = fluidCollision.clip(from, to, currPos); ++ ++ if (fluidHit != null) { ++ if (blockHit == null) { ++ return fluidHit; ++ } ++ ++ return from.distanceToSqr(blockHit.getLocation()) <= from.distanceToSqr(fluidHit.getLocation()) ? blockHit : fluidHit; ++ } ++ } ++ ++ if (blockHit != null) { ++ return blockHit; ++ } ++ } // else: usually fall here ++ ++ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { ++ return miss(clipContext); ++ } ++ ++ // inc the smallest normalized coordinate ++ ++ if (normalizedCurrX < normalizedCurrY) { ++ if (normalizedCurrX < normalizedCurrZ) { ++ currX += dx; ++ normalizedCurrX += normalizedDiffX; ++ } else { ++ // x < y && x >= z <--> z < y && z <= x ++ currZ += dz; ++ normalizedCurrZ += normalizedDiffZ; ++ } ++ } else if (normalizedCurrY < normalizedCurrZ) { ++ // y <= x && y < z ++ currY += dy; ++ normalizedCurrY += normalizedDiffY; ++ } else { ++ // y <= x && z <= y <--> z <= y && z <= x ++ currZ += dz; ++ normalizedCurrZ += normalizedDiffZ; ++ } ++ } ++ } ++ ++ /** ++ * @reason Route to optimized call ++ * @author Spottedleaf ++ */ ++ @Override ++ public final net.minecraft.world.phys.BlockHitResult clip(final ClipContext clipContext) { ++ // can only do this in this class, as not everything that implements BlockGetter can retrieve chunks ++ return fastClip(clipContext.getFrom(), clipContext.getTo(), (Level)(Object)this, clipContext); ++ } ++ ++ /** ++ * @reason Route to faster logic ++ * @author Spottedleaf ++ */ ++ @Override ++ public final boolean collidesWithSuffocatingBlock(final Entity entity, final AABB box) { ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder((Level)(Object)this, entity, box, null, null, ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_ONLY, ++ (final BlockState state, final BlockPos pos) -> { ++ return state.isSuffocating((Level)(Object)Level.this, pos); ++ } ++ ); ++ } ++ ++ private static VoxelShape inflateAABBToVoxel(final AABB aabb, final double x, final double y, final double z) { ++ return net.minecraft.world.phys.shapes.Shapes.create( ++ aabb.minX - x, ++ aabb.minY - y, ++ aabb.minZ - z, ++ ++ aabb.maxX + x, ++ aabb.maxY + y, ++ aabb.maxZ + z ++ ); ++ } ++ ++ /** ++ * @reason Use optimised OR operator join strategy, avoid streams ++ * @author Spottedleaf ++ */ ++ @Override ++ public final java.util.Optional<net.minecraft.world.phys.Vec3> findFreePosition(final Entity entity, final VoxelShape boundsShape, final Vec3 fromPosition, ++ final double rangeX, final double rangeY, final double rangeZ) { ++ if (boundsShape.isEmpty()) { ++ return java.util.Optional.empty(); ++ } ++ ++ final double expandByX = rangeX * 0.5; ++ final double expandByY = rangeY * 0.5; ++ final double expandByZ = rangeZ * 0.5; ++ ++ // note: it is useless to look at shapes outside of range / 2.0 ++ final AABB collectionVolume = boundsShape.bounds().inflate(expandByX, expandByY, expandByZ); ++ ++ final List<AABB> aabbs = new java.util.ArrayList<>(); ++ final List<VoxelShape> voxels = new java.util.ArrayList<>(); ++ ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder( ++ (Level)(Object)this, entity, collectionVolume, voxels, aabbs, ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, ++ null ++ ); ++ ++ final WorldBorder worldBorder = this.getWorldBorder(); ++ if (worldBorder != null) { ++ aabbs.removeIf((final AABB aabb) -> { ++ return !worldBorder.isWithinBounds(aabb); ++ }); ++ voxels.removeIf((final VoxelShape shape) -> { ++ return !worldBorder.isWithinBounds(shape.bounds()); ++ }); ++ } ++ ++ // push voxels into aabbs ++ for (int i = 0, len = voxels.size(); i < len; ++i) { ++ aabbs.addAll(voxels.get(i).toAabbs()); ++ } ++ ++ // expand AABBs ++ final VoxelShape first = aabbs.isEmpty() ? net.minecraft.world.phys.shapes.Shapes.empty() : inflateAABBToVoxel(aabbs.get(0), expandByX, expandByY, expandByZ); ++ final VoxelShape[] rest = new VoxelShape[Math.max(0, aabbs.size() - 1)]; ++ ++ for (int i = 1, len = aabbs.size(); i < len; ++i) { ++ rest[i - 1] = inflateAABBToVoxel(aabbs.get(i), expandByX, expandByY, expandByZ); ++ } ++ ++ // use optimized implementation of ORing the shapes together ++ final VoxelShape joined = net.minecraft.world.phys.shapes.Shapes.or(first, rest); ++ ++ // find free space ++ // can use unoptimized join here (instead of join()), as closestPointTo uses toAabbs() ++ final VoxelShape freeSpace = net.minecraft.world.phys.shapes.Shapes.joinUnoptimized(boundsShape, joined, net.minecraft.world.phys.shapes.BooleanOp.ONLY_FIRST); ++ ++ return freeSpace.closestPointTo(fromPosition); ++ } ++ ++ /** ++ * @reason Route to faster logic ++ * @author Spottedleaf ++ */ ++ @Override ++ public final java.util.Optional<net.minecraft.core.BlockPos> findSupportingBlock(final Entity entity, final AABB aabb) { ++ final int minBlockX = Mth.floor(aabb.minX - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1; ++ final int maxBlockX = Mth.floor(aabb.maxX + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1; ++ ++ final int minBlockY = Mth.floor(aabb.minY - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1; ++ final int maxBlockY = Mth.floor(aabb.maxY + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1; ++ ++ final int minBlockZ = Mth.floor(aabb.minZ - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1; ++ final int maxBlockZ = Mth.floor(aabb.maxZ + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) + 1; ++ ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext collisionContext = null; ++ ++ final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); ++ BlockPos selected = null; ++ double selectedDistance = Double.MAX_VALUE; ++ ++ final Vec3 entityPos = entity.position(); ++ ++ LevelChunk lastChunk = null; ++ int lastChunkX = Integer.MIN_VALUE; ++ int lastChunkZ = Integer.MIN_VALUE; ++ ++ final ChunkSource chunkSource = this.getChunkSource(); ++ ++ for (int currZ = minBlockZ; currZ <= maxBlockZ; ++currZ) { ++ pos.setZ(currZ); ++ for (int currX = minBlockX; currX <= maxBlockX; ++currX) { ++ pos.setX(currX); ++ ++ final int newChunkX = currX >> 4; ++ final int newChunkZ = currZ >> 4; ++ ++ if (((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)) != 0) { ++ lastChunkX = newChunkX; ++ lastChunkZ = newChunkZ; ++ lastChunk = (LevelChunk)chunkSource.getChunk(newChunkX, newChunkZ, ChunkStatus.FULL, false); ++ } ++ ++ if (lastChunk == null) { ++ continue; ++ } ++ for (int currY = minBlockY; currY <= maxBlockY; ++currY) { ++ int edgeCount = ((currX == minBlockX || currX == maxBlockX) ? 1 : 0) + ++ ((currY == minBlockY || currY == maxBlockY) ? 1 : 0) + ++ ((currZ == minBlockZ || currZ == maxBlockZ) ? 1 : 0); ++ if (edgeCount == 3) { ++ continue; ++ } ++ ++ pos.setY(currY); ++ ++ final double distance = pos.distToCenterSqr(entityPos); ++ if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(pos) >= 0)) { ++ continue; ++ } ++ ++ final BlockState state = ((ca.spottedleaf.moonrise.patches.chunk_getblock.GetBlockChunk)lastChunk).moonrise$getBlock(currX, currY, currZ); ++ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)state).moonrise$emptyCollisionShape()) { ++ continue; ++ } ++ ++ VoxelShape blockCollision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)state).moonrise$getConstantCollisionShape(); ++ ++ if ((edgeCount != 1 || state.hasLargeCollisionShape()) && (edgeCount != 2 || state.getBlock() == Blocks.MOVING_PISTON)) { ++ if (collisionContext == null) { ++ collisionContext = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(entity); ++ } ++ ++ if (blockCollision == null) { ++ blockCollision = state.getCollisionShape((Level)(Object)this, pos, collisionContext); ++ } ++ ++ if (blockCollision.isEmpty()) { ++ continue; ++ } ++ ++ // avoid VoxelShape#move by shifting the entity collision shape instead ++ final AABB shiftedAABB = aabb.move(-(double)currX, -(double)currY, -(double)currZ); ++ ++ final AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation(); ++ if (singleAABB != null) { ++ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(singleAABB, shiftedAABB)) { ++ continue; ++ } ++ ++ selected = pos.immutable(); ++ selectedDistance = distance; ++ continue; ++ } ++ ++ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) { ++ continue; ++ } ++ ++ selected = pos.immutable(); ++ selectedDistance = distance; ++ continue; ++ } ++ } ++ } ++ } ++ ++ return java.util.Optional.ofNullable(selected); ++ } ++ // Paper end - optimise collisions ++ // Paper start - optimise random ticking ++ @Override ++ public abstract Holder<Biome> getUncachedNoiseBiome(final int x, final int y, final int z); ++ ++ /** ++ * @reason Make getChunk and getUncachedNoiseBiome virtual calls instead of interface calls ++ * by implementing the superclass method in this class. ++ * @author Spottedleaf ++ */ ++ @Override ++ public Holder<Biome> getNoiseBiome(final int x, final int y, final int z) { ++ final ChunkAccess chunk = this.getChunk(x >> 2, z >> 2, ChunkStatus.BIOMES, false); ++ ++ return chunk != null ? chunk.getNoiseBiome(x, y, z) : this.getUncachedNoiseBiome(x, y, z); ++ } ++ // Paper end - optimise random ticking ++ + 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 +759,11 @@ 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 - optimise collisions ++ this.minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(this); ++ this.maxSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(this); ++ // Paper end - optimise collisions + } + + // Paper start - Cancel hit for vanished players +@@ -549,7 +1032,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + this.setBlocksDirty(blockposition, iblockdata1, iblockdata2); + } + +- if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement ++ if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.FULL)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement // Paper - rewrite chunk system - change from ticking to full + this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i); + } + +@@ -813,6 +1296,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + // Iterator<TickingBlockEntity> iterator = this.blockEntityTickers.iterator(); + boolean flag = this.tickRateManager().runsNormally(); + ++ int tickedEntities = 0; // Paper - rewrite chunk system ++ + int tilesThisCycle = 0; + var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<TickingBlockEntity>(); // Paper - Fix MC-117075; use removeAll + toRemove.add(null); // Paper - Fix MC-117075 +@@ -828,6 +1313,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + // Spigot end + } else if (flag && this.shouldTickBlocksAt(tickingblockentity.getPos())) { + tickingblockentity.tick(); ++ // Paper start - rewrite chunk system ++ if ((++tickedEntities & 7) == 0) { ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)(Level)(Object)this).moonrise$midTickTasks(); ++ } ++ // Paper end - rewrite chunk system + } + } + this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 +@@ -850,12 +1340,20 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); + // Paper end - Prevent block entity and entity crashes + } ++ this.moonrise$midTickTasks(); // Paper - rewrite chunk system + } + // Paper start - Option to prevent armor stands from doing entity lookups + @Override + public boolean noCollision(@Nullable Entity entity, AABB box) { + if (entity instanceof net.minecraft.world.entity.decoration.ArmorStand && !entity.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return false; +- return LevelAccessor.super.noCollision(entity, box); ++ // Paper start - optimise collisions ++ final int flags = entity == null ? (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER | ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_ONLY) : ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_ONLY; ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisionsForBlocksOrWorldBorder((Level)(Object)this, entity, box, null, null, flags, null)) { ++ return false; ++ } ++ ++ return !ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getEntityHardCollisions((Level)(Object)this, entity, box, null, flags, null); ++ // Paper end - optimise collisions + } + // Paper end - Option to prevent armor stands from doing entity lookups + +@@ -949,7 +1447,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + // Paper end - Perf: Optimize capturedTileEntities lookup + // CraftBukkit end +- return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && Thread.currentThread() != this.thread ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); ++ return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && !io.papermc.paper.util.TickThread.isTickThread() ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); // Paper - rewrite chunk system + } + + public void setBlockEntity(BlockEntity blockEntity) { +@@ -1039,28 +1537,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + @Override + public List<Entity> getEntities(@Nullable Entity except, AABB box, Predicate<? super Entity> predicate) { + this.getProfiler().incrementCounter("getEntities"); +- List<Entity> list = Lists.newArrayList(); ++ // Paper start - rewrite chunk system ++ final List<Entity> ret = new java.util.ArrayList<>(); + +- this.getEntities().get(box, (entity1) -> { +- if (entity1 != except && predicate.test(entity1)) { +- list.add(entity1); +- } ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(except, box, ret, predicate); + +- 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); +- } +- } +- } +- +- }); +- return list; ++ return ret; ++ // Paper end - rewrite chunk system + } + + @Override +@@ -1075,36 +1558,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/biome/Biome.java b/src/main/java/net/minecraft/world/level/biome/Biome.java +index 15f82c9a1ce1fef2e951d1b3c7a65e64b82061ea..90c165c890a2d998e3b0af9b4310e3995ede6f64 100644 +--- a/src/main/java/net/minecraft/world/level/biome/Biome.java ++++ b/src/main/java/net/minecraft/world/level/biome/Biome.java +@@ -111,20 +111,7 @@ public final class Biome { + + @Deprecated + public float getTemperature(BlockPos blockPos) { +- long l = blockPos.asLong(); +- Long2FloatLinkedOpenHashMap long2FloatLinkedOpenHashMap = this.temperatureCache.get(); +- float f = long2FloatLinkedOpenHashMap.get(l); +- if (!Float.isNaN(f)) { +- return f; +- } else { +- float g = this.getHeightAdjustedTemperature(blockPos); +- if (long2FloatLinkedOpenHashMap.size() == 1024) { +- long2FloatLinkedOpenHashMap.removeFirstFloat(); +- } +- +- long2FloatLinkedOpenHashMap.put(l, g); +- return g; +- } ++ return this.getHeightAdjustedTemperature(blockPos); // Paper - optimise random ticking + } + + public boolean shouldFreeze(LevelReader world, BlockPos blockPos) { +diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java +index d775ab8b0d37797f29e650842191d40691fb7afc..a7108b2be0746aa1f0e574d8c6f5ffad6d369835 100644 +--- a/src/main/java/net/minecraft/world/level/block/Block.java ++++ b/src/main/java/net/minecraft/world/level/block/Block.java +@@ -280,7 +280,7 @@ public class Block extends BlockBehaviour implements ItemLike { + } + + public static boolean isShapeFullBlock(VoxelShape shape) { +- return (Boolean) Block.SHAPE_FULL_BLOCK_CACHE.getUnchecked(shape); ++ return ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$isFullBlock(); // Paper - optimise collisions + } + + public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) {} +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..0f289d8f9bda2fb2ca2cd2dfd667a975529b3e4c 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, ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState { // Paper - rewrite chunk system // Paper - optimise collisions + + private final int lightEmission; + private final boolean useShapeForLightOcclusion; +@@ -794,6 +794,76 @@ 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 ++ // Paper start - optimise collisions ++ private static final int RANDOM_OFFSET = 704237939; ++ private static final Direction[] DIRECTIONS_CACHED = Direction.values(); ++ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger(); ++ private final int id1 = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement() + RANDOM_OFFSET) + RANDOM_OFFSET); ++ private final int id2 = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement() + RANDOM_OFFSET) + RANDOM_OFFSET); ++ private boolean occludesFullBlock; ++ private boolean emptyCollisionShape; ++ private VoxelShape constantCollisionShape; ++ private AABB constantAABBCollision; ++ ++ private static void initCaches(final VoxelShape shape) { ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$isFullBlock(); ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$occludesFullBlock(); ++ shape.toAabbs(); ++ if (!shape.isEmpty()) { ++ shape.bounds(); ++ } ++ } ++ ++ @Override ++ public final boolean moonrise$hasCache() { ++ return this.cache != null; ++ } ++ ++ @Override ++ public final boolean moonrise$occludesFullBlock() { ++ return this.occludesFullBlock; ++ } ++ ++ @Override ++ public final boolean moonrise$emptyCollisionShape() { ++ return this.emptyCollisionShape; ++ } ++ ++ @Override ++ public final int moonrise$uniqueId1() { ++ return this.id1; ++ } ++ ++ @Override ++ public final int moonrise$uniqueId2() { ++ return this.id2; ++ } ++ ++ @Override ++ public final VoxelShape moonrise$getConstantCollisionShape() { ++ return this.constantCollisionShape; ++ } ++ ++ @Override ++ public final AABB moonrise$getConstantCollisionAABB() { ++ return this.constantAABBCollision; ++ } ++ // Paper end - optimise collisions ++ + protected BlockStateBase(Block block, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<BlockState> codec) { + super(block, propertyMap, codec); + this.fluidState = Fluids.EMPTY.defaultFluidState(); +@@ -864,6 +934,43 @@ 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 ++ // Paper start - optimise collisions ++ if (this.cache != null) { ++ final VoxelShape collisionShape = this.cache.collisionShape; ++ try { ++ this.constantCollisionShape = this.getCollisionShape(null, null, null); ++ this.constantAABBCollision = this.constantCollisionShape == null ? null : ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this.constantCollisionShape).moonrise$getSingleAABBRepresentation(); ++ } catch (final Throwable throwable) { ++ this.constantCollisionShape = null; ++ this.constantAABBCollision = null; ++ } ++ this.occludesFullBlock = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)collisionShape).moonrise$occludesFullBlock(); ++ this.emptyCollisionShape = collisionShape.isEmpty(); ++ // init caches ++ initCaches(collisionShape); ++ if (collisionShape != Shapes.empty() && collisionShape != Shapes.block()) { ++ for (final Direction direction : DIRECTIONS_CACHED) { ++ // initialise the directional face shape cache as well ++ final VoxelShape shape = Shapes.getFaceShape(collisionShape, direction); ++ initCaches(shape); ++ } ++ } ++ if (this.cache.occlusionShapes != null) { ++ for (final VoxelShape shape : this.cache.occlusionShapes) { ++ initCaches(shape); ++ } ++ } ++ } else { ++ this.occludesFullBlock = false; ++ this.emptyCollisionShape = false; ++ this.constantCollisionShape = null; ++ this.constantAABBCollision = null; ++ } ++ // Paper end - optimise collisions + } + + 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..fba548c4e6d589323ec3ea5f6b269a6fe9faf6a1 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,57 @@ 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 ++ // Paper start - get block chunk optimisation ++ private final int minSection; ++ private final int maxSection; ++ // Paper end - get block chunk optimisation ++ + 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 +150,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 +162,16 @@ 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 ++ // Paper start - get block chunk optimisation ++ this.minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(levelHeightAccessor); ++ this.maxSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(levelHeightAccessor); ++ // Paper end - get block chunk optimisation + } + public final Registry<Biome> biomeRegistry; + // CraftBukkit end +@@ -442,22 +503,22 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom + + @Override + public Holder<Biome> getNoiseBiome(int biomeX, int biomeY, int biomeZ) { +- try { +- int l = QuartPos.fromBlock(this.getMinBuildHeight()); +- int i1 = l + QuartPos.fromBlock(this.getHeight()) - 1; +- int j1 = Mth.clamp(biomeY, l, i1); +- int k1 = this.getSectionIndex(QuartPos.toBlock(j1)); +- +- return this.sections[k1].getNoiseBiome(biomeX & 3, j1 & 3, biomeZ & 3); +- } catch (Throwable throwable) { +- CrashReport crashreport = CrashReport.forThrowable(throwable, "Getting biome"); +- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Biome being got"); +- +- crashreportsystemdetails.setDetail("Location", () -> { +- return CrashReportCategory.formatLocation(this, biomeX, biomeY, biomeZ); +- }); +- throw new ReportedException(crashreport); ++ // Paper start - get block chunk optimisation ++ int sectionY = (biomeY >> 2) - this.minSection; ++ int rel = biomeY & 3; ++ ++ final LevelChunkSection[] sections = this.sections; ++ ++ if (sectionY < 0) { ++ sectionY = 0; ++ rel = 0; ++ } else if (sectionY >= sections.length) { ++ sectionY = sections.length - 1; ++ rel = 3; + } ++ ++ return sections[sectionY].getNoiseBiome(biomeX & 3, rel, biomeZ & 3); ++ // Paper end - get block chunk optimisation + } + + // CraftBukkit start +@@ -514,12 +575,12 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom + } + + public void initializeLightSources() { +- this.skyLightSources.fillFrom(this); ++ // Paper - rewrite chunk system + } + + @Override + public ChunkSkyLightSources getSkyLightSources() { +- return this.skyLightSources; ++ return null; // Paper - rewrite chunk system + } + + public static record TicksToSave(SerializableTickContainer<Block> blocks, SerializableTickContainer<Fluid> fluids) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +index 29697fad32dad3377eebc82d280ba48d3c1ad516..488938c32a48437721a71d294c77468f00c035b9 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +@@ -119,7 +119,7 @@ public abstract class ChunkGenerator { + return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("init_biomes", () -> { + chunk.fillBiomesFromNoise(this.biomeSource, noiseConfig.sampler()); + return chunk; +- }), Util.backgroundExecutor()); ++ }), Runnable::run); // Paper - rewrite chunk system + } + + public abstract void applyCarvers(WorldGenRegion chunkRegion, long seed, RandomState noiseConfig, BiomeManager biomeAccess, StructureManager structureAccessor, ChunkAccess chunk, GenerationStep.Carving carverStep); +@@ -314,7 +314,7 @@ public abstract class ChunkGenerator { + return Pair.of(placement.getLocatePos(pos), holder); + } + +- ChunkAccess ichunkaccess = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); ++ ChunkAccess ichunkaccess = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader)world).moonrise$syncLoadNonFull(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); // Paper - rewrite chunk system + + structurestart = structureAccessor.getStartForStructure(SectionPos.bottomOf(ichunkaccess), (Structure) holder.value(), ichunkaccess); + } while (structurestart == null); +diff --git a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java +index dcc0acd259920463a4464213b9a5e793603852f9..ef4161884574d3d137e12591d983dc95a960cb19 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java +@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.material.FluidState; + import net.minecraft.world.level.material.Fluids; + +-public class EmptyLevelChunk extends LevelChunk { ++public class EmptyLevelChunk extends LevelChunk implements ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system + private final Holder<Biome> biome; + + public EmptyLevelChunk(Level world, ChunkPos pos, Holder<Biome> biomeEntry) { +@@ -21,6 +21,40 @@ public class EmptyLevelChunk extends LevelChunk { + this.biome = biomeEntry; + } + ++ // Paper start - rewrite chunk system ++ @Override ++ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() { ++ return ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(this.getLevel()); ++ } ++ ++ @Override ++ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {} ++ ++ @Override ++ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() { ++ return ca.spottedleaf.moonrise.patches.starlight.light.StarLightEngine.getFilledEmptyLight(this.getLevel()); ++ } ++ ++ @Override ++ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) {} ++ ++ @Override ++ public boolean[] starlight$getSkyEmptinessMap() { ++ return null; ++ } ++ ++ @Override ++ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) {} ++ ++ @Override ++ public boolean[] starlight$getBlockEmptinessMap() { ++ return null; ++ } ++ ++ @Override ++ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) {} ++ // Paper end - rewrite chunk system ++ + @Override + public BlockState getBlockState(BlockPos pos) { + return Blocks.VOID_AIR.defaultBlockState(); +diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java +index 365074be989aa4a178114fd5e9810f1a68640196..4af698930712389881601069a921f054c07935f2 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java +@@ -31,7 +31,7 @@ import net.minecraft.world.level.material.FluidState; + import net.minecraft.world.ticks.BlackholeTickAccess; + import net.minecraft.world.ticks.TickContainerAccess; + +-public class ImposterProtoChunk extends ProtoChunk { ++public class ImposterProtoChunk extends ProtoChunk implements ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk { // Paper - rewrite chunk system + private final LevelChunk wrapped; + private final boolean allowWrites; + +@@ -47,6 +47,48 @@ public class ImposterProtoChunk extends ProtoChunk { + this.allowWrites = propagateToWrapped; + } + ++ // Paper start - rewrite chunk system ++ @Override ++ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getBlockNibbles() { ++ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getBlockNibbles(); ++ } ++ ++ @Override ++ public void starlight$setBlockNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) { ++ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setBlockNibbles(nibbles); ++ } ++ ++ @Override ++ public ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] starlight$getSkyNibbles() { ++ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getSkyNibbles(); ++ } ++ ++ @Override ++ public void starlight$setSkyNibbles(final ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] nibbles) { ++ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setSkyNibbles(nibbles); ++ } ++ ++ @Override ++ public boolean[] starlight$getSkyEmptinessMap() { ++ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getSkyEmptinessMap(); ++ } ++ ++ @Override ++ public void starlight$setSkyEmptinessMap(final boolean[] emptinessMap) { ++ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setSkyEmptinessMap(emptinessMap); ++ } ++ ++ @Override ++ public boolean[] starlight$getBlockEmptinessMap() { ++ return ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$getBlockEmptinessMap(); ++ } ++ ++ @Override ++ public void starlight$setBlockEmptinessMap(final boolean[] emptinessMap) { ++ ((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)this.wrapped).starlight$setBlockEmptinessMap(emptinessMap); ++ } ++ // Paper end - rewrite chunk system ++ + @Nullable + @Override + public BlockEntity getBlockEntity(BlockPos pos) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index 602ad80c2b93d320bf2a25832d25a58cb8c72e4b..214bb6759d2670042d87b2fd0aae41df600a95ee 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, ca.spottedleaf.moonrise.patches.chunk_getblock.GetBlockChunk { // Paper - rewrite chunk system // Paper - get block chunk optimisation + + static final Logger LOGGER = LogUtils.getLogger(); + private static final TickingBlockEntity NULL_TICKER = new TickingBlockEntity() { +@@ -109,6 +109,14 @@ public class LevelChunk extends ChunkAccess { + this.postLoad = entityLoader; + this.blockTicks = blockTickScheduler; + this.fluidTicks = fluidTickScheduler; ++ // Paper start - get block chunk optimisation ++ this.minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(level); ++ this.maxSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMaxSection(level); ++ ++ final boolean empty = ((Object)this instanceof EmptyLevelChunk); ++ this.debug = !empty && this.level.isDebug(); ++ this.defaultBlockState = empty ? VOID_AIR_BLOCKSTATE : AIR_BLOCKSTATE; ++ // Paper end - get block chunk optimisation + } + + // CraftBukkit start +@@ -119,6 +127,28 @@ public class LevelChunk extends ChunkAccess { + // Paper start + boolean loadedTicketLevel; + // Paper end ++ // Paper start - rewrite chunk system ++ private boolean postProcessingDone; ++ ++ @Override ++ public final boolean moonrise$isPostProcessingDone() { ++ return this.postProcessingDone; ++ } ++ // Paper end - rewrite chunk system ++ // Paper start - get block chunk optimisation ++ private static final BlockState AIR_BLOCKSTATE = Blocks.AIR.defaultBlockState(); ++ private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState(); ++ private static final BlockState VOID_AIR_BLOCKSTATE = Blocks.VOID_AIR.defaultBlockState(); ++ private final int minSection; ++ private final int maxSection; ++ private final boolean debug; ++ private final BlockState defaultBlockState; ++ ++ @Override ++ public final BlockState moonrise$getBlock(final int x, final int y, final int z) { ++ return this.getBlockStateFinal(x, y, z); ++ } ++ // Paper end - get block chunk optimisation + + public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) { + this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData()); +@@ -148,13 +178,19 @@ public class LevelChunk extends ChunkAccess { + } + } + +- this.skyLightSources = protoChunk.skyLightSources; ++ // Paper - rewrite chunk system + this.setLightCorrect(protoChunk.isLightCorrect()); + this.unsaved = true; + this.needsDecoration = true; // CraftBukkit + // CraftBukkit start + this.persistentDataContainer = protoChunk.persistentDataContainer; // SPIGOT-6814: copy PDC to account for 1.17 to 1.18 chunk upgrading. + // CraftBukkit end ++ // Paper start - rewrite chunk system ++ this.starlight$setBlockNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getBlockNibbles()); ++ this.starlight$setSkyNibbles(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getSkyNibbles()); ++ this.starlight$setSkyEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getSkyEmptinessMap()); ++ this.starlight$setBlockEmptinessMap(((ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk)protoChunk).starlight$getBlockEmptinessMap()); ++ // Paper end - rewrite chunk system + } + + @Override +@@ -337,7 +373,7 @@ public class LevelChunk extends ChunkAccess { + ProfilerFiller gameprofilerfiller = this.level.getProfiler(); + + gameprofilerfiller.push("updateSkyLightSources"); +- this.skyLightSources.update(this, j, i, l); ++ // Paper - rewrite chunk system + gameprofilerfiller.popPush("queueCheckLight"); + this.level.getChunkSource().getLightEngine().checkBlock(blockposition); + gameprofilerfiller.pop(); +@@ -597,11 +633,12 @@ public class LevelChunk extends ChunkAccess { + + // CraftBukkit start + public void loadCallback() { ++ if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Paper + // Paper start + this.loadedTicketLevel = true; + // Paper end + org.bukkit.Server server = this.level.getCraftServer(); +- this.level.getChunkSource().addLoadedChunk(this); // Paper ++ // Paper - rewrite chunk system + if (server != null) { + /* + * If it's a new world, the first few chunks are generated inside +@@ -610,6 +647,7 @@ public class LevelChunk extends ChunkAccess { + */ + org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this); + server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(bukkitChunk, this.needsDecoration)); ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.locX, this.locZ).getEntityChunk().callEntitiesLoadEvent(); // Paper - rewrite chunk system + + if (this.needsDecoration) { + try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper +@@ -638,13 +676,15 @@ public class LevelChunk extends ChunkAccess { + } + + public void unloadCallback() { ++ if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper + org.bukkit.Server server = this.level.getCraftServer(); ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.locX, this.locZ).getEntityChunk().callEntitiesUnloadEvent(); // Paper - rewrite chunk system + org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this); +- org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, this.isUnsaved()); ++ org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, true); // Paper - rewrite chunk system - force save to true so that mustNotSave is correctly set below + server.getPluginManager().callEvent(unloadEvent); + // note: saving can be prevented, but not forced if no saving is actually required + this.mustNotSave = !unloadEvent.isSaveChunk(); +- this.level.getChunkSource().removeLoadedChunk(this); // Paper ++ // Paper - rewrite chunk system + // Paper start + this.loadedTicketLevel = false; + // Paper end +@@ -652,8 +692,27 @@ public class LevelChunk extends ChunkAccess { + + @Override + public boolean isUnsaved() { +- return super.isUnsaved() && !this.mustNotSave; ++ // Paper start - rewrite chunk system ++ final long gameTime = this.level.getGameTime(); ++ if (((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$isDirty(gameTime) ++ || ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$isDirty(gameTime)) { ++ return true; ++ } ++ ++ return super.isUnsaved(); ++ // Paper end - rewrite chunk system ++ } ++ ++ // Paper start - rewrite chunk system ++ @Override ++ public void setUnsaved(final boolean needsSaving) { ++ if (!needsSaving) { ++ ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$clearDirty(); ++ ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$clearDirty(); ++ } ++ super.setUnsaved(needsSaving); + } ++ // Paper end - rewrite chunk system + // CraftBukkit end + + public boolean isEmpty() { +@@ -759,6 +818,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/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +index 90d1c3e23e753c29660f7d993b3c90ac022941c3..d7b8d984122ba6b6ef5a0be6e012a8828a1af22d 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.Blocks; + import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.material.FluidState; + +-public class LevelChunkSection { ++public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection { // Paper - block counting + + public static final int SECTION_WIDTH = 16; + public static final int SECTION_HEIGHT = 16; +@@ -26,6 +26,28 @@ public class LevelChunkSection { + // CraftBukkit start - read/write + private PalettedContainer<Holder<Biome>> biomes; + ++ // Paper start - block counting ++ private static final it.unimi.dsi.fastutil.ints.IntArrayList FULL_LIST = new it.unimi.dsi.fastutil.ints.IntArrayList(16*16*16); ++ static { ++ for (int i = 0; i < (16*16*16); ++i) { ++ FULL_LIST.add(i); ++ } ++ } ++ ++ private int specialCollidingBlocks; ++ private final ca.spottedleaf.moonrise.common.list.IBlockDataList tickingBlocks = new ca.spottedleaf.moonrise.common.list.IBlockDataList(); ++ ++ @Override ++ public final int moonrise$getSpecialCollidingBlocks() { ++ return this.specialCollidingBlocks; ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.common.list.IBlockDataList moonrise$getTickingBlockList() { ++ return this.tickingBlocks; ++ } ++ // Paper end - block counting ++ + public LevelChunkSection(PalettedContainer<BlockState> datapaletteblock, PalettedContainer<Holder<Biome>> palettedcontainerro) { + // CraftBukkit end + this.states = datapaletteblock; +@@ -92,6 +114,22 @@ public class LevelChunkSection { + ++this.tickingFluidCount; + } + ++ // Paper start - block counting ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(iblockdata1)) { ++ --this.specialCollidingBlocks; ++ } ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(state)) { ++ ++this.specialCollidingBlocks; ++ } ++ ++ if (iblockdata1.isRandomlyTicking()) { ++ this.tickingBlocks.remove(x, y, z); ++ } ++ if (state.isRandomlyTicking()) { ++ this.tickingBlocks.add(x, y, z, state); ++ } ++ // Paper end - block counting ++ + return iblockdata1; + } + +@@ -112,40 +150,65 @@ public class LevelChunkSection { + } + + public void recalcBlockCounts() { +- class a implements PalettedContainer.CountConsumer<BlockState> { ++ // Paper start - block counting ++ // reset, then recalculate ++ this.nonEmptyBlockCount = (short)0; ++ this.tickingBlockCount = (short)0; ++ this.tickingFluidCount = (short)0; ++ this.specialCollidingBlocks = (short)0; ++ this.tickingBlocks.clear(); ++ ++ if (this.maybeHas((final BlockState state) -> !state.isAir())) { ++ final PalettedContainer.Data<BlockState> data = this.states.data; ++ final Palette<BlockState> palette = data.palette(); ++ final int paletteSize = palette.getSize(); ++ final net.minecraft.util.BitStorage storage = data.storage(); ++ ++ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> counts; ++ if (paletteSize == 1) { ++ counts = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1); ++ counts.put(0, FULL_LIST); ++ } else { ++ counts = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingBitStorage)storage).moonrise$countEntries(); ++ } + +- public int nonEmptyBlockCount; +- public int tickingBlockCount; +- public int tickingFluidCount; ++ for (final java.util.Iterator<it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.ints.IntArrayList>> iterator = counts.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.ints.IntArrayList> entry = iterator.next(); ++ final int paletteIdx = entry.getIntKey(); ++ final it.unimi.dsi.fastutil.ints.IntArrayList coordinates = entry.getValue(); ++ final int paletteCount = coordinates.size(); + +- a(final LevelChunkSection chunksection) {} ++ final BlockState state = palette.valueFor(paletteIdx); + +- public void accept(BlockState iblockdata, int i) { +- FluidState fluid = iblockdata.getFluidState(); ++ if (state.isAir()) { ++ continue; ++ } + +- if (!iblockdata.isAir()) { +- this.nonEmptyBlockCount += i; +- if (iblockdata.isRandomlyTicking()) { +- this.tickingBlockCount += i; ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(state)) { ++ this.specialCollidingBlocks += paletteCount; ++ } ++ this.nonEmptyBlockCount += paletteCount; ++ if (state.isRandomlyTicking()) { ++ this.tickingBlockCount += paletteCount; ++ final int[] raw = coordinates.elements(); ++ ++ java.util.Objects.checkFromToIndex(0, paletteCount, raw.length); ++ for (int i = 0; i < paletteCount; ++i) { ++ this.tickingBlocks.add(raw[i], state); + } + } + ++ final FluidState fluid = state.getFluidState(); ++ + if (!fluid.isEmpty()) { +- this.nonEmptyBlockCount += i; ++ //this.nonEmptyBlockCount += count; // fix vanilla bug: make non empty block count correct + if (fluid.isRandomlyTicking()) { +- this.tickingFluidCount += i; ++ this.tickingFluidCount += paletteCount; + } + } +- + } + } +- +- a a0 = new a(this); +- +- this.states.count(a0); +- this.nonEmptyBlockCount = (short) a0.nonEmptyBlockCount; +- this.tickingBlockCount = (short) a0.tickingBlockCount; +- this.tickingFluidCount = (short) a0.tickingFluidCount; ++ // Paper end - block counting + } + + public PalettedContainer<BlockState> getStates() { +@@ -163,6 +226,7 @@ public class LevelChunkSection { + + datapaletteblock.read(buf); + this.biomes = datapaletteblock; ++ this.recalcBlockCounts(); // Paper - block counting + } + + public void readBiomes(FriendlyByteBuf buf) { +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..339cac6b34b9f2f53852cfcee821bec9e0286c50 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +@@ -28,7 +28,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer + private static final int MIN_PALETTE_BITS = 0; + private final PaletteResize<T> dummyPaletteResize = (newSize, added) -> 0; + public final IdMap<T> registry; +- private volatile PalettedContainer.Data<T> data; ++ public volatile PalettedContainer.Data<T> data; // Paper - optimise collisions - public + private final PalettedContainer.Strategy strategy; + // private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused + +@@ -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/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java +index c8f7c43134e7c51ce8af5b3c1a28c11db67715a2..29123f3a2f211c08d1a9ccf62ca9bc9822f90111 100644 +--- a/src/main/java/net/minecraft/world/phys/AABB.java ++++ b/src/main/java/net/minecraft/world/phys/AABB.java +@@ -326,7 +326,7 @@ public class AABB { + } + + @Nullable +- private static Direction getDirection( ++ public static Direction getDirection( // Paper - optimise collisions - public + AABB box, Vec3 intersectingVector, double[] traceDistanceResult, @Nullable Direction approachDirection, double deltaX, double deltaY, double deltaZ + ) { + if (deltaX > 1.0E-7) { +diff --git a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java +index 4fee67f7214b464b9e09862778e3ef187fcb8b72..31a54af04ab072a433d6df9fe37beb12243fea80 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java +@@ -20,7 +20,7 @@ public class ArrayVoxelShape extends VoxelShape { + ); + } + +- ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) { ++ public ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) { // Paper - optimise collisions - public + super(shape); + int i = shape.getXSize() + 1; + int j = shape.getYSize() + 1; +@@ -34,6 +34,7 @@ public class ArrayVoxelShape extends VoxelShape { + new IllegalArgumentException("Lengths of point arrays must be consistent with the size of the VoxelShape.") + ); + } ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this).moonrise$initCache(); // Paper - optimise collisions + } + + @Override +diff --git a/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java +index e8f3307727e7e3da9a7629cafc6e1ee53790b75d..97ef481156ec5d821779f126ab98a8f28cbaf30b 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java +@@ -4,13 +4,13 @@ import java.util.BitSet; + import net.minecraft.core.Direction; + + public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape { +- private final BitSet storage; +- private int xMin; +- private int yMin; +- private int zMin; +- private int xMax; +- private int yMax; +- private int zMax; ++ public final BitSet storage; // Paper - optimise collisions - public ++ public int xMin; // Paper - optimise collisions - public ++ public int yMin; // Paper - optimise collisions - public ++ public int zMin; // Paper - optimise collisions - public ++ public int xMax; // Paper - optimise collisions - public ++ public int yMax; // Paper - optimise collisions - public ++ public int zMax; // Paper - optimise collisions - public + + public BitSetDiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) { + super(sizeX, sizeY, sizeZ); +@@ -150,47 +150,109 @@ public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape { + return bitSetDiscreteVoxelShape; + } + +- protected static void forAllBoxes(DiscreteVoxelShape voxelSet, DiscreteVoxelShape.IntLineConsumer callback, boolean coalesce) { +- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = new BitSetDiscreteVoxelShape(voxelSet); ++ // Paper start - optimise collisions ++ public static void forAllBoxes(final DiscreteVoxelShape shape, final DiscreteVoxelShape.IntLineConsumer consumer, final boolean mergeAdjacent) { ++ // Paper - remove debug ++ // called with the shape of a VoxelShape, so we can expect the cache to exist ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cache = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape) shape).moonrise$getOrCreateCachedShapeData(); ++ ++ final int sizeX = cache.sizeX(); ++ final int sizeY = cache.sizeY(); ++ final int sizeZ = cache.sizeZ(); ++ ++ int indexX; ++ int indexY = 0; ++ int indexZ; ++ ++ int incY = sizeZ; ++ int incX = sizeZ * sizeY; ++ ++ long[] bitset = cache.voxelSet(); ++ ++ // index = z + y*size_z + x*(size_z*size_y) ++ ++ if (!mergeAdjacent) { ++ // due to the odd selection of loop order (which does affect behavior, unfortunately) we can't simply ++ // increment an index in the Z loop, and have to perform this trash (keeping track of 3 counters) to avoid ++ // the multiplication ++ for (int y = 0; y < sizeY; ++y, indexY += incY) { ++ indexX = indexY; ++ for (int x = 0; x < sizeX; ++x, indexX += incX) { ++ indexZ = indexX; ++ for (int z = 0; z < sizeZ; ++z, ++indexZ) { ++ if ((bitset[indexZ >>> 6] & (1L << indexZ)) != 0L) { ++ consumer.consume(x, y, z, x + 1, y + 1, z + 1); ++ } ++ } ++ } ++ } ++ } else { ++ // same notes about loop order as the above ++ // this branch is actually important to optimise, as it affects uncached toAabbs() (which affects optimize()) + +- for (int i = 0; i < bitSetDiscreteVoxelShape.ySize; i++) { +- for (int j = 0; j < bitSetDiscreteVoxelShape.xSize; j++) { +- int k = -1; ++ // only clone when we may write to it ++ bitset = ca.spottedleaf.moonrise.common.util.MixinWorkarounds.clone(bitset); + +- for (int l = 0; l <= bitSetDiscreteVoxelShape.zSize; l++) { +- if (bitSetDiscreteVoxelShape.isFullWide(j, i, l)) { +- if (coalesce) { +- if (k == -1) { +- k = l; +- } +- } else { +- callback.consume(j, i, l, j + 1, i + 1, l + 1); ++ for (int y = 0; y < sizeY; ++y, indexY += incY) { ++ indexX = indexY; ++ for (int x = 0; x < sizeX; ++x, indexX += incX) { ++ for (int zIdx = indexX, endIndex = indexX + sizeZ; zIdx < endIndex; ) { ++ final int firstSetZ = ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.firstSet(bitset, zIdx, endIndex); ++ ++ if (firstSetZ == -1) { ++ break; ++ } ++ ++ int lastSetZ = ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.firstClear(bitset, firstSetZ, endIndex); ++ if (lastSetZ == -1) { ++ lastSetZ = endIndex; + } +- } else if (k != -1) { +- int m = j; +- int n = i; +- bitSetDiscreteVoxelShape.clearZStrip(k, l, j, i); +- +- while (bitSetDiscreteVoxelShape.isZStripFull(k, l, m + 1, i)) { +- bitSetDiscreteVoxelShape.clearZStrip(k, l, m + 1, i); +- m++; ++ ++ ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.clearRange(bitset, firstSetZ, lastSetZ); ++ ++ // try to merge neighbouring on the X axis ++ int endX = x + 1; // exclusive ++ for (int neighbourIdxStart = firstSetZ + incX, neighbourIdxEnd = lastSetZ + incX; ++ endX < sizeX && ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.isRangeSet(bitset, neighbourIdxStart, neighbourIdxEnd); ++ neighbourIdxStart += incX, neighbourIdxEnd += incX) { ++ ++ ++endX; ++ ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.clearRange(bitset, neighbourIdxStart, neighbourIdxEnd); + } + +- while (bitSetDiscreteVoxelShape.isXZRectangleFull(j, m + 1, k, l, n + 1)) { +- for (int o = j; o <= m; o++) { +- bitSetDiscreteVoxelShape.clearZStrip(k, l, o, n + 1); ++ // try to merge neighbouring on the Y axis ++ ++ int endY; // exclusive ++ int firstSetZY, lastSetZY; ++ y_merge: ++ for (endY = y + 1, firstSetZY = firstSetZ + incY, lastSetZY = lastSetZ + incY; endY < sizeY; ++ firstSetZY += incY, lastSetZY += incY) { ++ ++ // test the whole XZ range ++ for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX; ++ ++testX, start += incX, end += incX) { ++ if (!ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.isRangeSet(bitset, start, end)) { ++ break y_merge; ++ } + } + +- n++; ++ ++endY; ++ ++ // passed, so we can clear it ++ for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX; ++ ++testX, start += incX, end += incX) { ++ ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.clearRange(bitset, start, end); ++ } + } + +- callback.consume(j, i, k, m + 1, n + 1, l); +- k = -1; ++ consumer.consume(x, y, firstSetZ - indexX, endX, endY, lastSetZ - indexX); ++ zIdx = lastSetZ; + } + } + } + } + } ++ // Paper end - optimise collisions + + private boolean isZStripFull(int z1, int z2, int x, int y) { + return x < this.xSize && y < this.ySize && this.storage.nextClearBit(this.getIndex(x, y, z1)) >= this.getIndex(x, y, z2); +diff --git a/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java +index d812949c7329ae2696b38dc792fa011ba87decb9..7743495c7ec3fc5e17947144457cef7bbe0f4b38 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java +@@ -7,6 +7,7 @@ import net.minecraft.util.Mth; + public final class CubeVoxelShape extends VoxelShape { + protected CubeVoxelShape(DiscreteVoxelShape voxels) { + super(voxels); ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this).moonrise$initCache(); // Paper - optimise collisions + } + + @Override +diff --git a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java +index 01693ba050b12b9debcdaefceeff9cbcd503b369..1d36f8dcffd22cf844448d3d8351fb8718cf5227 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java +@@ -3,12 +3,79 @@ package net.minecraft.world.phys.shapes; + import net.minecraft.core.AxisCycle; + import net.minecraft.core.Direction; + +-public abstract class DiscreteVoxelShape { ++public abstract class DiscreteVoxelShape implements ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape { // Paper - optimise collisions + private static final Direction.Axis[] AXIS_VALUES = Direction.Axis.values(); + protected final int xSize; + protected final int ySize; + protected final int zSize; + ++ // Paper start - optimise collisions ++ // ignore race conditions on field read/write: the shape is static, so it doesn't matter ++ private ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData; ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getOrCreateCachedShapeData() { ++ if (this.cachedShapeData != null) { ++ return this.cachedShapeData; ++ } ++ ++ final DiscreteVoxelShape discreteVoxelShape = (DiscreteVoxelShape)(Object)this; ++ ++ final int sizeX = discreteVoxelShape.getXSize(); ++ final int sizeY = discreteVoxelShape.getYSize(); ++ final int sizeZ = discreteVoxelShape.getZSize(); ++ ++ final int maxIndex = sizeX * sizeY * sizeZ; // exclusive ++ ++ final int longsRequired = (maxIndex + (Long.SIZE - 1)) >>> 6; ++ long[] voxelSet; ++ ++ final boolean isEmpty = discreteVoxelShape.isEmpty(); ++ ++ if (discreteVoxelShape instanceof BitSetDiscreteVoxelShape bitsetShape) { ++ voxelSet = bitsetShape.storage.toLongArray(); ++ if (voxelSet.length < longsRequired) { ++ // happens when the later long values are 0L, so we need to resize ++ voxelSet = java.util.Arrays.copyOf(voxelSet, longsRequired); ++ } ++ } else { ++ voxelSet = new long[longsRequired]; ++ if (!isEmpty) { ++ final int mulX = sizeZ * sizeY; ++ for (int x = 0; x < sizeX; ++x) { ++ for (int y = 0; y < sizeY; ++y) { ++ for (int z = 0; z < sizeZ; ++z) { ++ if (discreteVoxelShape.isFull(x, y, z)) { ++ // index = z + y*size_z + x*(size_z*size_y) ++ final int index = z + y * sizeZ + x * mulX; ++ ++ voxelSet[index >>> 6] |= 1L << index; ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ final boolean hasSingleAABB = sizeX == 1 && sizeY == 1 && sizeZ == 1 && !isEmpty && discreteVoxelShape.isFull(0, 0, 0); ++ ++ final int minFullX = discreteVoxelShape.firstFull(Direction.Axis.X); ++ final int minFullY = discreteVoxelShape.firstFull(Direction.Axis.Y); ++ final int minFullZ = discreteVoxelShape.firstFull(Direction.Axis.Z); ++ ++ final int maxFullX = discreteVoxelShape.lastFull(Direction.Axis.X); ++ final int maxFullY = discreteVoxelShape.lastFull(Direction.Axis.Y); ++ final int maxFullZ = discreteVoxelShape.lastFull(Direction.Axis.Z); ++ ++ return this.cachedShapeData = new ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData( ++ sizeX, sizeY, sizeZ, voxelSet, ++ minFullX, minFullY, minFullZ, ++ maxFullX, maxFullY, maxFullZ, ++ isEmpty, hasSingleAABB ++ ); ++ } ++ // Paper end - optimise collisions ++ + protected DiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) { + if (sizeX >= 0 && sizeY >= 0 && sizeZ >= 0) { + this.xSize = sizeX; +diff --git a/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java b/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java +index 7ec02a7849437a18860aa0df7d9ddd71b2447d4c..5e45e49ab09344cb95736f4124b1c6e002ef5b82 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java +@@ -4,8 +4,8 @@ import it.unimi.dsi.fastutil.doubles.AbstractDoubleList; + import it.unimi.dsi.fastutil.doubles.DoubleList; + + public class OffsetDoubleList extends AbstractDoubleList { +- private final DoubleList delegate; +- private final double offset; ++ public final DoubleList delegate; // Paper - optimise collisions - public ++ public final double offset; // Paper - optimise collisions - public + + public OffsetDoubleList(DoubleList oldList, double offset) { + this.delegate = oldList; +diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java +index 0fdd2cdd8d215ca1523eda8ad7316cdd5f41a6b5..7ede56f77af1d40e10fde2e660d5794e4b37dc5d 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java +@@ -16,9 +16,15 @@ public final class Shapes { + public static final double EPSILON = 1.0E-7; + public static final double BIG_EPSILON = 1.0E-6; + private static final VoxelShape BLOCK = Util.make(() -> { +- DiscreteVoxelShape discreteVoxelShape = new BitSetDiscreteVoxelShape(1, 1, 1); +- discreteVoxelShape.fill(0, 0, 0); +- return new CubeVoxelShape(discreteVoxelShape); ++ // Paper start - optimise collisions ++ final DiscreteVoxelShape shape = new BitSetDiscreteVoxelShape(1, 1, 1); ++ shape.fill(0, 0, 0); ++ ++ return new ArrayVoxelShape( ++ shape, ++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE ++ ); ++ // Paper end - optimise collisions + }); + public static final VoxelShape INFINITY = box( + Double.NEGATIVE_INFINITY, +@@ -43,6 +49,30 @@ public final class Shapes { + return BLOCK; + } + ++ // Paper start - optimise collisions ++ private static final DoubleArrayList[] PARTS_BY_BITS = new DoubleArrayList[] { ++ DoubleArrayList.wrap(generateCubeParts(1 << 0)), ++ DoubleArrayList.wrap(generateCubeParts(1 << 1)), ++ DoubleArrayList.wrap(generateCubeParts(1 << 2)), ++ DoubleArrayList.wrap(generateCubeParts(1 << 3)) ++ }; ++ ++ private static double[] generateCubeParts(final int parts) { ++ // note: parts is a power of two, so we do not need to worry about loss of precision here ++ // note: parts is from [2^0, 2^3] ++ final double inc = 1.0 / (double)parts; ++ ++ final double[] ret = new double[parts + 1]; ++ double val = 0.0; ++ for (int i = 0; i <= parts; ++i) { ++ ret[i] = val; ++ val += inc; ++ } ++ ++ return ret; ++ } ++ // Paper end - optimise collisions ++ + public static VoxelShape box(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + if (!(minX > maxX) && !(minY > maxY) && !(minZ > maxZ)) { + return create(minX, minY, minZ, maxX, maxY, maxZ); +@@ -52,39 +82,42 @@ public final class Shapes { + } + + public static VoxelShape create(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { ++ // Paper start - optimise collisions + if (!(maxX - minX < 1.0E-7) && !(maxY - minY < 1.0E-7) && !(maxZ - minZ < 1.0E-7)) { +- int i = findBits(minX, maxX); +- int j = findBits(minY, maxY); +- int k = findBits(minZ, maxZ); +- if (i < 0 || j < 0 || k < 0) { ++ final int bitsX = findBits(minX, maxX); ++ final int bitsY = findBits(minY, maxY); ++ final int bitsZ = findBits(minZ, maxZ); ++ if (bitsX >= 0 && bitsY >= 0 && bitsZ >= 0) { ++ if (bitsX == 0 && bitsY == 0 && bitsZ == 0) { ++ return BLOCK; ++ } else { ++ final int sizeX = 1 << bitsX; ++ final int sizeY = 1 << bitsY; ++ final int sizeZ = 1 << bitsZ; ++ final BitSetDiscreteVoxelShape shape = BitSetDiscreteVoxelShape.withFilledBounds( ++ sizeX, sizeY, sizeZ, ++ (int)Math.round(minX * (double)sizeX), (int)Math.round(minY * (double)sizeY), (int)Math.round(minZ * (double)sizeZ), ++ (int)Math.round(maxX * (double)sizeX), (int)Math.round(maxY * (double)sizeY), (int)Math.round(maxZ * (double)sizeZ) ++ ); ++ return new ArrayVoxelShape( ++ shape, ++ PARTS_BY_BITS[bitsX], ++ PARTS_BY_BITS[bitsY], ++ PARTS_BY_BITS[bitsZ] ++ ); ++ } ++ } else { + return new ArrayVoxelShape( + BLOCK.shape, +- DoubleArrayList.wrap(new double[]{minX, maxX}), +- DoubleArrayList.wrap(new double[]{minY, maxY}), +- DoubleArrayList.wrap(new double[]{minZ, maxZ}) +- ); +- } else if (i == 0 && j == 0 && k == 0) { +- return block(); +- } else { +- int l = 1 << i; +- int m = 1 << j; +- int n = 1 << k; +- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.withFilledBounds( +- l, +- m, +- n, +- (int)Math.round(minX * (double)l), +- (int)Math.round(minY * (double)m), +- (int)Math.round(minZ * (double)n), +- (int)Math.round(maxX * (double)l), +- (int)Math.round(maxY * (double)m), +- (int)Math.round(maxZ * (double)n) ++ minX == 0.0 && maxX == 1.0 ? ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minX, maxX }), ++ minY == 0.0 && maxY == 1.0 ? ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minY, maxY }), ++ minZ == 0.0 && maxZ == 1.0 ? ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minZ, maxZ }) + ); +- return new CubeVoxelShape(bitSetDiscreteVoxelShape); + } + } else { +- return empty(); ++ return EMPTY; + } ++ // Paper end - optimise collisions + } + + public static VoxelShape create(AABB box) { +@@ -119,80 +152,54 @@ public final class Shapes { + return join(first, second, BooleanOp.OR); + } + +- public static VoxelShape or(VoxelShape first, VoxelShape... others) { +- return Arrays.stream(others).reduce(first, Shapes::or); ++ // Paper start - optimise collisions ++ public static VoxelShape or(VoxelShape shape, VoxelShape... others) { ++ int size = others.length; ++ if (size == 0) { ++ return shape; ++ } ++ ++ // reduce complexity of joins by splitting the merges ++ ++ // add extra slot for first shape ++ ++size; ++ final VoxelShape[] tmp = Arrays.copyOf(others, size); ++ // insert first shape ++ tmp[size - 1] = shape; ++ ++ while (size > 1) { ++ int newSize = 0; ++ for (int i = 0; i < size; i += 2) { ++ final int next = i + 1; ++ if (next >= size) { ++ // nothing to merge with, so leave it for next iteration ++ tmp[newSize++] = tmp[i]; ++ break; ++ } else { ++ // merge with adjacent ++ final VoxelShape first = tmp[i]; ++ final VoxelShape second = tmp[next]; ++ ++ tmp[newSize++] = Shapes.or(first, second); ++ } ++ } ++ size = newSize; ++ } ++ ++ return tmp[0]; ++ // Paper end - optimise collisions + } + + public static VoxelShape join(VoxelShape first, VoxelShape second, BooleanOp function) { +- return joinUnoptimized(first, second, function).optimize(); ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.joinOptimized(first, second, function); // Paper - optimise collisions + } + + public static VoxelShape joinUnoptimized(VoxelShape one, VoxelShape two, BooleanOp function) { +- if (function.apply(false, false)) { +- throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException()); +- } else if (one == two) { +- return function.apply(true, true) ? one : empty(); +- } else { +- boolean bl = function.apply(true, false); +- boolean bl2 = function.apply(false, true); +- if (one.isEmpty()) { +- return bl2 ? two : empty(); +- } else if (two.isEmpty()) { +- return bl ? one : empty(); +- } else { +- IndexMerger indexMerger = createIndexMerger(1, one.getCoords(Direction.Axis.X), two.getCoords(Direction.Axis.X), bl, bl2); +- IndexMerger indexMerger2 = createIndexMerger(indexMerger.size() - 1, one.getCoords(Direction.Axis.Y), two.getCoords(Direction.Axis.Y), bl, bl2); +- IndexMerger indexMerger3 = createIndexMerger( +- (indexMerger.size() - 1) * (indexMerger2.size() - 1), one.getCoords(Direction.Axis.Z), two.getCoords(Direction.Axis.Z), bl, bl2 +- ); +- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.join( +- one.shape, two.shape, indexMerger, indexMerger2, indexMerger3, function +- ); +- return (VoxelShape)(indexMerger instanceof DiscreteCubeMerger +- && indexMerger2 instanceof DiscreteCubeMerger +- && indexMerger3 instanceof DiscreteCubeMerger +- ? new CubeVoxelShape(bitSetDiscreteVoxelShape) +- : new ArrayVoxelShape(bitSetDiscreteVoxelShape, indexMerger.getList(), indexMerger2.getList(), indexMerger3.getList())); +- } +- } ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.joinUnoptimized(one, two, function); // Paper - optimise collisions + } + + public static boolean joinIsNotEmpty(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) { +- if (predicate.apply(false, false)) { +- throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException()); +- } else { +- boolean bl = shape1.isEmpty(); +- boolean bl2 = shape2.isEmpty(); +- if (!bl && !bl2) { +- if (shape1 == shape2) { +- return predicate.apply(true, true); +- } else { +- boolean bl3 = predicate.apply(true, false); +- boolean bl4 = predicate.apply(false, true); +- +- for (Direction.Axis axis : AxisCycle.AXIS_VALUES) { +- if (shape1.max(axis) < shape2.min(axis) - 1.0E-7) { +- return bl3 || bl4; +- } +- +- if (shape2.max(axis) < shape1.min(axis) - 1.0E-7) { +- return bl3 || bl4; +- } +- } +- +- IndexMerger indexMerger = createIndexMerger(1, shape1.getCoords(Direction.Axis.X), shape2.getCoords(Direction.Axis.X), bl3, bl4); +- IndexMerger indexMerger2 = createIndexMerger( +- indexMerger.size() - 1, shape1.getCoords(Direction.Axis.Y), shape2.getCoords(Direction.Axis.Y), bl3, bl4 +- ); +- IndexMerger indexMerger3 = createIndexMerger( +- (indexMerger.size() - 1) * (indexMerger2.size() - 1), shape1.getCoords(Direction.Axis.Z), shape2.getCoords(Direction.Axis.Z), bl3, bl4 +- ); +- return joinIsNotEmpty(indexMerger, indexMerger2, indexMerger3, shape1.shape, shape2.shape, predicate); +- } +- } else { +- return predicate.apply(!bl, !bl2); +- } +- } ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isJoinNonEmpty(shape1, shape2, predicate); // Paper - optimise collisions + } + + private static boolean joinIsNotEmpty( +@@ -219,70 +226,120 @@ public final class Shapes { + return maxDist; + } + +- public static boolean blockOccudes(VoxelShape shape, VoxelShape neighbor, Direction direction) { +- if (shape == block() && neighbor == block()) { ++ // Paper start - optimise collisions ++ public static boolean blockOccudes(final VoxelShape first, final VoxelShape second, final Direction direction) { ++ final boolean firstBlock = first == BLOCK; ++ final boolean secondBlock = second == BLOCK; ++ ++ if (firstBlock & secondBlock) { + return true; +- } else if (neighbor.isEmpty()) { ++ } ++ ++ if (first.isEmpty() | second.isEmpty()) { ++ return false; ++ } ++ ++ // we optimise getOpposite, so we can use it ++ // secondly, use our cache to retrieve sliced shape ++ final VoxelShape newFirst = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getFaceShapeClamped(direction); ++ if (newFirst.isEmpty()) { + return false; +- } else { +- Direction.Axis axis = direction.getAxis(); +- Direction.AxisDirection axisDirection = direction.getAxisDirection(); +- VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? shape : neighbor; +- VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? neighbor : shape; +- BooleanOp booleanOp = axisDirection == Direction.AxisDirection.POSITIVE ? BooleanOp.ONLY_FIRST : BooleanOp.ONLY_SECOND; +- return DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7) +- && DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7) +- && !joinIsNotEmpty(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), booleanOp); + } ++ final VoxelShape newSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getFaceShapeClamped(direction.getOpposite()); ++ if (newSecond.isEmpty()) { ++ return false; ++ } ++ ++ return !joinIsNotEmpty(newFirst, newSecond, BooleanOp.ONLY_FIRST); ++ // Paper end - optimise collisions + } + + public static VoxelShape getFaceShape(VoxelShape shape, Direction direction) { +- if (shape == block()) { +- return block(); +- } else { +- Direction.Axis axis = direction.getAxis(); +- boolean bl; +- int i; +- if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) { +- bl = DoubleMath.fuzzyEquals(shape.max(axis), 1.0, 1.0E-7); +- i = shape.shape.getSize(axis) - 1; +- } else { +- bl = DoubleMath.fuzzyEquals(shape.min(axis), 0.0, 1.0E-7); +- i = 0; +- } ++ return ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$getFaceShapeClamped(direction); // Paper - optimise collisions ++ } + +- return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i)); +- } ++ // Paper start - optimise collisions ++ private static boolean mergedMayOccludeBlock(final VoxelShape shape1, final VoxelShape shape2) { ++ // if the combined bounds of the two shapes cannot occlude, then neither can the merged ++ final AABB bounds1 = shape1.bounds(); ++ final AABB bounds2 = shape2.bounds(); ++ ++ final double minX = Math.min(bounds1.minX, bounds2.minX); ++ final double minY = Math.min(bounds1.minY, bounds2.minY); ++ final double minZ = Math.min(bounds1.minZ, bounds2.minZ); ++ ++ final double maxX = Math.max(bounds1.maxX, bounds2.maxX); ++ final double maxY = Math.max(bounds1.maxY, bounds2.maxY); ++ final double maxZ = Math.max(bounds1.maxZ, bounds2.maxZ); ++ ++ return (minX <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && maxX >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) && ++ (minY <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && maxY >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) && ++ (minZ <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && maxZ >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)); + } ++ // Paper end - optimise collisions + +- public static boolean mergedFaceOccludes(VoxelShape one, VoxelShape two, Direction direction) { +- if (one != block() && two != block()) { +- Direction.Axis axis = direction.getAxis(); +- Direction.AxisDirection axisDirection = direction.getAxisDirection(); +- VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? one : two; +- VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? two : one; +- if (!DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7)) { +- voxelShape = empty(); +- } ++ // Paper start - optimise collisions ++ public static boolean mergedFaceOccludes(final VoxelShape first, final VoxelShape second, final Direction direction) { ++ // see if any of the shapes on their own occludes, only if cached ++ if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$occludesFullBlockIfCached() || ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$occludesFullBlockIfCached()) { ++ return true; ++ } + +- if (!DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7)) { +- voxelShape2 = empty(); +- } ++ if (first.isEmpty() & second.isEmpty()) { ++ return false; ++ } + +- return !joinIsNotEmpty( +- block(), +- joinUnoptimized(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), BooleanOp.OR), +- BooleanOp.ONLY_FIRST +- ); +- } else { ++ // we optimise getOpposite, so we can use it ++ // secondly, use our cache to retrieve sliced shape ++ final VoxelShape newFirst = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getFaceShapeClamped(direction); ++ final VoxelShape newSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getFaceShapeClamped(direction.getOpposite()); ++ ++ // see if any of the shapes on their own occludes, only if cached ++ if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newFirst).moonrise$occludesFullBlockIfCached() || ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newSecond).moonrise$occludesFullBlockIfCached()) { + return true; + } ++ ++ final boolean firstEmpty = newFirst.isEmpty(); ++ final boolean secondEmpty = newSecond.isEmpty(); ++ ++ if (firstEmpty & secondEmpty) { ++ return false; ++ } ++ ++ if (firstEmpty | secondEmpty) { ++ return secondEmpty ? ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newFirst).moonrise$occludesFullBlock() : ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newSecond).moonrise$occludesFullBlock(); ++ } ++ ++ if (newFirst == newSecond) { ++ return ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newFirst).moonrise$occludesFullBlock(); ++ } ++ ++ return mergedMayOccludeBlock(newFirst, newSecond) && ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)newFirst).moonrise$orUnoptimized(newSecond)).moonrise$occludesFullBlock(); + } ++ // Paper end - optimise collisions ++ ++ // Paper start - optimise collisions ++ public static boolean faceShapeOccludes(final VoxelShape shape1, final VoxelShape shape2) { ++ if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape1).moonrise$occludesFullBlockIfCached() || ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape2).moonrise$occludesFullBlockIfCached()) { ++ return true; ++ } ++ ++ final boolean s1Empty = shape1.isEmpty(); ++ final boolean s2Empty = shape2.isEmpty(); ++ if (s1Empty & s2Empty) { ++ return false; ++ } ++ ++ if (s1Empty | s2Empty) { ++ return s2Empty ? ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape1).moonrise$occludesFullBlock() : ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape2).moonrise$occludesFullBlock(); ++ } ++ ++ if (shape1 == shape2) { ++ return ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape1).moonrise$occludesFullBlock(); ++ } + +- public static boolean faceShapeOccludes(VoxelShape one, VoxelShape two) { +- return one == block() +- || two == block() +- || (!one.isEmpty() || !two.isEmpty()) && !joinIsNotEmpty(block(), joinUnoptimized(one, two, BooleanOp.OR), BooleanOp.ONLY_FIRST); ++ return mergedMayOccludeBlock(shape1, shape2) && ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape1).moonrise$orUnoptimized(shape2)).moonrise$occludesFullBlock(); ++ // Paper end - optimise collisions + } + + @VisibleForTesting +diff --git a/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java b/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java +index b07f1c58e00d232e7c83e6df3499e4b677645609..b88c71f27996d24d29048e06a69a004617eb53a2 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java +@@ -12,6 +12,7 @@ public class SliceShape extends VoxelShape { + super(makeSlice(shape.shape, axis, sliceWidth)); + this.delegate = shape; + this.axis = axis; ++ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this).moonrise$initCache(); // Paper - optimise collisions + } + + private static DiscreteVoxelShape makeSlice(DiscreteVoxelShape voxelSet, Direction.Axis axis, int sliceWidth) { +diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java +index d440003ca4403511a964f61bcf67ac2cd75c5359..11824d39e72fa003b3a56aa9b8d679fe8e23a1a4 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java +@@ -15,38 +15,505 @@ import net.minecraft.world.phys.AABB; + import net.minecraft.world.phys.BlockHitResult; + import net.minecraft.world.phys.Vec3; + +-public abstract class VoxelShape { +- protected final DiscreteVoxelShape shape; ++public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape { // Paper - optimise collisions ++ public final DiscreteVoxelShape shape; // Paper - optimise collisions - public + @Nullable + private VoxelShape[] faces; + ++ // Paper start - optimise collisions ++ private double offsetX; ++ private double offsetY; ++ private double offsetZ; ++ private AABB singleAABBRepresentation; ++ private double[] rootCoordinatesX; ++ private double[] rootCoordinatesY; ++ private double[] rootCoordinatesZ; ++ private ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData; ++ private boolean isEmpty; ++ private ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs; ++ private AABB cachedBounds; ++ private Boolean isFullBlock; ++ private Boolean occludesFullBlock; ++ ++ // must be power of two ++ private static final int MERGED_CACHE_SIZE = 16; ++ private ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache[] mergedORCache; ++ ++ @Override ++ public final double moonrise$offsetX() { ++ return this.offsetX; ++ } ++ ++ @Override ++ public final double moonrise$offsetY() { ++ return this.offsetY; ++ } ++ ++ @Override ++ public final double moonrise$offsetZ() { ++ return this.offsetZ; ++ } ++ ++ @Override ++ public final AABB moonrise$getSingleAABBRepresentation() { ++ return this.singleAABBRepresentation; ++ } ++ ++ @Override ++ public final double[] moonrise$rootCoordinatesX() { ++ return this.rootCoordinatesX; ++ } ++ ++ @Override ++ public final double[] moonrise$rootCoordinatesY() { ++ return this.rootCoordinatesY; ++ } ++ ++ @Override ++ public final double[] moonrise$rootCoordinatesZ() { ++ return this.rootCoordinatesZ; ++ } ++ ++ private static double[] extractRawArray(final DoubleList list) { ++ if (list instanceof it.unimi.dsi.fastutil.doubles.DoubleArrayList rawList) { ++ final double[] raw = rawList.elements(); ++ final int expected = rawList.size(); ++ if (raw.length == expected) { ++ return raw; ++ } else { ++ return java.util.Arrays.copyOf(raw, expected); ++ } ++ } else { ++ return list.toDoubleArray(); ++ } ++ } ++ ++ @Override ++ public final void moonrise$initCache() { ++ this.cachedShapeData = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape)this.shape).moonrise$getOrCreateCachedShapeData(); ++ this.isEmpty = this.cachedShapeData.isEmpty(); ++ ++ final DoubleList xList = this.getCoords(Direction.Axis.X); ++ final DoubleList yList = this.getCoords(Direction.Axis.Y); ++ final DoubleList zList = this.getCoords(Direction.Axis.Z); ++ ++ if (xList instanceof OffsetDoubleList offsetDoubleList) { ++ this.offsetX = offsetDoubleList.offset; ++ this.rootCoordinatesX = extractRawArray(offsetDoubleList.delegate); ++ } else { ++ this.rootCoordinatesX = extractRawArray(xList); ++ } ++ ++ if (yList instanceof OffsetDoubleList offsetDoubleList) { ++ this.offsetY = offsetDoubleList.offset; ++ this.rootCoordinatesY = extractRawArray(offsetDoubleList.delegate); ++ } else { ++ this.rootCoordinatesY = extractRawArray(yList); ++ } ++ ++ if (zList instanceof OffsetDoubleList offsetDoubleList) { ++ this.offsetZ = offsetDoubleList.offset; ++ this.rootCoordinatesZ = extractRawArray(offsetDoubleList.delegate); ++ } else { ++ this.rootCoordinatesZ = extractRawArray(zList); ++ } ++ ++ if (this.cachedShapeData.hasSingleAABB()) { ++ this.singleAABBRepresentation = new AABB( ++ this.rootCoordinatesX[0] + this.offsetX, this.rootCoordinatesY[0] + this.offsetY, this.rootCoordinatesZ[0] + this.offsetZ, ++ this.rootCoordinatesX[1] + this.offsetX, this.rootCoordinatesY[1] + this.offsetY, this.rootCoordinatesZ[1] + this.offsetZ ++ ); ++ this.cachedBounds = this.singleAABBRepresentation; ++ } ++ } ++ ++ @Override ++ public final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getCachedVoxelData() { ++ return this.cachedShapeData; ++ } ++ ++ private VoxelShape[] faceShapeClampedCache; ++ ++ @Override ++ public final VoxelShape moonrise$getFaceShapeClamped(final Direction direction) { ++ if (this.isEmpty) { ++ return (VoxelShape)(Object)this; ++ } ++ if ((VoxelShape)(Object)this == Shapes.block()) { ++ return (VoxelShape)(Object)this; ++ } ++ ++ VoxelShape[] cache = this.faceShapeClampedCache; ++ if (cache != null) { ++ final VoxelShape ret = cache[direction.ordinal()]; ++ if (ret != null) { ++ return ret; ++ } ++ } ++ ++ ++ if (cache == null) { ++ this.faceShapeClampedCache = cache = new VoxelShape[6]; ++ } ++ ++ final Direction.Axis axis = direction.getAxis(); ++ ++ final VoxelShape ret; ++ ++ if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) { ++ if (DoubleMath.fuzzyEquals(this.max(axis), 1.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) { ++ ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1)); ++ } else { ++ ret = Shapes.empty(); ++ } ++ } else { ++ if (DoubleMath.fuzzyEquals(this.min(axis), 0.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) { ++ ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, 0)); ++ } else { ++ ret = Shapes.empty(); ++ } ++ } ++ ++ cache[direction.ordinal()] = ret; ++ ++ return ret; ++ } ++ ++ private static VoxelShape tryForceBlock(final VoxelShape other) { ++ if (other == Shapes.block()) { ++ return other; ++ } ++ ++ final AABB otherAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)other).moonrise$getSingleAABBRepresentation(); ++ if (otherAABB == null) { ++ return other; ++ } ++ ++ if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)Shapes.block()).moonrise$getSingleAABBRepresentation().equals(otherAABB)) { ++ return Shapes.block(); ++ } ++ ++ return other; ++ } ++ ++ private boolean computeOccludesFullBlock() { ++ if (this.isEmpty) { ++ this.occludesFullBlock = Boolean.FALSE; ++ return false; ++ } ++ ++ if (this.moonrise$isFullBlock()) { ++ this.occludesFullBlock = Boolean.TRUE; ++ return true; ++ } ++ ++ final AABB singleAABB = this.singleAABBRepresentation; ++ if (singleAABB != null) { ++ // check if the bounding box encloses the full cube ++ final boolean ret = ++ (singleAABB.minY <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && singleAABB.maxY >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) && ++ (singleAABB.minX <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && singleAABB.maxX >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) && ++ (singleAABB.minZ <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && singleAABB.maxZ >= (1 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)); ++ this.occludesFullBlock = Boolean.valueOf(ret); ++ return ret; ++ } ++ ++ final boolean ret = !Shapes.joinIsNotEmpty(Shapes.block(), ((VoxelShape)(Object)this), BooleanOp.ONLY_FIRST); ++ this.occludesFullBlock = Boolean.valueOf(ret); ++ return ret; ++ } ++ ++ @Override ++ public final boolean moonrise$occludesFullBlock() { ++ final Boolean ret = this.occludesFullBlock; ++ if (ret != null) { ++ return ret.booleanValue(); ++ } ++ ++ return this.computeOccludesFullBlock(); ++ } ++ ++ @Override ++ public final boolean moonrise$occludesFullBlockIfCached() { ++ final Boolean ret = this.occludesFullBlock; ++ return ret != null ? ret.booleanValue() : false; ++ } ++ ++ private static int hash(final VoxelShape key) { ++ return it.unimi.dsi.fastutil.HashCommon.mix(System.identityHashCode(key)); ++ } ++ ++ @Override ++ public final VoxelShape moonrise$orUnoptimized(final VoxelShape other) { ++ // don't cache simple cases ++ if (((VoxelShape)(Object)this) == other) { ++ return other; ++ } ++ ++ if (this.isEmpty) { ++ return other; ++ } ++ ++ if (other.isEmpty()) { ++ return (VoxelShape)(Object)this; ++ } ++ ++ // try this cache first ++ final int thisCacheKey = hash(other) & (MERGED_CACHE_SIZE - 1); ++ final ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache cached = this.mergedORCache == null ? null : this.mergedORCache[thisCacheKey]; ++ if (cached != null && cached.key() == other) { ++ return cached.result(); ++ } ++ ++ // try other cache ++ final int otherCacheKey = hash((VoxelShape)(Object)this) & (MERGED_CACHE_SIZE - 1); ++ final ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache otherCache = ((VoxelShape)(Object)other).mergedORCache == null ? null : ((VoxelShape)(Object)other).mergedORCache[otherCacheKey]; ++ if (otherCache != null && otherCache.key() == (VoxelShape)(Object)this) { ++ return otherCache.result(); ++ } ++ ++ // note: unsure if joinUnoptimized(1, 2, OR) == joinUnoptimized(2, 1, OR) for all cases ++ final VoxelShape result = Shapes.joinUnoptimized((VoxelShape)(Object)this, other, BooleanOp.OR); ++ ++ if (cached != null && otherCache == null) { ++ // try to use second cache instead of replacing an entry in this cache ++ if (((VoxelShape)(Object)other).mergedORCache == null) { ++ ((VoxelShape)(Object)other).mergedORCache = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache[MERGED_CACHE_SIZE]; ++ } ++ ((VoxelShape)(Object)other).mergedORCache[otherCacheKey] = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache((VoxelShape)(Object)this, result); ++ } else { ++ // line is not occupied or other cache line is full ++ // always bias to replace this cache, as this cache is the first we check ++ if (this.mergedORCache == null) { ++ this.mergedORCache = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache[MERGED_CACHE_SIZE]; ++ } ++ this.mergedORCache[thisCacheKey] = new ca.spottedleaf.moonrise.patches.collisions.shape.MergedORCache(other, result); ++ } ++ ++ return result; ++ } ++ ++ private static DoubleList offsetList(final DoubleList src, final double by) { ++ if (src instanceof OffsetDoubleList offsetDoubleList) { ++ return new OffsetDoubleList(offsetDoubleList.delegate, by + offsetDoubleList.offset); ++ } ++ return new OffsetDoubleList(src, by); ++ } ++ ++ private List<AABB> toAabbsUncached() { ++ final List<AABB> ret = new java.util.ArrayList<>(); ++ if (this.singleAABBRepresentation != null) { ++ ret.add(this.singleAABBRepresentation); ++ } else { ++ final double[] coordsX = this.rootCoordinatesX; ++ final double[] coordsY = this.rootCoordinatesY; ++ final double[] coordsZ = this.rootCoordinatesZ; ++ ++ final double offX = this.offsetX; ++ final double offY = this.offsetY; ++ final double offZ = this.offsetZ; ++ ++ this.shape.forAllBoxes((final int minX, final int minY, final int minZ, ++ final int maxX, final int maxY, final int maxZ) -> { ++ ret.add(new AABB( ++ coordsX[minX] + offX, ++ coordsY[minY] + offY, ++ coordsZ[minZ] + offZ, ++ ++ ++ coordsX[maxX] + offX, ++ coordsY[maxY] + offY, ++ coordsZ[maxZ] + offZ ++ )); ++ }, true); ++ } ++ ++ // cache result ++ this.cachedToAABBs = new ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs(ret, false, 0.0, 0.0, 0.0); ++ ++ return ret; ++ } ++ ++ private boolean computeFullBlock() { ++ Boolean ret; ++ if (this.isEmpty) { ++ ret = Boolean.FALSE; ++ } else if ((VoxelShape)(Object)this == Shapes.block()) { ++ ret = Boolean.TRUE; ++ } else { ++ final AABB singleAABB = this.singleAABBRepresentation; ++ if (singleAABB == null) { ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData; ++ final int sMinX = shapeData.minFullX(); ++ final int sMinY = shapeData.minFullY(); ++ final int sMinZ = shapeData.minFullZ(); ++ ++ final int sMaxX = shapeData.maxFullX(); ++ final int sMaxY = shapeData.maxFullY(); ++ final int sMaxZ = shapeData.maxFullZ(); ++ ++ if (Math.abs(this.rootCoordinatesX[sMinX] + this.offsetX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(this.rootCoordinatesY[sMinY] + this.offsetY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(this.rootCoordinatesZ[sMinZ] + this.offsetZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && ++ ++ Math.abs(1.0 - (this.rootCoordinatesX[sMaxX] + this.offsetX)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(1.0 - (this.rootCoordinatesY[sMaxY] + this.offsetY)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(1.0 - (this.rootCoordinatesZ[sMaxZ] + this.offsetZ)) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) { ++ ++ // index = z + y*sizeZ + x*(sizeZ*sizeY) ++ ++ final int sizeY = shapeData.sizeY(); ++ final int sizeZ = shapeData.sizeZ(); ++ ++ final long[] bitset = shapeData.voxelSet(); ++ ++ ret = Boolean.TRUE; ++ ++ check_full: ++ for (int x = sMinX; x < sMaxX; ++x) { ++ for (int y = sMinY; y < sMaxY; ++y) { ++ final int baseIndex = y*sizeZ + x*(sizeZ*sizeY); ++ if (!ca.spottedleaf.moonrise.common.util.FlatBitsetUtil.isRangeSet(bitset, baseIndex + sMinZ, baseIndex + sMaxZ)) { ++ ret = Boolean.FALSE; ++ break check_full; ++ } ++ } ++ } ++ } else { ++ ret = Boolean.FALSE; ++ } ++ } else { ++ ret = Boolean.valueOf( ++ Math.abs(singleAABB.minX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(singleAABB.minY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(singleAABB.minZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && ++ ++ Math.abs(1.0 - singleAABB.maxX) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(1.0 - singleAABB.maxY) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(1.0 - singleAABB.maxZ) <= ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON ++ ); ++ } ++ } ++ ++ this.isFullBlock = ret; ++ ++ return ret.booleanValue(); ++ } ++ ++ @Override ++ public final boolean moonrise$isFullBlock() { ++ final Boolean ret = this.isFullBlock; ++ ++ if (ret != null) { ++ return ret.booleanValue(); ++ } ++ ++ return this.computeFullBlock(); ++ } ++ ++ private static BlockHitResult clip(final AABB aabb, final Vec3 from, final Vec3 to, final BlockPos offset) { ++ final double[] minDistanceArr = new double[] { 1.0 }; ++ final double diffX = to.x - from.x; ++ final double diffY = to.y - from.y; ++ final double diffZ = to.z - from.z; ++ ++ final Direction direction = AABB.getDirection(aabb.move(offset), from, minDistanceArr, null, diffX, diffY, diffZ); ++ ++ if (direction == null) { ++ return null; ++ } ++ ++ final double minDistance = minDistanceArr[0]; ++ return new BlockHitResult(from.add(minDistance * diffX, minDistance * diffY, minDistance * diffZ), direction, offset, false); ++ } ++ // Paper end - optimise collisions ++ + protected VoxelShape(DiscreteVoxelShape voxels) { + this.shape = voxels; + } + + public double min(Direction.Axis axis) { +- int i = this.shape.firstFull(axis); +- return i >= this.shape.getSize(axis) ? Double.POSITIVE_INFINITY : this.get(axis, i); ++ // Paper start - optimise collisions ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData; ++ switch (axis) { ++ case X: { ++ final int idx = shapeData.minFullX(); ++ return idx >= shapeData.sizeX() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX); ++ } ++ case Y: { ++ final int idx = shapeData.minFullY(); ++ return idx >= shapeData.sizeY() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY); ++ } ++ case Z: { ++ final int idx = shapeData.minFullZ(); ++ return idx >= shapeData.sizeZ() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ); ++ } ++ default: { ++ // should never get here ++ return Double.POSITIVE_INFINITY; ++ } ++ } ++ // Paper end - optimise collisions + } + + public double max(Direction.Axis axis) { +- int i = this.shape.lastFull(axis); +- return i <= 0 ? Double.NEGATIVE_INFINITY : this.get(axis, i); ++ // Paper start - optimise collisions ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData; ++ switch (axis) { ++ case X: { ++ final int idx = shapeData.maxFullX(); ++ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX); ++ } ++ case Y: { ++ final int idx = shapeData.maxFullY(); ++ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY); ++ } ++ case Z: { ++ final int idx = shapeData.maxFullZ(); ++ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ); ++ } ++ default: { ++ // should never get here ++ return Double.NEGATIVE_INFINITY; ++ } ++ } ++ // Paper end - optimise collisions + } + + public AABB bounds() { +- if (this.isEmpty()) { +- throw (UnsupportedOperationException)Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape.")); +- } else { +- return new AABB( +- this.min(Direction.Axis.X), +- this.min(Direction.Axis.Y), +- this.min(Direction.Axis.Z), +- this.max(Direction.Axis.X), +- this.max(Direction.Axis.Y), +- this.max(Direction.Axis.Z) +- ); ++ // Paper start - optimise collisions ++ if (this.isEmpty) { ++ throw Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape.")); ++ } ++ AABB cached = this.cachedBounds; ++ if (cached != null) { ++ return cached; + } ++ ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeData = this.cachedShapeData; ++ ++ final double[] coordsX = this.rootCoordinatesX; ++ final double[] coordsY = this.rootCoordinatesY; ++ final double[] coordsZ = this.rootCoordinatesZ; ++ ++ final double offX = this.offsetX; ++ final double offY = this.offsetY; ++ final double offZ = this.offsetZ; ++ ++ // note: if not empty, then there is one full AABB so no bounds checks are needed on the minFull/maxFull indices ++ cached = new AABB( ++ coordsX[shapeData.minFullX()] + offX, ++ coordsY[shapeData.minFullY()] + offY, ++ coordsZ[shapeData.minFullZ()] + offZ, ++ ++ coordsX[shapeData.maxFullX()] + offX, ++ coordsY[shapeData.maxFullY()] + offY, ++ coordsZ[shapeData.maxFullZ()] + offZ ++ ); ++ ++ this.cachedBounds = cached; ++ return cached; ++ // Paper end - optimise collisions + } + + public VoxelShape singleEncompassing() { +@@ -69,28 +536,95 @@ public abstract class VoxelShape { + public abstract DoubleList getCoords(Direction.Axis axis); + + public boolean isEmpty() { +- return this.shape.isEmpty(); ++ return this.isEmpty; // Paper - optimise collisions + } + + public VoxelShape move(double x, double y, double z) { +- return (VoxelShape)(this.isEmpty() +- ? Shapes.empty() +- : new ArrayVoxelShape( +- this.shape, +- new OffsetDoubleList(this.getCoords(Direction.Axis.X), x), +- new OffsetDoubleList(this.getCoords(Direction.Axis.Y), y), +- new OffsetDoubleList(this.getCoords(Direction.Axis.Z), z) +- )); ++ // Paper start - optimise collisions ++ if (this.isEmpty) { ++ return Shapes.empty(); ++ } ++ ++ final ArrayVoxelShape ret = new ArrayVoxelShape( ++ this.shape, ++ offsetList(this.getCoords(Direction.Axis.X), x), ++ offsetList(this.getCoords(Direction.Axis.Y), y), ++ offsetList(this.getCoords(Direction.Axis.Z), z) ++ ); ++ ++ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs = this.cachedToAABBs; ++ if (cachedToAABBs != null) { ++ ((VoxelShape)(Object)ret).cachedToAABBs = ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs.offset(cachedToAABBs, x, y, z); ++ } ++ ++ return ret; ++ // Paper end - optimise collisions + } + + public VoxelShape optimize() { +- VoxelShape[] voxelShapes = new VoxelShape[]{Shapes.empty()}; +- this.forAllBoxes( +- (minX, minY, minZ, maxX, maxY, maxZ) -> voxelShapes[0] = Shapes.joinUnoptimized( +- voxelShapes[0], Shapes.box(minX, minY, minZ, maxX, maxY, maxZ), BooleanOp.OR +- ) +- ); +- return voxelShapes[0]; ++ // Paper start - optimise collisions ++ if (this.isEmpty) { ++ return Shapes.empty(); ++ } ++ ++ if (this.singleAABBRepresentation != null) { ++ // note: the isFullBlock() is fuzzy, and Shapes.create() is also fuzzy which would return block() ++ return this.moonrise$isFullBlock() ? Shapes.block() : (VoxelShape)(Object)this; ++ } ++ ++ final List<AABB> aabbs = this.toAabbs(); ++ ++ if (aabbs.size() == 1) { ++ final AABB singleAABB = aabbs.get(0); ++ final VoxelShape ret = Shapes.create(singleAABB); ++ ++ // forward AABB cache ++ if (((VoxelShape)(Object)ret).cachedToAABBs == null) { ++ ((VoxelShape)(Object)ret).cachedToAABBs = this.cachedToAABBs; ++ } ++ ++ return ret; ++ } else { ++ // reduce complexity of joins by splitting the merges (old complexity: n^2, new: nlogn) ++ ++ // set up flat array so that this merge is done in-place ++ final VoxelShape[] tmp = new VoxelShape[aabbs.size()]; ++ ++ // initialise as unmerged ++ for (int i = 0, len = aabbs.size(); i < len; ++i) { ++ tmp[i] = Shapes.create(aabbs.get(i)); ++ } ++ ++ int size = aabbs.size(); ++ while (size > 1) { ++ int newSize = 0; ++ for (int i = 0; i < size; i += 2) { ++ final int next = i + 1; ++ if (next >= size) { ++ // nothing to merge with, so leave it for next iteration ++ tmp[newSize++] = tmp[i]; ++ break; ++ } else { ++ // merge with adjacent ++ final VoxelShape first = tmp[i]; ++ final VoxelShape second = tmp[next]; ++ ++ tmp[newSize++] = Shapes.joinUnoptimized(first, second, BooleanOp.OR); ++ } ++ } ++ size = newSize; ++ } ++ ++ final VoxelShape ret = tmp[0]; ++ ++ // forward AABB cache ++ if (((VoxelShape)(Object)ret).cachedToAABBs == null) { ++ ((VoxelShape)(Object)ret).cachedToAABBs = this.cachedToAABBs; ++ } ++ ++ return ret; ++ } ++ // Paper end - optimise collisions + } + + public void forAllEdges(Shapes.DoubleLineConsumer consumer) { +@@ -127,9 +661,24 @@ public abstract class VoxelShape { + } + + public List<AABB> toAabbs() { +- List<AABB> list = Lists.newArrayList(); +- this.forAllBoxes((x1, y1, z1, x2, y2, z2) -> list.add(new AABB(x1, y1, z1, x2, y2, z2))); +- return list; ++ // Paper start - optimise collisions ++ ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cachedToAABBs = this.cachedToAABBs; ++ if (cachedToAABBs != null) { ++ if (!cachedToAABBs.isOffset()) { ++ return cachedToAABBs.aabbs(); ++ } ++ ++ // all we need to do is offset the cache ++ cachedToAABBs = cachedToAABBs.removeOffset(); ++ // update cache ++ this.cachedToAABBs = cachedToAABBs; ++ ++ return cachedToAABBs.aabbs(); ++ } ++ ++ // make new cache ++ return this.toAabbsUncached(); ++ // Paper end - optimise collisions + } + + public double min(Direction.Axis axis, double from, double to) { +@@ -155,42 +704,63 @@ public abstract class VoxelShape { + } + + @Nullable +- public BlockHitResult clip(Vec3 start, Vec3 end, BlockPos pos) { +- if (this.isEmpty()) { ++ // Paper start - optimise collisions ++ public BlockHitResult clip(final Vec3 from, final Vec3 to, final BlockPos offset) { ++ if (this.isEmpty) { + return null; +- } else { +- Vec3 vec3 = end.subtract(start); +- if (vec3.lengthSqr() < 1.0E-7) { +- return null; +- } else { +- Vec3 vec32 = start.add(vec3.scale(0.001)); +- return this.shape +- .isFullWide( +- this.findIndex(Direction.Axis.X, vec32.x - (double)pos.getX()), +- this.findIndex(Direction.Axis.Y, vec32.y - (double)pos.getY()), +- this.findIndex(Direction.Axis.Z, vec32.z - (double)pos.getZ()) +- ) +- ? new BlockHitResult(vec32, Direction.getNearest(vec3.x, vec3.y, vec3.z).getOpposite(), pos, true) +- : AABB.clip(this.toAabbs(), start, end, pos); ++ } ++ ++ final Vec3 directionOpposite = to.subtract(from); ++ if (directionOpposite.lengthSqr() < ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) { ++ return null; ++ } ++ ++ final Vec3 fromBehind = from.add(directionOpposite.scale(0.001)); ++ final double fromBehindOffsetX = fromBehind.x - (double)offset.getX(); ++ final double fromBehindOffsetY = fromBehind.y - (double)offset.getY(); ++ final double fromBehindOffsetZ = fromBehind.z - (double)offset.getZ(); ++ ++ final AABB singleAABB = this.singleAABBRepresentation; ++ if (singleAABB != null) { ++ if (singleAABB.contains(fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) { ++ return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true); + } ++ return clip(singleAABB, from, to, offset); ++ } ++ ++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.strictlyContains((VoxelShape)(Object)this, fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) { ++ return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true); + } ++ ++ return AABB.clip(((VoxelShape)(Object)this).toAabbs(), from, to, offset); ++ // Paper end - optimise collisions + } + +- public Optional<Vec3> closestPointTo(Vec3 target) { +- if (this.isEmpty()) { ++ // Paper start - optimise collisions ++ public Optional<Vec3> closestPointTo(Vec3 point) { ++ if (this.isEmpty) { + return Optional.empty(); +- } else { +- Vec3[] vec3s = new Vec3[1]; +- this.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { +- double d = Mth.clamp(target.x(), minX, maxX); +- double e = Mth.clamp(target.y(), minY, maxY); +- double f = Mth.clamp(target.z(), minZ, maxZ); +- if (vec3s[0] == null || target.distanceToSqr(d, e, f) < target.distanceToSqr(vec3s[0])) { +- vec3s[0] = new Vec3(d, e, f); +- } +- }); +- return Optional.of(vec3s[0]); + } ++ ++ Vec3 ret = null; ++ double retDistance = Double.MAX_VALUE; ++ ++ final List<AABB> aabbs = this.toAabbs(); ++ for (int i = 0, len = aabbs.size(); i < len; ++i) { ++ final AABB aabb = aabbs.get(i); ++ final double x = Mth.clamp(point.x, aabb.minX, aabb.maxX); ++ final double y = Mth.clamp(point.y, aabb.minY, aabb.maxY); ++ final double z = Mth.clamp(point.z, aabb.minZ, aabb.maxZ); ++ ++ double dist = point.distanceToSqr(x, y, z); ++ if (dist < retDistance) { ++ ret = new Vec3(x, y, z); ++ retDistance = dist; ++ } ++ } ++ ++ return Optional.ofNullable(ret); ++ // Paper end - optimise collisions + } + + public VoxelShape getFaceShape(Direction facing) { +@@ -226,8 +796,29 @@ public abstract class VoxelShape { + } + } + +- public double collide(Direction.Axis axis, AABB box, double maxDist) { +- return this.collideX(AxisCycle.between(axis, Direction.Axis.X), box, maxDist); ++ // Paper start - optimise collisions ++ public double collide(final Direction.Axis axis, final AABB source, final double source_move) { ++ if (this.isEmpty) { ++ return source_move; ++ } ++ if (Math.abs(source_move) < ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) { ++ return 0.0; ++ } ++ switch (axis) { ++ case X: { ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.collideX((VoxelShape)(Object)this, source, source_move); ++ } ++ case Y: { ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.collideY((VoxelShape)(Object)this, source, source_move); ++ } ++ case Z: { ++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.collideZ((VoxelShape)(Object)this, source, source_move); ++ } ++ default: { ++ throw new RuntimeException("Unknown axis: " + axis); ++ } ++ } ++ // Paper end - optimise collisions + } + + protected double collideX(AxisCycle axisCycle, AABB box, double maxDist) { +diff --git a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java +index 47c2b2da9799690291396effb9e1b06d71efc6fd..c42c0d1e4da30aa15f32d4ca524aeabd26fc50cf 100644 +--- a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java ++++ b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java +@@ -18,7 +18,7 @@ import net.minecraft.core.BlockPos; + import net.minecraft.nbt.ListTag; + import net.minecraft.world.level.ChunkPos; + +-public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickContainerAccess<T> { ++public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickContainerAccess<T>, ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks { // Paper - rewrite chunk system + private final Queue<ScheduledTick<T>> tickQueue = new PriorityQueue<>(ScheduledTick.DRAIN_ORDER); + @Nullable + private List<SavedTick<T>> pendingTicks; +@@ -26,6 +26,30 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon + @Nullable + private BiConsumer<LevelChunkTicks<T>, ScheduledTick<T>> onTickAdded; + ++ // Paper start - rewrite chunk system ++ /* ++ * Since ticks are saved using relative delays, we need to consider the entire tick list dirty when there are scheduled ticks ++ * and the last saved tick is not equal to the current tick ++ */ ++ /* ++ * In general, it would be nice to be able to "re-pack" ticks once the chunk becomes non-ticking again, but that is a ++ * bit out of scope for the chunk system ++ */ ++ ++ private boolean dirty; ++ private long lastSaved = Long.MIN_VALUE; ++ ++ @Override ++ public final boolean moonrise$isDirty(final long tick) { ++ return this.dirty || (!this.tickQueue.isEmpty() && tick != this.lastSaved); ++ } ++ ++ @Override ++ public final void moonrise$clearDirty() { ++ this.dirty = false; ++ } ++ // Paper end - rewrite chunk system ++ + public LevelChunkTicks() { + } + +@@ -50,7 +74,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon + public ScheduledTick<T> poll() { + ScheduledTick<T> scheduledTick = this.tickQueue.poll(); + if (scheduledTick != null) { +- this.ticksPerPosition.remove(scheduledTick); ++ this.ticksPerPosition.remove(scheduledTick); this.dirty = true; // Paper - rewrite chunk system + } + + return scheduledTick; +@@ -59,7 +83,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon + @Override + public void schedule(ScheduledTick<T> orderedTick) { + if (this.ticksPerPosition.add(orderedTick)) { +- this.scheduleUnchecked(orderedTick); ++ this.scheduleUnchecked(orderedTick); this.dirty = true; // Paper - rewrite chunk system + } + } + +@@ -81,7 +105,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon + while (iterator.hasNext()) { + ScheduledTick<T> scheduledTick = iterator.next(); + if (predicate.test(scheduledTick)) { +- iterator.remove(); ++ iterator.remove(); this.dirty = true; // Paper - rewrite chunk system + this.ticksPerPosition.remove(scheduledTick); + } + } +@@ -98,6 +122,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon + + @Override + public ListTag save(long l, Function<T, String> function) { ++ this.lastSaved = l; // Paper - rewrite chunk system + ListTag listTag = new ListTag(); + if (this.pendingTicks != null) { + for (SavedTick<T> savedTick : this.pendingTicks) { +@@ -114,6 +139,7 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, TickCon + + public void unpack(long time) { + if (this.pendingTicks != null) { ++ this.lastSaved = time; // Paper - rewrite chunk system + int i = -this.pendingTicks.size(); + + for (SavedTick<T> savedTick : this.pendingTicks) { +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +index 69c7fe5bf5b914276a9f7a0e57ce668e569d91f9..33322b57b4c6922f4daad0f584733f0f24083911 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +@@ -82,6 +82,12 @@ public class CraftChunk implements Chunk { + } + + public ChunkAccess getHandle(ChunkStatus chunkStatus) { ++ // Paper start - rewrite chunk system ++ net.minecraft.world.level.chunk.LevelChunk full = this.worldServer.getChunkIfLoaded(this.x, this.z); ++ if (full != null) { ++ return full; ++ } ++ // Paper end - rewrite chunk system + ChunkAccess chunkAccess = this.worldServer.getChunk(this.x, this.z, chunkStatus); + + // SPIGOT-7332: Get unwrapped extension +@@ -116,60 +122,12 @@ public class CraftChunk implements Chunk { + + @Override + public boolean isEntitiesLoaded() { +- return this.getCraftWorld().getHandle().entityManager.areEntitiesLoaded(ChunkPos.asLong(this.x, this.z)); ++ return this.getCraftWorld().getHandle().areEntitiesLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(this.x, this.z)); // Paper - rewrite chunk system + } + + @Override + public Entity[] getEntities() { +- if (!this.isLoaded()) { +- this.getWorld().getChunkAt(this.x, this.z); // Transient load for this tick +- } +- +- PersistentEntitySectionManager<net.minecraft.world.entity.Entity> entityManager = this.getCraftWorld().getHandle().entityManager; +- long pair = ChunkPos.asLong(this.x, this.z); +- +- if (entityManager.areEntitiesLoaded(pair)) { +- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream() +- .map(net.minecraft.world.entity.Entity::getBukkitEntity) +- .filter(Objects::nonNull).toArray(Entity[]::new); +- } +- +- entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading +- +- // SPIGOT-6772: Use entity mailbox and re-schedule entities if they get unloaded +- ProcessorMailbox<Runnable> mailbox = ((EntityStorage) entityManager.permanentStorage).entityDeserializerQueue; +- BooleanSupplier supplier = () -> { +- // only execute inbox if our entities are not present +- if (entityManager.areEntitiesLoaded(pair)) { +- return true; +- } +- +- if (!entityManager.isPending(pair)) { +- // Our entities got unloaded, this should normally not happen. +- entityManager.ensureChunkQueuedForLoad(pair); // Re-start entity loading +- } +- +- // tick loading inbox, which loads the created entities to the world +- // (if present) +- entityManager.tick(); +- // check if our entities are loaded +- return entityManager.areEntitiesLoaded(pair); +- }; +- +- // now we wait until the entities are loaded, +- // the converting from NBT to entity object is done on the main Thread which is why we wait +- while (!supplier.getAsBoolean()) { +- if (mailbox.size() != 0) { +- mailbox.run(); +- } else { +- Thread.yield(); +- LockSupport.parkNanos("waiting for entity loading", 100000L); +- } +- } +- +- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream() +- .map(net.minecraft.world.entity.Entity::getBukkitEntity) +- .filter(Objects::nonNull).toArray(Entity[]::new); ++ return this.getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - rewrite chunk system + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 77f3ac4e45a691181a94831cf49f7840c9f88e3a..05e44a1448f30ceb8cecba2bed76f51aac5543f9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -1419,7 +1419,7 @@ public final class CraftServer implements Server { + // Paper - Put world into worldlist before initing the world; move up + + this.getServer().prepareLevels(internal.getChunkSource().chunkMap.progressListener, internal); +- internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API ++ // Paper - rewrite chunk system + + this.pluginManager.callEvent(new WorldLoadEvent(internal.getWorld())); + return internal.getWorld(); +@@ -1464,7 +1464,7 @@ public final class CraftServer implements Server { + } + + handle.getChunkSource().close(save); +- handle.entityManager.close(save); // SPIGOT-6722: close entityManager ++ // Paper - rewrite chunk system + handle.convertable.close(); + } catch (Exception ex) { + this.getLogger().log(Level.SEVERE, null, ex); +@@ -2500,7 +2500,7 @@ public final class CraftServer implements Server { + + @Override + public boolean isPrimaryThread() { +- return Thread.currentThread().equals(this.console.serverThread) || this.console.hasStopped() || !org.spigotmc.AsyncCatcher.enabled; // All bets are off if we have shut down (e.g. due to watchdog) ++ return io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system + } + + // Paper start - Adventure +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 8f88ccec6b8947ca2738dc07c23aebe258145c83..cdc704364cf339084537d089e654f6078f8be783 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -456,10 +456,14 @@ public class CraftWorld extends CraftRegionAccessor implements World { + ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); + if (playerChunk == null) return false; + +- playerChunk.getTickingChunkFuture().thenAccept(either -> { +- either.ifSuccess(chunk -> { ++ // Paper start - chunk system ++ net.minecraft.world.level.chunk.LevelChunk chunk = playerChunk.getChunkToSend(); ++ if (chunk == null) { ++ return false; ++ } ++ // Paper end - chunk system + List<ServerPlayer> playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false); +- if (playersInRange.isEmpty()) return; ++ if (playersInRange.isEmpty()) return true; // Paper - chunk system + + ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, this.world.getLightEngine(), null, null); + for (ServerPlayer player : playersInRange) { +@@ -467,8 +471,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + player.connection.send(refreshPacket); + } +- }); +- }); ++ // Paper - chunk system + + return true; + } +@@ -572,20 +575,8 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + public Collection<Plugin> getPluginChunkTickets(int x, int z) { + DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; +- SortedArraySet<Ticket<?>> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z)); + +- if (tickets == null) { +- return Collections.emptyList(); +- } +- +- ImmutableList.Builder<Plugin> ret = ImmutableList.builder(); +- for (Ticket<?> ticket : tickets) { +- if (ticket.getType() == TicketType.PLUGIN_TICKET) { +- ret.add((Plugin) ticket.key); +- } +- } +- +- return ret.build(); ++ return chunkDistanceManager.getChunkHolderManager().getPluginChunkTickets(x, z); // Paper - rewrite chunk system + } + + @Override +@@ -593,7 +584,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + Map<Plugin, ImmutableList.Builder<Chunk>> ret = new HashMap<>(); + DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; + +- for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) { ++ for (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> chunkTickets : chunkDistanceManager.getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system + long chunkKey = chunkTickets.getLongKey(); + SortedArraySet<Ticket<?>> tickets = chunkTickets.getValue(); + +@@ -1290,12 +1281,12 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public int getViewDistance() { +- return this.world.getChunkSource().chunkMap.serverViewDistance; ++ return this.getHandle().moonrise$getPlayerChunkLoader().getAPIViewDistance(); // Paper - rewrite chunk system + } + + @Override + public int getSimulationDistance() { +- return this.world.getChunkSource().chunkMap.getDistanceManager().simulationDistance; ++ return this.getHandle().moonrise$getPlayerChunkLoader().getAPITickDistance(); // Paper - rewrite chunk system + } + + public BlockMetadataStore getBlockMetadata() { +@@ -2433,17 +2424,20 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void setSimulationDistance(final int simulationDistance) { +- throw new UnsupportedOperationException("Not implemented yet"); ++ if (simulationDistance < 2 || simulationDistance > 32) { ++ throw new IllegalArgumentException("Simulation distance " + simulationDistance + " is out of range of [2, 32]"); ++ } ++ this.getHandle().chunkSource.setSimulationDistance(simulationDistance); // Paper - rewrite chunk system + } + + @Override + public int getSendViewDistance() { +- return this.getViewDistance(); ++ return this.getHandle().moonrise$getPlayerChunkLoader().getAPISendViewDistance(); // Paper - rewrite chunk system + } + + @Override + public void setSendViewDistance(final int viewDistance) { +- throw new UnsupportedOperationException("Not implemented yet"); ++ this.getHandle().chunkSource.setSendViewDistance(viewDistance); // Paper - rewrite chunk system + } + + // Paper start - implement pointers +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 3480f06d4476c5c200246e6200e2eda2a5de1a5a..62bfde27a182be24710eb3b72448e82da58afc8f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -3492,12 +3492,14 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public int getViewDistance() { +- return io.papermc.paper.chunk.system.ChunkSystem.getLoadViewDistance(this.getHandle()); ++ return io.papermc.paper.chunk.system.ChunkSystem.getLoadViewDistance(this.getHandle()) - 1; // Paper - rewrite chunk system - TODO do this better + } + + @Override + public void setViewDistance(final int viewDistance) { +- throw new UnsupportedOperationException("Not implemented yet"); ++ // Paper - rewrite chunk system - TODO do this better ++ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle()) ++ .moonrise$getViewDistanceHolder().setLoadViewDistance(viewDistance + 1); + } + + @Override +@@ -3507,7 +3509,9 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public void setSimulationDistance(final int simulationDistance) { +- throw new UnsupportedOperationException("Not implemented yet"); ++ // Paper - rewrite chunk system - TODO do this better ++ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle()) ++ .moonrise$getViewDistanceHolder().setTickViewDistance(simulationDistance); + } + + @Override +@@ -3517,6 +3521,8 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public void setSendViewDistance(final int viewDistance) { +- throw new UnsupportedOperationException("Not implemented yet"); ++ // Paper - rewrite chunk system - TODO do this better ++ ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle()) ++ .moonrise$getViewDistanceHolder().setSendViewDistance(viewDistance); + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java +index 5717c0e1d6df07a4613356dc78d970d2101c68d7..cab7ca4218e5903b6a5e518af55457b9a1b5111c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java ++++ b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java +@@ -263,7 +263,7 @@ public class CustomChunkGenerator extends InternalChunkGenerator { + return ichunkaccess1; + }; + +- return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), net.minecraft.Util.backgroundExecutor()) : future.thenApply(function); ++ return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), Runnable::run) : future.thenApply(function); // Paper - rewrite chunk system + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java +index fceed3d08ee6f4c171685986bb19d2be592eedc6..bf18f9ad7dec2b09ebfcb5ec6566f2556e842f22 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java +@@ -829,5 +829,12 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel { + public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) { + return this.handle.getChunkIfLoadedImmediately(x, z); + } ++ ++ // Paper start - rewrite chunk system ++ @Override ++ public java.util.List<net.minecraft.world.entity.Entity> moonrise$getHardCollidingEntities(final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB box, final java.util.function.Predicate<? super net.minecraft.world.entity.Entity> predicate) { ++ return this.handle.moonrise$getHardCollidingEntities(entity, box, predicate); ++ } ++ // Paper end - rewrite chunk system + // Paper end + } +diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java +index e8e3cc48cf1c58bd8151d1f28df28781859cd0e3..67c8e90d3a2a93d858371d7fc1c3aaac3fdef71c 100644 +--- a/src/main/java/org/spigotmc/AsyncCatcher.java ++++ b/src/main/java/org/spigotmc/AsyncCatcher.java +@@ -9,7 +9,7 @@ public class AsyncCatcher + + public static void catchOp(String reason) + { +- if ( (AsyncCatcher.enabled || io.papermc.paper.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Paper ++ if (!io.papermc.paper.util.TickThread.isTickThread()) // Paper // Paper - rewrite chunk system + { + MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); // Paper + throw new IllegalStateException( "Asynchronous " + reason + "!" ); +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index ad282d34919716b75acd10426cd071da9d064a51..bab2bd91528a8b2e9e74a3f9eb061d52619102f6 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -8,7 +8,7 @@ import java.util.logging.Logger; + import net.minecraft.server.MinecraftServer; + import org.bukkit.Bukkit; + +-public class WatchdogThread extends Thread ++public class WatchdogThread extends io.papermc.paper.util.TickThread // Paper - rewrite chunk system + { + + private static WatchdogThread instance; +@@ -115,6 +115,7 @@ public class WatchdogThread extends Thread + // Paper end - Different message for short timeout + log.log( Level.SEVERE, "------------------------------" ); + log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(MinecraftServer.getServer(), isLongTimeout); // Paper - rewrite chunk system + WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); + log.log( Level.SEVERE, "------------------------------" ); + // |