Paper/patches/server/0009-MC-Utils.patch
Spottedleaf 8c5b837e05 Rework async chunk api implementation
Firstly, the old methods all routed to the CompletableFuture method.
However, the CF method could not guarantee that if the caller
was off-main that the future would be "completed" on-main. Since
the callback methods used the CF one, this meant that the callback
methods did not guarantee that the callbacks were to be called on
the main thread.

Now, all methods route to getChunkAtAsync(x, z, gen, urgent, cb)
so that the methods with the callback are guaranteed to invoke
the callback on the main thread. The CF behavior remains unchanged;
it may still appear to complete on main if invoked off-main.

Secondly, remove the scheduleOnMain invocation in the async
chunk completion. This unnecessarily delays the callback
by 1 tick.

Thirdly, add getChunksAtAsync(minX, minZ, maxX, maxZ, ...) which
will load chunks within an area. This method is provided as a helper
as keeping all chunks loaded within an area can be complicated to
implement for plugins (due to the lacking ticket API), and is
already implemented internally anyways.

Fourthly, remove the ticket addition that occured with getChunkAt
and getChunkAtAsync. The ticket addition may delay the unloading
of the chunk unnecessarily. It also fixes a very rare timing bug
where the future/callback would be completed after the chunk
unloads.
2024-11-18 23:00:59 -08:00

6452 lines
261 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Mon, 28 Mar 2016 20:55:47 -0400
Subject: [PATCH] MC Utils
== AT ==
public net.minecraft.server.level.ServerChunkCache mainThread
public net.minecraft.server.level.ServerLevel chunkSource
public org.bukkit.craftbukkit.inventory.CraftItemStack handle
public net.minecraft.server.level.ChunkMap getVisibleChunkIfPresent(J)Lnet/minecraft/server/level/ChunkHolder;
public net.minecraft.server.level.ServerChunkCache mainThreadProcessor
public net.minecraft.server.level.ServerChunkCache$MainThreadExecutor
public net.minecraft.world.level.chunk.LevelChunkSection states
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java b/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
new file mode 100644
index 0000000000000000000000000000000000000000..6c98d420ea84c10ef4f15d4deb3f04e610ed8548
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
@@ -0,0 +1,117 @@
+package ca.spottedleaf.moonrise.common;
+
+import com.mojang.datafixers.DSL;
+import com.mojang.datafixers.DataFixer;
+import net.minecraft.core.BlockPos;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.GenerationChunkHolder;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.Entity;
+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.LevelChunk;
+import net.minecraft.world.level.chunk.ProtoChunk;
+import net.minecraft.world.level.chunk.storage.SerializableChunkData;
+import net.minecraft.world.level.entity.EntityTypeTest;
+import net.minecraft.world.phys.AABB;
+import java.util.List;
+import java.util.ServiceLoader;
+import java.util.function.Predicate;
+
+public interface PlatformHooks {
+ public static PlatformHooks get() {
+ return Holder.INSTANCE;
+ }
+
+ public String getBrand();
+
+ public int getLightEmission(final BlockState blockState, final BlockGetter world, final BlockPos pos);
+
+ public Predicate<BlockState> maybeHasLightEmission();
+
+ public boolean hasCurrentlyLoadingChunk();
+
+ public LevelChunk getCurrentlyLoadingChunk(final GenerationChunkHolder holder);
+
+ public void setCurrentlyLoading(final GenerationChunkHolder holder, final LevelChunk levelChunk);
+
+ public void chunkFullStatusComplete(final LevelChunk newChunk, final ProtoChunk original);
+
+ public boolean allowAsyncTicketUpdates();
+
+ public void onChunkHolderTicketChange(final ServerLevel world, final ChunkHolder holder, final int oldLevel, final int newLevel);
+
+ public void chunkUnloadFromWorld(final LevelChunk chunk);
+
+ public void chunkSyncSave(final ServerLevel world, final ChunkAccess chunk, final SerializableChunkData data);
+
+ public void onChunkWatch(final ServerLevel world, final LevelChunk chunk, final ServerPlayer player);
+
+ public void onChunkUnWatch(final ServerLevel world, final ChunkPos chunk, final ServerPlayer player);
+
+ public void addToGetEntities(final Level world, final Entity entity, final AABB boundingBox, final Predicate<? super Entity> predicate,
+ final List<Entity> into);
+
+ public <T extends Entity> void addToGetEntities(final Level world, final EntityTypeTest<Entity, T> entityTypeTest,
+ final AABB boundingBox, final Predicate<? super T> predicate,
+ final List<? super T> into, final int maxCount);
+
+ public void entityMove(final Entity entity, final long oldSection, final long newSection);
+
+ public boolean screenEntity(final ServerLevel world, final Entity entity, final boolean fromDisk, final boolean event);
+
+ public boolean configFixMC224294();
+
+ public boolean configAutoConfigSendDistance();
+
+ public double configPlayerMaxLoadRate();
+
+ public double configPlayerMaxGenRate();
+
+ public double configPlayerMaxSendRate();
+
+ public int configPlayerMaxConcurrentLoads();
+
+ public int configPlayerMaxConcurrentGens();
+
+ public long configAutoSaveInterval(final ServerLevel world);
+
+ public int configMaxAutoSavePerTick(final ServerLevel world);
+
+ public boolean configFixMC159283();
+
+ // support for CB chunk mustNotSave
+ public boolean forceNoSave(final ChunkAccess chunk);
+
+ public CompoundTag convertNBT(final DSL.TypeReference type, final DataFixer dataFixer, final CompoundTag nbt,
+ final int fromVersion, final int toVersion);
+
+ public boolean hasMainChunkLoadHook();
+
+ public void mainChunkLoad(final ChunkAccess chunk, final SerializableChunkData chunkData);
+
+ public List<Entity> modifySavedEntities(final ServerLevel world, final int chunkX, final int chunkZ, final List<Entity> entities);
+
+ public void unloadEntity(final Entity entity);
+
+ public void postLoadProtoChunk(final ServerLevel world, final ProtoChunk chunk);
+
+ public int modifyEntityTrackingRange(final Entity entity, final int currentRange);
+
+ public static final class Holder {
+ private Holder() {
+ }
+
+ private static final PlatformHooks INSTANCE;
+
+ static {
+ INSTANCE = ServiceLoader.load(PlatformHooks.class, PlatformHooks.class.getClassLoader()).findFirst()
+ .orElseThrow(() -> new RuntimeException("Failed to locate PlatformHooks"));
+ }
+ }
+}
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..7fed43a1e7bcf35c4d7fd3224837a47fedd59860
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
@@ -0,0 +1,128 @@
+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> {
+
+ private final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f);
+ {
+ this.entityToIndex.defaultReturnValue(Integer.MIN_VALUE);
+ }
+
+ private static final Entity[] EMPTY_LIST = new Entity[0];
+
+ private Entity[] entities = EMPTY_LIST;
+ private 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<>() {
+ private Entity lastRet;
+ private 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/IntList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java
new file mode 100644
index 0000000000000000000000000000000000000000..9f3b25bb2439f283f878db93973a02fcdcd14eed
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java
@@ -0,0 +1,77 @@
+package ca.spottedleaf.moonrise.common.list;
+
+import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
+import java.util.Arrays;
+
+public final class IntList {
+
+ private final Int2IntOpenHashMap map = new Int2IntOpenHashMap();
+ {
+ this.map.defaultReturnValue(Integer.MIN_VALUE);
+ }
+
+ private static final int[] EMPTY_LIST = new int[0];
+
+ private int[] byIndex = EMPTY_LIST;
+ private int count;
+
+ public int size() {
+ return this.count;
+ }
+
+ public void setMinCapacity(final int len) {
+ final int[] byIndex = this.byIndex;
+ if (byIndex.length < len) {
+ this.byIndex = Arrays.copyOf(byIndex, len);
+ }
+ }
+
+ public int getRaw(final int index) {
+ return this.byIndex[index];
+ }
+
+ public boolean add(final int value) {
+ final int count = this.count;
+ final int currIndex = this.map.putIfAbsent(value, count);
+
+ if (currIndex != Integer.MIN_VALUE) {
+ return false; // already in this list
+ }
+
+ int[] list = this.byIndex;
+
+ if (list.length == count) {
+ // resize required
+ list = this.byIndex = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
+ }
+
+ list[count] = value;
+ this.count = count + 1;
+
+ return true;
+ }
+
+ public boolean remove(final int value) {
+ final int index = this.map.remove(value);
+ if (index == Integer.MIN_VALUE) {
+ return false;
+ }
+
+ // move the entry at the end to this index
+ final int endIndex = --this.count;
+ final int end = this.byIndex[endIndex];
+ if (index != endIndex) {
+ // not empty after this call
+ this.map.put(end, index);
+ }
+ this.byIndex[index] = end;
+ this.byIndex[endIndex] = 0;
+
+ return true;
+ }
+
+ public void clear() {
+ this.count = 0;
+ this.map.clear();
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java
new file mode 100644
index 0000000000000000000000000000000000000000..c21e00812f1aaa1279834a0562d360d6b89e146c
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IteratorSafeOrderedReferenceSet.java
@@ -0,0 +1,312 @@
+package ca.spottedleaf.moonrise.common.list;
+
+import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import java.util.Arrays;
+import java.util.NoSuchElementException;
+
+public final class IteratorSafeOrderedReferenceSet<E> {
+
+ public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0;
+
+ private final Reference2IntLinkedOpenHashMap<E> indexMap;
+ private int firstInvalidIndex = -1;
+
+ /* list impl */
+ private E[] listElements;
+ private int listSize;
+
+ private final double maxFragFactor;
+
+ private int iteratorCount;
+
+ public IteratorSafeOrderedReferenceSet() {
+ this(16, 0.75f, 16, 0.2);
+ }
+
+ public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity,
+ final double maxFragFactor) {
+ this.indexMap = new Reference2IntLinkedOpenHashMap<>(setCapacity, setLoadFactor);
+ this.indexMap.defaultReturnValue(-1);
+ this.maxFragFactor = maxFragFactor;
+ this.listElements = (E[])new Object[arrayCapacity];
+ }
+
+ /*
+ public void check() {
+ int iterated = 0;
+ ReferenceOpenHashSet<E> check = new ReferenceOpenHashSet<>();
+ if (this.listElements != null) {
+ for (int i = 0; i < this.listSize; ++i) {
+ Object obj = this.listElements[i];
+ if (obj != null) {
+ iterated++;
+ if (!check.add((E)obj)) {
+ throw new IllegalStateException("contains duplicate");
+ }
+ if (!this.contains((E)obj)) {
+ throw new IllegalStateException("desync");
+ }
+ }
+ }
+ }
+
+ if (iterated != this.size()) {
+ throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size());
+ }
+
+ check.clear();
+ iterated = 0;
+ for (final java.util.Iterator<E> iterator = this.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
+ final E element = iterator.next();
+ iterated++;
+ if (!check.add(element)) {
+ throw new IllegalStateException("contains duplicate (iterator is wrong)");
+ }
+ if (!this.contains(element)) {
+ throw new IllegalStateException("desync (iterator is wrong)");
+ }
+ }
+
+ if (iterated != this.size()) {
+ throw new IllegalStateException("Size is mismatched! (iterator is wrong) Got " + iterated + ", expected " + this.size());
+ }
+ }
+ */
+
+ private double getFragFactor() {
+ return 1.0 - ((double)this.indexMap.size() / (double)this.listSize);
+ }
+
+ public int createRawIterator() {
+ ++this.iteratorCount;
+ if (this.indexMap.isEmpty()) {
+ return -1;
+ } else {
+ return this.firstInvalidIndex == 0 ? this.indexMap.getInt(this.indexMap.firstKey()) : 0;
+ }
+ }
+
+ public int advanceRawIterator(final int index) {
+ final E[] elements = this.listElements;
+ int ret = index + 1;
+ for (int len = this.listSize; ret < len; ++ret) {
+ if (elements[ret] != null) {
+ return ret;
+ }
+ }
+
+ return -1;
+ }
+
+ public void finishRawIterator() {
+ if (--this.iteratorCount == 0) {
+ if (this.getFragFactor() >= this.maxFragFactor) {
+ this.defrag();
+ }
+ }
+ }
+
+ public boolean remove(final E element) {
+ final int index = this.indexMap.removeInt(element);
+ if (index >= 0) {
+ if (this.firstInvalidIndex < 0 || index < this.firstInvalidIndex) {
+ this.firstInvalidIndex = index;
+ }
+ if (this.listElements[index] != element) {
+ throw new IllegalStateException();
+ }
+ this.listElements[index] = null;
+ if (this.iteratorCount == 0 && this.getFragFactor() >= this.maxFragFactor) {
+ this.defrag();
+ }
+ //this.check();
+ return true;
+ }
+ return false;
+ }
+
+ public boolean contains(final E element) {
+ return this.indexMap.containsKey(element);
+ }
+
+ public boolean add(final E element) {
+ final int listSize = this.listSize;
+
+ final int previous = this.indexMap.putIfAbsent(element, listSize);
+ if (previous != -1) {
+ return false;
+ }
+
+ if (listSize >= this.listElements.length) {
+ this.listElements = Arrays.copyOf(this.listElements, listSize * 2);
+ }
+ this.listElements[listSize] = element;
+ this.listSize = listSize + 1;
+
+ //this.check();
+ return true;
+ }
+
+ private void defrag() {
+ if (this.firstInvalidIndex < 0) {
+ return; // nothing to do
+ }
+
+ if (this.indexMap.isEmpty()) {
+ Arrays.fill(this.listElements, 0, this.listSize, null);
+ this.listSize = 0;
+ this.firstInvalidIndex = -1;
+ //this.check();
+ return;
+ }
+
+ final E[] backingArray = this.listElements;
+
+ int lastValidIndex;
+ java.util.Iterator<Reference2IntMap.Entry<E>> iterator;
+
+ if (this.firstInvalidIndex == 0) {
+ iterator = this.indexMap.reference2IntEntrySet().fastIterator();
+ lastValidIndex = 0;
+ } else {
+ lastValidIndex = this.firstInvalidIndex;
+ final E key = backingArray[lastValidIndex - 1];
+ iterator = this.indexMap.reference2IntEntrySet().fastIterator(new Reference2IntMap.Entry<E>() {
+ @Override
+ public int getIntValue() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int setValue(int i) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public E getKey() {
+ return key;
+ }
+ });
+ }
+
+ while (iterator.hasNext()) {
+ final Reference2IntMap.Entry<E> entry = iterator.next();
+
+ final int newIndex = lastValidIndex++;
+ backingArray[newIndex] = entry.getKey();
+ entry.setValue(newIndex);
+ }
+
+ // cleanup end
+ Arrays.fill(backingArray, lastValidIndex, this.listSize, null);
+ this.listSize = lastValidIndex;
+ this.firstInvalidIndex = -1;
+ //this.check();
+ }
+
+ public E rawGet(final int index) {
+ return this.listElements[index];
+ }
+
+ public int size() {
+ // always returns the correct amount - listSize can be different
+ return this.indexMap.size();
+ }
+
+ public IteratorSafeOrderedReferenceSet.Iterator<E> iterator() {
+ return this.iterator(0);
+ }
+
+ public IteratorSafeOrderedReferenceSet.Iterator<E> iterator(final int flags) {
+ ++this.iteratorCount;
+ return new BaseIterator<>(this, true, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
+ }
+
+ public java.util.Iterator<E> unsafeIterator() {
+ return this.unsafeIterator(0);
+ }
+ public java.util.Iterator<E> unsafeIterator(final int flags) {
+ return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize);
+ }
+
+ public static interface Iterator<E> extends java.util.Iterator<E> {
+
+ public void finishedIterating();
+
+ }
+
+ private static final class BaseIterator<E> implements IteratorSafeOrderedReferenceSet.Iterator<E> {
+
+ private final IteratorSafeOrderedReferenceSet<E> set;
+ private final boolean canFinish;
+ private final int maxIndex;
+ private int nextIndex;
+ private E pendingValue;
+ private boolean finished;
+ private E lastReturned;
+
+ private BaseIterator(final IteratorSafeOrderedReferenceSet<E> set, final boolean canFinish, final int maxIndex) {
+ this.set = set;
+ this.canFinish = canFinish;
+ this.maxIndex = maxIndex;
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (this.finished) {
+ return false;
+ }
+ if (this.pendingValue != null) {
+ return true;
+ }
+
+ final E[] elements = this.set.listElements;
+ int index, len;
+ for (index = this.nextIndex, len = Math.min(this.maxIndex, this.set.listSize); index < len; ++index) {
+ final E element = elements[index];
+ if (element != null) {
+ this.pendingValue = element;
+ this.nextIndex = index + 1;
+ return true;
+ }
+ }
+
+ this.nextIndex = index;
+ return false;
+ }
+
+ @Override
+ public E next() {
+ if (!this.hasNext()) {
+ throw new NoSuchElementException();
+ }
+ final E ret = this.pendingValue;
+
+ this.pendingValue = null;
+ this.lastReturned = ret;
+
+ return ret;
+ }
+
+ @Override
+ public void remove() {
+ final E lastReturned = this.lastReturned;
+ if (lastReturned == null) {
+ throw new IllegalStateException();
+ }
+ this.lastReturned = null;
+ this.set.remove(lastReturned);
+ }
+
+ @Override
+ public void finishedIterating() {
+ if (this.finished || !this.canFinish) {
+ throw new IllegalStateException();
+ }
+ this.lastReturned = null;
+ this.finished = true;
+ this.set.finishRawIterator();
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
new file mode 100644
index 0000000000000000000000000000000000000000..2e876b918672e8ef3b5197b7e6b1597247fdeaa1
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ReferenceList.java
@@ -0,0 +1,142 @@
+package ca.spottedleaf.moonrise.common.list;
+
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+public final class ReferenceList<E> implements Iterable<E> {
+
+ private static final Object[] EMPTY_LIST = new Object[0];
+
+ private final Reference2IntOpenHashMap<E> referenceToIndex;
+ private E[] references;
+ private int count;
+
+ public ReferenceList() {
+ this((E[])EMPTY_LIST);
+ }
+
+ public ReferenceList(final E[] referenceArray) {
+ this.references = referenceArray;
+ this.referenceToIndex = new Reference2IntOpenHashMap<>(2, 0.8f);
+ this.referenceToIndex.defaultReturnValue(Integer.MIN_VALUE);
+ }
+
+ private ReferenceList(final E[] references, final int count, final Reference2IntOpenHashMap<E> referenceToIndex) {
+ this.references = references;
+ this.count = count;
+ this.referenceToIndex = referenceToIndex;
+ }
+
+ public ReferenceList<E> copy() {
+ return new ReferenceList<>(this.references.clone(), this.count, this.referenceToIndex.clone());
+ }
+
+ public int size() {
+ return this.count;
+ }
+
+ public boolean contains(final E obj) {
+ return this.referenceToIndex.containsKey(obj);
+ }
+
+ public boolean remove(final E obj) {
+ final int index = this.referenceToIndex.removeInt(obj);
+ if (index == Integer.MIN_VALUE) {
+ return false;
+ }
+
+ // move the object at the end to this index
+ final int endIndex = --this.count;
+ final E end = (E)this.references[endIndex];
+ if (index != endIndex) {
+ // not empty after this call
+ this.referenceToIndex.put(end, index); // update index
+ }
+ this.references[index] = end;
+ this.references[endIndex] = null;
+
+ return true;
+ }
+
+ public boolean add(final E obj) {
+ final int count = this.count;
+ final int currIndex = this.referenceToIndex.putIfAbsent(obj, count);
+
+ if (currIndex != Integer.MIN_VALUE) {
+ return false; // already in this list
+ }
+
+ E[] list = this.references;
+
+ if (list.length == count) {
+ // resize required
+ list = this.references = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
+ }
+
+ list[count] = obj;
+ this.count = count + 1;
+
+ return true;
+ }
+
+ public E getChecked(final int index) {
+ if (index < 0 || index >= this.count) {
+ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
+ }
+ return this.references[index];
+ }
+
+ public E getUnchecked(final int index) {
+ return this.references[index];
+ }
+
+ public Object[] getRawData() {
+ return this.references;
+ }
+
+ public E[] getRawDataUnchecked() {
+ return this.references;
+ }
+
+ public void clear() {
+ this.referenceToIndex.clear();
+ Arrays.fill(this.references, 0, this.count, null);
+ this.count = 0;
+ }
+
+ @Override
+ public Iterator<E> iterator() {
+ return new Iterator<>() {
+ private E lastRet;
+ private int current;
+
+ @Override
+ public boolean hasNext() {
+ return this.current < ReferenceList.this.count;
+ }
+
+ @Override
+ public E next() {
+ if (this.current >= ReferenceList.this.count) {
+ throw new NoSuchElementException();
+ }
+ return this.lastRet = ReferenceList.this.references[this.current++];
+ }
+
+ @Override
+ public void remove() {
+ final E lastRet = this.lastRet;
+
+ if (lastRet == null) {
+ throw new IllegalStateException();
+ }
+ this.lastRet = null;
+
+ ReferenceList.this.remove(lastRet);
+ --this.current;
+ }
+ };
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java
new file mode 100644
index 0000000000000000000000000000000000000000..2bae9949ef325d0001aa638150fbbdf968367e75
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java
@@ -0,0 +1,77 @@
+package ca.spottedleaf.moonrise.common.list;
+
+import it.unimi.dsi.fastutil.shorts.Short2ShortOpenHashMap;
+import java.util.Arrays;
+
+public final class ShortList {
+
+ private final Short2ShortOpenHashMap map = new Short2ShortOpenHashMap();
+ {
+ this.map.defaultReturnValue(Short.MIN_VALUE);
+ }
+
+ private static final short[] EMPTY_LIST = new short[0];
+
+ private short[] byIndex = EMPTY_LIST;
+ private short count;
+
+ public int size() {
+ return (int)this.count;
+ }
+
+ public short getRaw(final int index) {
+ return this.byIndex[index];
+ }
+
+ public void setMinCapacity(final int len) {
+ final short[] byIndex = this.byIndex;
+ if (byIndex.length < len) {
+ this.byIndex = Arrays.copyOf(byIndex, len);
+ }
+ }
+
+ public boolean add(final short value) {
+ final int count = (int)this.count;
+ final short currIndex = this.map.putIfAbsent(value, (short)count);
+
+ if (currIndex != Short.MIN_VALUE) {
+ return false; // already in this list
+ }
+
+ short[] list = this.byIndex;
+
+ if (list.length == count) {
+ // resize required
+ list = this.byIndex = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
+ }
+
+ list[count] = value;
+ this.count = (short)(count + 1);
+
+ return true;
+ }
+
+ public boolean remove(final short value) {
+ final short index = this.map.remove(value);
+ if (index == Short.MIN_VALUE) {
+ return false;
+ }
+
+ // move the entry at the end to this index
+ final short endIndex = --this.count;
+ final short end = this.byIndex[endIndex];
+ if (index != endIndex) {
+ // not empty after this call
+ this.map.put(end, index);
+ }
+ this.byIndex[(int)index] = end;
+ this.byIndex[(int)endIndex] = (short)0;
+
+ return true;
+ }
+
+ public void clear() {
+ this.count = (short)0;
+ this.map.clear();
+ }
+}
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/LazyRunnable.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java
new file mode 100644
index 0000000000000000000000000000000000000000..c2d917c2eac55b8a4411a6e159f177f9428b1150
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java
@@ -0,0 +1,22 @@
+package ca.spottedleaf.moonrise.common.misc;
+
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import java.lang.invoke.VarHandle;
+
+public final class LazyRunnable implements Runnable {
+
+ private volatile Runnable toRun;
+ private static final VarHandle TO_RUN_HANDLE = ConcurrentUtil.getVarHandle(LazyRunnable.class, "toRun", Runnable.class);
+
+ public void setRunnable(final Runnable run) {
+ final Runnable prev = (Runnable)TO_RUN_HANDLE.compareAndExchange(this, (Runnable)null, run);
+ if (prev != null) {
+ throw new IllegalStateException("Runnable already set");
+ }
+ }
+
+ @Override
+ public void run() {
+ ((Runnable)TO_RUN_HANDLE.getVolatile(this)).run();
+ }
+}
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..bb44de17a37082e57f2292a4f470740be1d09b11
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
@@ -0,0 +1,273 @@
+package ca.spottedleaf.moonrise.common.misc;
+
+import ca.spottedleaf.moonrise.common.list.ReferenceList;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.MoonriseConstants;
+import ca.spottedleaf.moonrise.common.util.ChunkSystem;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData;
+import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants;
+import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel;
+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;
+import java.util.ArrayList;
+
+public final class NearbyPlayers {
+
+ public static enum NearbyMapType {
+ GENERAL,
+ GENERAL_SMALL,
+ GENERAL_REALLY_SMALL,
+ TICK_VIEW_DISTANCE,
+ VIEW_DISTANCE,
+ // Moonrise start - chunk tick iteration
+ SPAWN_RANGE {
+ @Override
+ void addTo(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) {
+ ((ChunkTickServerLevel)world).moonrise$addPlayerTickingRequest(chunkX, chunkZ);
+ }
+
+ @Override
+ void removeFrom(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) {
+ ((ChunkTickServerLevel)world).moonrise$removePlayerTickingRequest(chunkX, chunkZ);
+ }
+ };
+ // Moonrise end - chunk tick iteration
+
+ void addTo(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) {
+
+ }
+
+ void removeFrom(final ServerPlayer player, final ServerLevel world, final int chunkX, final int chunkZ) {
+
+ }
+ }
+
+ 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<>();
+ private final Long2ReferenceOpenHashMap<ReferenceList<ServerPlayer>>[] directByChunk = new Long2ReferenceOpenHashMap[TOTAL_MAP_TYPES];
+ {
+ for (int i = 0; i < this.directByChunk.length; ++i) {
+ this.directByChunk[i] = 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 clear() {
+ if (this.players.isEmpty()) {
+ return;
+ }
+
+ for (final ServerPlayer player : new ArrayList<>(this.players.keySet())) {
+ this.removePlayer(player);
+ }
+ }
+
+ public void tickPlayer(final ServerPlayer player) {
+ final TrackedPlayer[] players = this.players.get(player);
+ if (players == null) {
+ throw new IllegalStateException("Don't have player " + player);
+ }
+
+ final ChunkPos chunk = player.chunkPosition();
+
+ players[NearbyMapType.GENERAL.ordinal()].update(chunk.x, chunk.z, GENERAL_AREA_VIEW_DISTANCE);
+ players[NearbyMapType.GENERAL_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_SMALL_VIEW_DISTANCE);
+ players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE);
+ players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player));
+ players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getLoadViewDistance(player));
+ players[NearbyMapType.SPAWN_RANGE.ordinal()].update(chunk.x, chunk.z, ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Moonrise - chunk tick iteration
+ }
+
+ public TrackedChunk getChunk(final ChunkPos pos) {
+ return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
+ }
+
+ public TrackedChunk getChunk(final BlockPos pos) {
+ return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
+ }
+
+ public TrackedChunk getChunk(final int chunkX, final int chunkZ) {
+ return this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+
+ public ReferenceList<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) {
+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos));
+ }
+
+ public ReferenceList<ServerPlayer> getPlayers(final ChunkPos pos, final NearbyMapType type) {
+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos));
+ }
+
+ public ReferenceList<ServerPlayer> getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) {
+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+
+ public ReferenceList<ServerPlayer> getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) {
+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4));
+ }
+
+ public static final class TrackedChunk {
+
+ private static final ServerPlayer[] EMPTY_PLAYERS_ARRAY = new ServerPlayer[0];
+
+ private final long chunkKey;
+ private final NearbyPlayers nearbyPlayers;
+ private final ReferenceList<ServerPlayer>[] players = new ReferenceList[TOTAL_MAP_TYPES];
+ private int nonEmptyLists;
+ private long updateCount;
+
+ public TrackedChunk(final long chunkKey, final NearbyPlayers nearbyPlayers) {
+ this.chunkKey = chunkKey;
+ this.nearbyPlayers = nearbyPlayers;
+ }
+
+ 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;
+ final ReferenceList<ServerPlayer> players = (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY));
+ this.nearbyPlayers.directByChunk[idx].put(this.chunkKey, players);
+ players.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.nearbyPlayers.directByChunk[idx].remove(this.chunkKey);
+ --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);
+
+ final TrackedChunk chunk = NearbyPlayers.this.byChunk.get(chunkKey);
+ final NearbyMapType type = this.type;
+ if (chunk != null) {
+ chunk.addPlayer(parameter, type);
+ type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ);
+ } else {
+ final TrackedChunk created = new TrackedChunk(chunkKey, NearbyPlayers.this);
+ NearbyPlayers.this.byChunk.put(chunkKey, created);
+ created.addPlayer(parameter, type);
+ type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ);
+
+ ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$requestChunkData(chunkKey).nearbyPlayers = created;
+ }
+ }
+
+ @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));
+ }
+
+ final NearbyMapType type = this.type;
+ chunk.removePlayer(parameter, type);
+ type.removeFrom(parameter, NearbyPlayers.this.world, chunkX, chunkZ);
+
+ if (chunk.isEmpty()) {
+ NearbyPlayers.this.byChunk.remove(chunkKey);
+ final ChunkData chunkData = ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$releaseChunkData(chunkKey);
+ if (chunkData != null) {
+ chunkData.nearbyPlayers = null;
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/PositionCountingAreaMap.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/PositionCountingAreaMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..90560769d09538f7a740753a41a3b8e017b0b92a
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/PositionCountingAreaMap.java
@@ -0,0 +1,99 @@
+package ca.spottedleaf.moonrise.common.misc;
+
+import ca.spottedleaf.concurrentutil.util.IntPairUtil;
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongSet;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ReferenceSet;
+
+public final class PositionCountingAreaMap<T> {
+
+ private final Reference2ReferenceOpenHashMap<T, PositionCounter> counters = new Reference2ReferenceOpenHashMap<>();
+ private final Long2IntOpenHashMap positions = new Long2IntOpenHashMap();
+
+ public ReferenceSet<T> getObjects() {
+ return this.counters.keySet();
+ }
+
+ public LongSet getPositions() {
+ return this.positions.keySet();
+ }
+
+ public int getTotalPositions() {
+ return this.positions.size();
+ }
+
+ public boolean hasObjectsNear(final int toX, final int toZ) {
+ return this.positions.containsKey(IntPairUtil.key(toX, toZ));
+ }
+
+ public int getObjectsNear(final int toX, final int toZ) {
+ return this.positions.get(IntPairUtil.key(toX, toZ));
+ }
+
+ public boolean add(final T parameter, final int toX, final int toZ, final int distance) {
+ final PositionCounter existing = this.counters.get(parameter);
+ if (existing != null) {
+ return false;
+ }
+
+ final PositionCounter counter = new PositionCounter(parameter);
+
+ this.counters.put(parameter, counter);
+
+ return counter.add(toX, toZ, distance);
+ }
+
+ public boolean addOrUpdate(final T parameter, final int toX, final int toZ, final int distance) {
+ final PositionCounter existing = this.counters.get(parameter);
+ if (existing != null) {
+ return existing.update(toX, toZ, distance);
+ }
+
+ final PositionCounter counter = new PositionCounter(parameter);
+
+ this.counters.put(parameter, counter);
+
+ return counter.add(toX, toZ, distance);
+ }
+
+ public boolean remove(final T parameter) {
+ final PositionCounter counter = this.counters.remove(parameter);
+ if (counter == null) {
+ return false;
+ }
+
+ counter.remove();
+
+ return true;
+ }
+
+ public boolean update(final T parameter, final int toX, final int toZ, final int distance) {
+ final PositionCounter counter = this.counters.get(parameter);
+ if (counter == null) {
+ return false;
+ }
+
+ return counter.update(toX, toZ, distance);
+ }
+
+ private final class PositionCounter extends SingleUserAreaMap<T> {
+
+ public PositionCounter(final T parameter) {
+ super(parameter);
+ }
+
+ @Override
+ protected void addCallback(final T parameter, final int toX, final int toZ) {
+ PositionCountingAreaMap.this.positions.addTo(IntPairUtil.key(toX, toZ), 1);
+ }
+
+ @Override
+ protected void removeCallback(final T parameter, final int toX, final int toZ) {
+ final long key = IntPairUtil.key(toX, toZ);
+ if (PositionCountingAreaMap.this.positions.addTo(key, -1) == 1) {
+ PositionCountingAreaMap.this.positions.remove(key);
+ }
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..94689e0342cf95dbedec955d67c95fa07a219678
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/SingleUserAreaMap.java
@@ -0,0 +1,248 @@
+package ca.spottedleaf.moonrise.common.misc;
+
+import ca.spottedleaf.concurrentutil.util.IntegerUtil;
+
+public abstract class SingleUserAreaMap<T> {
+
+ public static final int NOT_SET = Integer.MIN_VALUE;
+
+ private final T parameter;
+ private int lastChunkX = NOT_SET;
+ private int lastChunkZ = NOT_SET;
+ private int distance = NOT_SET;
+
+ public SingleUserAreaMap(final T parameter) {
+ this.parameter = parameter;
+ }
+
+ public final T getParameter() {
+ return this.parameter;
+ }
+
+ public final int getLastChunkX() {
+ return this.lastChunkX;
+ }
+
+ public final int getLastChunkZ() {
+ return this.lastChunkZ;
+ }
+
+ public final int getLastDistance() {
+ return this.distance;
+ }
+
+ /* math sign function except 0 returns 1 */
+ protected static int sign(int val) {
+ return 1 | (val >> (Integer.SIZE - 1));
+ }
+
+ protected abstract void addCallback(final T parameter, final int chunkX, final int chunkZ);
+
+ protected abstract void removeCallback(final T parameter, final int chunkX, final int chunkZ);
+
+ private void addToNew(final T parameter, final int chunkX, final int chunkZ, final int distance) {
+ final int maxX = chunkX + distance;
+ final int maxZ = chunkZ + distance;
+
+ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
+ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
+ this.addCallback(parameter, cx, cz);
+ }
+ }
+ }
+
+ private void removeFromOld(final T parameter, final int chunkX, final int chunkZ, final int distance) {
+ final int maxX = chunkX + distance;
+ final int maxZ = chunkZ + distance;
+
+ for (int cx = chunkX - distance; cx <= maxX; ++cx) {
+ for (int cz = chunkZ - distance; cz <= maxZ; ++cz) {
+ this.removeCallback(parameter, cx, cz);
+ }
+ }
+ }
+
+ public final boolean add(final int chunkX, final int chunkZ, final int distance) {
+ if (distance < 0) {
+ throw new IllegalArgumentException(Integer.toString(distance));
+ }
+ if (this.lastChunkX != NOT_SET) {
+ return false;
+ }
+ this.lastChunkX = chunkX;
+ this.lastChunkZ = chunkZ;
+ this.distance = distance;
+
+ this.addToNew(this.parameter, chunkX, chunkZ, distance);
+
+ return true;
+ }
+
+ public final boolean update(final int toX, final int toZ, final int newViewDistance) {
+ if (newViewDistance < 0) {
+ throw new IllegalArgumentException(Integer.toString(newViewDistance));
+ }
+ final int fromX = this.lastChunkX;
+ final int fromZ = this.lastChunkZ;
+ final int oldViewDistance = this.distance;
+ if (fromX == NOT_SET) {
+ return false;
+ }
+
+ this.lastChunkX = toX;
+ this.lastChunkZ = toZ;
+ this.distance = newViewDistance;
+
+ final T parameter = this.parameter;
+
+
+ final int dx = toX - fromX;
+ final int dz = toZ - fromZ;
+
+ final int totalX = IntegerUtil.branchlessAbs(fromX - toX);
+ final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
+
+ if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
+ // teleported
+ this.removeFromOld(parameter, fromX, fromZ, oldViewDistance);
+ this.addToNew(parameter, toX, toZ, newViewDistance);
+ return true;
+ }
+
+ if (oldViewDistance != newViewDistance) {
+ // remove loop
+
+ final int oldMinX = fromX - oldViewDistance;
+ final int oldMinZ = fromZ - oldViewDistance;
+ final int oldMaxX = fromX + oldViewDistance;
+ final int oldMaxZ = fromZ + oldViewDistance;
+ for (int currX = oldMinX; currX <= oldMaxX; ++currX) {
+ for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) {
+
+ // only remove if we're outside the new view distance...
+ if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) {
+ this.removeCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ // add loop
+
+ final int newMinX = toX - newViewDistance;
+ final int newMinZ = toZ - newViewDistance;
+ final int newMaxX = toX + newViewDistance;
+ final int newMaxZ = toZ + newViewDistance;
+ for (int currX = newMinX; currX <= newMaxX; ++currX) {
+ for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) {
+
+ // only add if we're outside the old view distance...
+ if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) {
+ this.addCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ // x axis is width
+ // z axis is height
+ // right refers to the x axis of where we moved
+ // top refers to the z axis of where we moved
+
+ // same view distance
+
+ // used for relative positioning
+ final int up = sign(dz); // 1 if dz >= 0, -1 otherwise
+ final int right = sign(dx); // 1 if dx >= 0, -1 otherwise
+
+ // The area excluded by overlapping the two view distance squares creates four rectangles:
+ // Two on the left, and two on the right. The ones on the left we consider the "removed" section
+ // and on the right the "added" section.
+ // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually
+ // exclusive to the regions they surround.
+
+ // 4 points of the rectangle
+ int maxX; // exclusive
+ int minX; // inclusive
+ int maxZ; // exclusive
+ int minZ; // inclusive
+
+ if (dx != 0) {
+ // handle right addition
+
+ maxX = toX + (oldViewDistance * right) + right; // exclusive
+ minX = fromX + (oldViewDistance * right) + right; // inclusive
+ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
+ minZ = toZ - (oldViewDistance * up); // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.addCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ if (dz != 0) {
+ // handle up addition
+
+ maxX = toX + (oldViewDistance * right) + right; // exclusive
+ minX = toX - (oldViewDistance * right); // inclusive
+ maxZ = toZ + (oldViewDistance * up) + up; // exclusive
+ minZ = fromZ + (oldViewDistance * up) + up; // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.addCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ if (dx != 0) {
+ // handle left removal
+
+ maxX = toX - (oldViewDistance * right); // exclusive
+ minX = fromX - (oldViewDistance * right); // inclusive
+ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
+ minZ = toZ - (oldViewDistance * up); // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.removeCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ if (dz != 0) {
+ // handle down removal
+
+ maxX = fromX + (oldViewDistance * right) + right; // exclusive
+ minX = fromX - (oldViewDistance * right); // inclusive
+ maxZ = toZ - (oldViewDistance * up); // exclusive
+ minZ = fromZ - (oldViewDistance * up); // inclusive
+
+ for (int currX = minX; currX != maxX; currX += right) {
+ for (int currZ = minZ; currZ != maxZ; currZ += up) {
+ this.removeCallback(parameter, currX, currZ);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public final boolean remove() {
+ final int chunkX = this.lastChunkX;
+ final int chunkZ = this.lastChunkZ;
+ final int distance = this.distance;
+ if (chunkX == NOT_SET) {
+ return false;
+ }
+
+ this.lastChunkX = this.lastChunkZ = this.distance = NOT_SET;
+
+ this.removeFromOld(this.parameter, chunkX, chunkZ, distance);
+
+ return true;
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java
new file mode 100644
index 0000000000000000000000000000000000000000..4123edddc556c47f3f8d83523c125fd2e46b30e2
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/set/OptimizedSmallEnumSet.java
@@ -0,0 +1,68 @@
+package ca.spottedleaf.moonrise.common.set;
+
+import java.util.Collection;
+
+public final class OptimizedSmallEnumSet<E extends Enum<E>> {
+
+ private final Class<E> enumClass;
+ private long backingSet;
+
+ public OptimizedSmallEnumSet(final Class<E> clazz) {
+ if (clazz == null) {
+ throw new IllegalArgumentException("Null class");
+ }
+ if (!clazz.isEnum()) {
+ throw new IllegalArgumentException("Class must be enum, not " + clazz.getCanonicalName());
+ }
+ this.enumClass = clazz;
+ }
+
+ public boolean addUnchecked(final E element) {
+ final int ordinal = element.ordinal();
+ final long key = 1L << ordinal;
+
+ final long prev = this.backingSet;
+ this.backingSet = prev | key;
+
+ return (prev & key) == 0;
+ }
+
+ public boolean removeUnchecked(final E element) {
+ final int ordinal = element.ordinal();
+ final long key = 1L << ordinal;
+
+ final long prev = this.backingSet;
+ this.backingSet = prev & ~key;
+
+ return (prev & key) != 0;
+ }
+
+ public void clear() {
+ this.backingSet = 0L;
+ }
+
+ public int size() {
+ return Long.bitCount(this.backingSet);
+ }
+
+ public void addAllUnchecked(final Collection<E> enums) {
+ for (final E element : enums) {
+ if (element == null) {
+ throw new NullPointerException("Null element");
+ }
+ this.backingSet |= (1L << element.ordinal());
+ }
+ }
+
+ public long getBackingSet() {
+ return this.backingSet;
+ }
+
+ public boolean hasCommonElements(final OptimizedSmallEnumSet<E> other) {
+ return (other.backingSet & this.backingSet) != 0;
+ }
+
+ public boolean hasElement(final E element) {
+ return (this.backingSet & (1L << element.ordinal())) != 0;
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
new file mode 100644
index 0000000000000000000000000000000000000000..49fe9eed5d5d08abd6e9778fe0d0545f35552435
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
@@ -0,0 +1,288 @@
+package ca.spottedleaf.moonrise.common.util;
+
+import ca.spottedleaf.concurrentutil.util.Priority;
+import ca.spottedleaf.moonrise.common.PlatformHooks;
+import com.mojang.logging.LogUtils;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+import org.slf4j.Logger;
+import java.util.List;
+import java.util.function.Consumer;
+
+public final class ChunkSystem {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static final net.minecraft.world.level.chunk.status.ChunkStep FULL_CHUNK_STEP = net.minecraft.world.level.chunk.status.ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FULL);
+
+ private static int getDistance(final ChunkStatus status) {
+ return FULL_CHUNK_STEP.getAccumulatedRadiusOf(status);
+ }
+
+ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
+ scheduleChunkTask(level, chunkX, chunkZ, run, Priority.NORMAL);
+ }
+
+ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final Priority priority) {
+ level.chunkSource.mainThreadProcessor.execute(run);
+ }
+
+ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
+ final ChunkStatus toStatus, final boolean addTicket, final Priority priority,
+ final Consumer<ChunkAccess> onComplete) {
+ if (gen) {
+ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ return;
+ }
+ scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
+ if (chunk == null) {
+ if (onComplete != null) {
+ onComplete.accept(null);
+ }
+ } else {
+ if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
+ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ } else {
+ if (onComplete != null) {
+ onComplete.accept(null);
+ }
+ }
+ }
+ });
+ }
+
+ static final net.minecraft.server.level.TicketType<Long> CHUNK_LOAD = net.minecraft.server.level.TicketType.create("chunk_load", Long::compareTo);
+
+ private static long chunkLoadCounter = 0L;
+ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+ final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete) {
+ if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
+ scheduleChunkTask(level, chunkX, chunkZ, () -> {
+ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ }, priority);
+ return;
+ }
+
+ final int minLevel = 33 + getDistance(toStatus);
+ final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
+ final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
+
+ if (addTicket) {
+ level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+ }
+ level.chunkSource.runDistanceManagerUpdates();
+
+ final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
+ try {
+ if (onComplete != null) {
+ onComplete.accept(chunk);
+ }
+ } catch (final Throwable thr) {
+ LOGGER.error("Exception handling chunk load callback", thr);
+ com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
+ } finally {
+ if (addTicket) {
+ level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
+ level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+ }
+ }
+ };
+
+ final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (holder == null || holder.getTicketLevel() > minLevel) {
+ loadCallback.accept(null);
+ return;
+ }
+
+ final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess>> loadFuture = holder.scheduleChunkGenerationTask(toStatus, level.chunkSource.chunkMap);
+
+ if (loadFuture.isDone()) {
+ loadCallback.accept(loadFuture.join().orElse(null));
+ return;
+ }
+
+ loadFuture.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.ChunkAccess> result, final Throwable thr) -> {
+ if (thr != null) {
+ loadCallback.accept(null);
+ return;
+ }
+ loadCallback.accept(result.orElse(null));
+ }, (final Runnable r) -> {
+ scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
+ });
+ }
+
+ public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
+ final FullChunkStatus toStatus, final boolean addTicket,
+ final Priority priority, final Consumer<LevelChunk> onComplete) {
+ // This method goes unused until the chunk system rewrite
+ if (toStatus == FullChunkStatus.INACCESSIBLE) {
+ throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
+ }
+
+ if (!org.bukkit.Bukkit.isOwnedByCurrentRegion(level.getWorld(), chunkX, chunkZ)) {
+ scheduleChunkTask(level, chunkX, chunkZ, () -> {
+ scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ }, priority);
+ return;
+ }
+
+ final int minLevel = 33 - (toStatus.ordinal() - 1);
+ final int radius = toStatus.ordinal() - 1;
+ final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
+ final net.minecraft.world.level.ChunkPos chunkPos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
+
+ if (addTicket) {
+ level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+ }
+ level.chunkSource.runDistanceManagerUpdates();
+
+ final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> {
+ try {
+ if (onComplete != null) {
+ onComplete.accept(chunk);
+ }
+ } catch (final Throwable thr) {
+ LOGGER.error("Exception handling chunk load callback", thr);
+ com.destroystokyo.paper.util.SneakyThrow.sneaky(thr);
+ } finally {
+ if (addTicket) {
+ level.chunkSource.addTicketAtLevel(net.minecraft.server.level.TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
+ level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+ }
+ }
+ };
+
+ final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (holder == null || holder.getTicketLevel() > minLevel) {
+ loadCallback.accept(null);
+ return;
+ }
+
+ final java.util.concurrent.CompletableFuture<net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk>> tickingState;
+ switch (toStatus) {
+ case FULL: {
+ tickingState = holder.getFullChunkFuture();
+ break;
+ }
+ case BLOCK_TICKING: {
+ tickingState = holder.getTickingChunkFuture();
+ break;
+ }
+ case ENTITY_TICKING: {
+ tickingState = holder.getEntityTickingChunkFuture();
+ break;
+ }
+ default: {
+ throw new IllegalStateException("Cannot reach here");
+ }
+ }
+
+ if (tickingState.isDone()) {
+ loadCallback.accept(tickingState.join().orElse(null));
+ return;
+ }
+
+ tickingState.whenCompleteAsync((final net.minecraft.server.level.ChunkResult<net.minecraft.world.level.chunk.LevelChunk> result, final Throwable thr) -> {
+ if (thr != null) {
+ loadCallback.accept(null);
+ return;
+ }
+ loadCallback.accept(result.orElse(null));
+ }, (final Runnable r) -> {
+ scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
+ });
+ }
+
+ public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
+ return new java.util.ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
+ }
+
+ public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
+ return new java.util.ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
+ }
+
+ public static int getVisibleChunkHolderCount(final ServerLevel level) {
+ return level.chunkSource.chunkMap.visibleChunkMap.size();
+ }
+
+ public static int getUpdatingChunkHolderCount(final ServerLevel level) {
+ return level.chunkSource.chunkMap.updatingChunkMap.size();
+ }
+
+ public static boolean hasAnyChunkHolders(final ServerLevel level) {
+ return getUpdatingChunkHolderCount(level) != 0;
+ }
+
+ public static boolean screenEntity(final ServerLevel level, final Entity entity, final boolean fromDisk, final boolean event) {
+ if (!PlatformHooks.get().screenEntity(level, entity, fromDisk, event)) {
+ return false;
+ }
+ return true;
+ }
+
+ public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
+
+ }
+
+ public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
+
+ }
+
+ public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
+
+ }
+
+ public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
+
+ }
+
+ public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
+
+ }
+
+ public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
+
+ }
+
+ public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
+
+ }
+
+ public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
+
+ }
+
+ public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) {
+ return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ);
+ }
+
+ public static int getSendViewDistance(final ServerPlayer player) {
+ return getLoadViewDistance(player);
+ }
+
+ public static int getLoadViewDistance(final ServerPlayer player) {
+ final ServerLevel level = player.serverLevel();
+ if (level == null) {
+ return org.bukkit.Bukkit.getViewDistance();
+ }
+ return level.chunkSource.chunkMap.getPlayerViewDistance(player);
+ }
+
+ public static int getTickViewDistance(final ServerPlayer player) {
+ final ServerLevel level = player.serverLevel();
+ if (level == null) {
+ return org.bukkit.Bukkit.getSimulationDistance();
+ }
+ return level.chunkSource.chunkMap.distanceManager.simulationDistance;
+ }
+
+ private ChunkSystem() {}
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..31b92bd48828cbea25b44a9f0f96886347aa1ae6
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/CoordinateUtils.java
@@ -0,0 +1,129 @@
+package ca.spottedleaf.moonrise.common.util;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.SectionPos;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.phys.Vec3;
+
+public final class CoordinateUtils {
+
+ // the chunk keys are compatible with vanilla
+
+ public static long getChunkKey(final BlockPos pos) {
+ return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL);
+ }
+
+ public static long getChunkKey(final Entity entity) {
+ return ((Mth.lfloor(entity.getZ()) >> 4) << 32) | ((Mth.lfloor(entity.getX()) >> 4) & 0xFFFFFFFFL);
+ }
+
+ public static long getChunkKey(final ChunkPos pos) {
+ return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL);
+ }
+
+ public static long getChunkKey(final SectionPos pos) {
+ return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL);
+ }
+
+ public static long getChunkKey(final int x, final int z) {
+ return ((long)z << 32) | (x & 0xFFFFFFFFL);
+ }
+
+ public static int getChunkX(final long chunkKey) {
+ return (int)chunkKey;
+ }
+
+ public static int getChunkZ(final long chunkKey) {
+ return (int)(chunkKey >>> 32);
+ }
+
+ public static int getChunkCoordinate(final double blockCoordinate) {
+ return Mth.floor(blockCoordinate) >> 4;
+ }
+
+ // the section keys are compatible with vanilla's
+
+ static final int SECTION_X_BITS = 22;
+ static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1;
+ static final int SECTION_Y_BITS = 20;
+ static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1;
+ static final int SECTION_Z_BITS = 22;
+ static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1;
+ // format is y,z,x (in order of LSB to MSB)
+ static final int SECTION_Y_SHIFT = 0;
+ static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS;
+ static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS;
+ static final int SECTION_TO_BLOCK_SHIFT = 4;
+
+ public static long getChunkSectionKey(final int x, final int y, final int z) {
+ return ((x & SECTION_X_MASK) << SECTION_X_SHIFT)
+ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
+ | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
+ }
+
+ public static long getChunkSectionKey(final SectionPos pos) {
+ return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT)
+ | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT)
+ | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT);
+ }
+
+ public static long getChunkSectionKey(final ChunkPos pos, final int y) {
+ return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT)
+ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
+ | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
+ }
+
+ public static long getChunkSectionKey(final BlockPos pos) {
+ return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
+ ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
+ (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
+ }
+
+ public static long getChunkSectionKey(final Entity entity) {
+ return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
+ ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
+ ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
+ }
+
+ public static int getChunkSectionX(final long key) {
+ return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS));
+ }
+
+ public static int getChunkSectionY(final long key) {
+ return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS));
+ }
+
+ public static int getChunkSectionZ(final long key) {
+ return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS));
+ }
+
+ public static int getBlockX(final Vec3 pos) {
+ return Mth.floor(pos.x);
+ }
+
+ public static int getBlockY(final Vec3 pos) {
+ return Mth.floor(pos.y);
+ }
+
+ public static int getBlockZ(final Vec3 pos) {
+ return Mth.floor(pos.z);
+ }
+
+ public static int getChunkX(final Vec3 pos) {
+ return Mth.floor(pos.x) >> 4;
+ }
+
+ public static int getChunkY(final Vec3 pos) {
+ return Mth.floor(pos.y) >> 4;
+ }
+
+ public static int getChunkZ(final Vec3 pos) {
+ return Mth.floor(pos.z) >> 4;
+ }
+
+ private CoordinateUtils() {
+ throw new RuntimeException();
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..0531f25aaad162386a029d33e68d7c8336b9d5d1
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java
@@ -0,0 +1,109 @@
+package ca.spottedleaf.moonrise.common.util;
+
+import java.util.Objects;
+
+public final class FlatBitsetUtil {
+
+ private static final int LOG2_LONG = 6;
+ private static final long ALL_SET = -1L;
+ private static final int BITS_PER_LONG = Long.SIZE;
+
+ // from inclusive
+ // to exclusive
+ public static int firstSet(final long[] bitset, final int from, final int to) {
+ if ((from | to | (to - from)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ int bitsetIdx = from >>> LOG2_LONG;
+ int bitIdx = from & ~(BITS_PER_LONG - 1);
+
+ long tmp = bitset[bitsetIdx] & (ALL_SET << from);
+ for (;;) {
+ if (tmp != 0L) {
+ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
+ return ret >= to ? -1 : ret;
+ }
+
+ bitIdx += BITS_PER_LONG;
+
+ if (bitIdx >= to) {
+ return -1;
+ }
+
+ tmp = bitset[++bitsetIdx];
+ }
+ }
+
+ // from inclusive
+ // to exclusive
+ public static int firstClear(final long[] bitset, final int from, final int to) {
+ if ((from | to | (to - from)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ // like firstSet, but invert the bitset
+
+ int bitsetIdx = from >>> LOG2_LONG;
+ int bitIdx = from & ~(BITS_PER_LONG - 1);
+
+ long tmp = (~bitset[bitsetIdx]) & (ALL_SET << from);
+ for (;;) {
+ if (tmp != 0L) {
+ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
+ return ret >= to ? -1 : ret;
+ }
+
+ bitIdx += BITS_PER_LONG;
+
+ if (bitIdx >= to) {
+ return -1;
+ }
+
+ tmp = ~bitset[++bitsetIdx];
+ }
+ }
+
+ // from inclusive
+ // to exclusive
+ public static void clearRange(final long[] bitset, final int from, int to) {
+ if ((from | to | (to - from)) < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ if (from == to) {
+ return;
+ }
+
+ --to;
+
+ final int fromBitsetIdx = from >>> LOG2_LONG;
+ final int toBitsetIdx = to >>> LOG2_LONG;
+
+ final long keepFirst = ~(ALL_SET << from);
+ final long keepLast = ~(ALL_SET >>> ((BITS_PER_LONG - 1) ^ to));
+
+ Objects.checkFromToIndex(fromBitsetIdx, toBitsetIdx, bitset.length);
+
+ if (fromBitsetIdx == toBitsetIdx) {
+ // special case: need to keep both first and last
+ bitset[fromBitsetIdx] &= (keepFirst | keepLast);
+ } else {
+ bitset[fromBitsetIdx] &= keepFirst;
+
+ for (int i = fromBitsetIdx + 1; i < toBitsetIdx; ++i) {
+ bitset[i] = 0L;
+ }
+
+ bitset[toBitsetIdx] &= keepLast;
+ }
+ }
+
+ // from inclusive
+ // to exclusive
+ public static boolean isRangeSet(final long[] bitset, final int from, final int to) {
+ return firstClear(bitset, from, to) == -1;
+ }
+
+
+ private FlatBitsetUtil() {}
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..91efda726b87a8a8f28dee84e31b6a7063752ebd
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/JsonUtil.java
@@ -0,0 +1,34 @@
+package ca.spottedleaf.moonrise.common.util;
+
+import com.google.gson.JsonElement;
+import com.google.gson.internal.Streams;
+import com.google.gson.stream.JsonWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+
+public final class JsonUtil {
+
+ public static void writeJson(final JsonElement element, final File file) throws IOException {
+ final StringWriter stringWriter = new StringWriter();
+ final JsonWriter jsonWriter = new JsonWriter(stringWriter);
+ jsonWriter.setIndent(" ");
+ jsonWriter.setLenient(false);
+ Streams.write(element, jsonWriter);
+
+ final String jsonString = stringWriter.toString();
+
+ final File parent = file.getParentFile();
+ if (parent != null) {
+ parent.mkdirs();
+ }
+ file.createNewFile();
+ try (final PrintStream out = new PrintStream(new FileOutputStream(file), false, StandardCharsets.UTF_8)) {
+ out.print(jsonString);
+ }
+ }
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
new file mode 100644
index 0000000000000000000000000000000000000000..97848869df61648fc415e4d39f409f433202c274
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
@@ -0,0 +1,14 @@
+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
+ // https://github.com/FabricMC/Mixin/pull/147
+ public static long[] clone(final long[] values) {
+ return values.clone();
+ }
+
+ public static byte[] clone(final byte[] 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..632920e04686d8a0fd0a60e87348be1fe7862a3c
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
@@ -0,0 +1,101 @@
+package ca.spottedleaf.moonrise.common.util;
+
+import ca.spottedleaf.concurrentutil.executor.thread.PrioritisedThreadPool;
+import ca.spottedleaf.moonrise.common.PlatformHooks;
+import com.mojang.logging.LogUtils;
+import org.slf4j.Logger;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+public final class MoonriseCommon {
+
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+
+ public static final PrioritisedThreadPool WORKER_POOL = new PrioritisedThreadPool(
+ new Consumer<>() {
+ private final AtomicInteger idGenerator = new AtomicInteger();
+
+ @Override
+ public void accept(Thread thread) {
+ thread.setDaemon(true);
+ thread.setName(PlatformHooks.get().getBrand() + " Common Worker #" + this.idGenerator.getAndIncrement());
+ thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(final Thread thread, final Throwable throwable) {
+ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
+ }
+ });
+ }
+ }
+ );
+ public static final long WORKER_QUEUE_HOLD_TIME = (long)(20.0e6); // 20ms
+ public static final int CLIENT_DIVISION = 0;
+ public static final PrioritisedThreadPool.ExecutorGroup RENDER_EXECUTOR_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(CLIENT_DIVISION, 0);
+ public static final int SERVER_DIVISION = 1;
+ public static final PrioritisedThreadPool.ExecutorGroup PARALLEL_GEN_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0);
+ public static final PrioritisedThreadPool.ExecutorGroup RADIUS_AWARE_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0);
+ public static final PrioritisedThreadPool.ExecutorGroup LOAD_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0);
+
+ public static void adjustWorkerThreads(final int configWorkerThreads, final int configIoThreads) {
+ int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2;
+ if (defaultWorkerThreads <= 4) {
+ defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2;
+ } else {
+ defaultWorkerThreads = defaultWorkerThreads / 2;
+ }
+ defaultWorkerThreads = Integer.getInteger(PlatformHooks.get().getBrand() + ".WorkerThreadCount", Integer.valueOf(defaultWorkerThreads));
+
+ int workerThreads = configWorkerThreads;
+
+ if (workerThreads <= 0) {
+ workerThreads = defaultWorkerThreads;
+ }
+
+ final int ioThreads = Math.max(1, configIoThreads);
+
+ WORKER_POOL.adjustThreadCount(workerThreads);
+ IO_POOL.adjustThreadCount(ioThreads);
+
+ LOGGER.info(PlatformHooks.get().getBrand() + " is using " + workerThreads + " worker threads, " + ioThreads + " I/O threads");
+ }
+
+ public static final PrioritisedThreadPool IO_POOL = new PrioritisedThreadPool(
+ new Consumer<>() {
+ private final AtomicInteger idGenerator = new AtomicInteger();
+
+ @Override
+ public void accept(final Thread thread) {
+ thread.setDaemon(true);
+ thread.setName(PlatformHooks.get().getBrand() + " I/O Worker #" + this.idGenerator.getAndIncrement());
+ thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(final Thread thread, final Throwable throwable) {
+ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
+ }
+ });
+ }
+ }
+ );
+ public static final long IO_QUEUE_HOLD_TIME = (long)(100.0e6); // 100ms
+ public static final PrioritisedThreadPool.ExecutorGroup CLIENT_PROFILER_IO_GROUP = IO_POOL.createExecutorGroup(CLIENT_DIVISION, 0);
+ public static final PrioritisedThreadPool.ExecutorGroup SERVER_REGION_IO_GROUP = IO_POOL.createExecutorGroup(SERVER_DIVISION, 0);
+
+ public static void haltExecutors() {
+ MoonriseCommon.WORKER_POOL.shutdown(false);
+ LOGGER.info("Awaiting termination of worker pool for up to 60s...");
+ if (!MoonriseCommon.WORKER_POOL.join(TimeUnit.SECONDS.toMillis(60L))) {
+ LOGGER.error("Worker pool did not shut down in time!");
+ MoonriseCommon.WORKER_POOL.halt(false);
+ }
+
+ MoonriseCommon.IO_POOL.shutdown(false);
+ LOGGER.info("Awaiting termination of I/O pool for up to 60s...");
+ if (!MoonriseCommon.IO_POOL.join(TimeUnit.SECONDS.toMillis(60L))) {
+ LOGGER.error("I/O pool did not shut down in time!");
+ MoonriseCommon.IO_POOL.halt(false);
+ }
+ }
+
+ 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..559c959aff3c9deef867b9e425fba3e2e669cac6
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
@@ -0,0 +1,11 @@
+package ca.spottedleaf.moonrise.common.util;
+
+import ca.spottedleaf.moonrise.common.PlatformHooks;
+
+public final class MoonriseConstants {
+
+ public static final int MAX_VIEW_DISTANCE = Integer.getInteger(PlatformHooks.get().getBrand() + ".MaxViewDistance", 32);
+
+ private MoonriseConstants() {}
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleRandom.java b/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleRandom.java
new file mode 100644
index 0000000000000000000000000000000000000000..a9ff1c1a70faf4b7a64b265932f07a8b8f00c1ff
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleRandom.java
@@ -0,0 +1,52 @@
+package ca.spottedleaf.moonrise.common.util;
+
+import net.minecraft.world.level.levelgen.LegacyRandomSource;
+
+/**
+ * Avoid costly CAS of superclass
+ */
+public final class SimpleRandom extends LegacyRandomSource {
+
+ private static final long MULTIPLIER = 25214903917L;
+ private static final long ADDEND = 11L;
+ private static final int BITS = 48;
+ private static final long MASK = (1L << BITS) - 1;
+
+ private long value;
+
+ public SimpleRandom(final long seed) {
+ super(0L);
+ this.value = seed;
+ }
+
+ @Override
+ public void setSeed(final long seed) {
+ this.value = (seed ^ MULTIPLIER) & MASK;
+ }
+
+ private long advanceSeed() {
+ return this.value = ((this.value * MULTIPLIER) + ADDEND) & MASK;
+ }
+
+ @Override
+ public int next(final int bits) {
+ return (int)(this.advanceSeed() >>> (BITS - bits));
+ }
+
+ @Override
+ public int nextInt() {
+ final long seed = this.advanceSeed();
+ return (int)(seed >>> (BITS - Integer.SIZE));
+ }
+
+ @Override
+ public int nextInt(final int bound) {
+ if (bound <= 0) {
+ throw new IllegalArgumentException();
+ }
+
+ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/
+ final long value = this.advanceSeed() >>> (BITS - Integer.SIZE);
+ return (int)((value * (long)bound) >>> Integer.SIZE);
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..217d1f908a36a5177ba3cbb80a33f73d4dab0fa0
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
@@ -0,0 +1,143 @@
+package ca.spottedleaf.moonrise.common.util;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.phys.AABB;
+import net.minecraft.world.phys.Vec3;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class TickThread extends Thread {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TickThread.class);
+
+ /**
+ * @deprecated
+ */
+ @Deprecated
+ public static void ensureTickThread(final String reason) {
+ if (!isTickThread()) {
+ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
+ throw new IllegalStateException(reason);
+ }
+ }
+
+ public static void ensureTickThread(final Level world, final BlockPos pos, final String reason) {
+ if (!isTickThreadFor(world, pos)) {
+ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
+ throw new IllegalStateException(reason);
+ }
+ }
+
+ public static void ensureTickThread(final Level world, final ChunkPos pos, final String reason) {
+ if (!isTickThreadFor(world, pos)) {
+ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
+ throw new IllegalStateException(reason);
+ }
+ }
+
+ public static void ensureTickThread(final Level world, final int chunkX, final int chunkZ, final String reason) {
+ if (!isTickThreadFor(world, chunkX, chunkZ)) {
+ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
+ throw new IllegalStateException(reason);
+ }
+ }
+
+ public static void ensureTickThread(final Entity entity, final String reason) {
+ if (!isTickThreadFor(entity)) {
+ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
+ throw new IllegalStateException(reason);
+ }
+ }
+
+ public static void ensureTickThread(final Level world, final AABB aabb, final String reason) {
+ if (!isTickThreadFor(world, aabb)) {
+ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
+ throw new IllegalStateException(reason);
+ }
+ }
+
+ public static void ensureTickThread(final Level world, final double blockX, final double blockZ, final String reason) {
+ if (!isTickThreadFor(world, blockX, blockZ)) {
+ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
+ throw new IllegalStateException(reason);
+ }
+ }
+
+ public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */
+
+ private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
+
+ public TickThread(final String name) {
+ this(null, name);
+ }
+
+ public TickThread(final Runnable run, final String name) {
+ this(null, run, name);
+ }
+
+ public TickThread(final ThreadGroup group, final Runnable run, final String name) {
+ this(group, run, name, ID_GENERATOR.incrementAndGet());
+ }
+
+ private TickThread(final ThreadGroup group, final Runnable run, final String name, final int id) {
+ super(group, run, name);
+ this.id = id;
+ }
+
+ public static TickThread getCurrentTickThread() {
+ return (TickThread)Thread.currentThread();
+ }
+
+ public static boolean isTickThread() {
+ return Thread.currentThread() instanceof TickThread;
+ }
+
+ public static boolean isShutdownThread() {
+ return false;
+ }
+
+ public static boolean isTickThreadFor(final Level world, final BlockPos pos) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final ChunkPos pos) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final Vec3 pos) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final AABB aabb) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final double blockX, final double blockZ) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final Vec3 position, final Vec3 deltaMovement, final int buffer) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ, final int radius) {
+ return isTickThread();
+ }
+
+ public static boolean isTickThreadFor(final Entity entity) {
+ return isTickThread();
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..efda2688ae1254a82ba7f6bf8bf597ef224cbb86
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
@@ -0,0 +1,62 @@
+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.getMaxSectionY();
+ }
+
+ public static int getMaxSection(final Level world) {
+ return world.getMaxSectionY();
+ }
+
+ public static int getMinSection(final LevelHeightAccessor world) {
+ return world.getMinSectionY();
+ }
+
+ public static int getMinSection(final Level world) {
+ return world.getMinSectionY();
+ }
+
+ public static int getMaxLightSection(final LevelHeightAccessor world) {
+ return getMaxSection(world) + 1;
+ }
+
+ public static int getMinLightSection(final LevelHeightAccessor world) {
+ return getMinSection(world) - 1;
+ }
+
+
+
+ public static int getTotalSections(final LevelHeightAccessor world) {
+ return getMaxSection(world) - getMinSection(world) + 1;
+ }
+
+ public static int getTotalLightSections(final LevelHeightAccessor world) {
+ return getMaxLightSection(world) - getMinLightSection(world) + 1;
+ }
+
+ public static int getMinBlockY(final LevelHeightAccessor world) {
+ return getMinSection(world) << 4;
+ }
+
+ public static int getMaxBlockY(final LevelHeightAccessor world) {
+ return (getMaxSection(world) << 4) | 15;
+ }
+
+ public static String getWorldName(final Level world) {
+ if (world == null) {
+ return "null world";
+ }
+ return world.getWorld().getName(); // Paper
+ }
+
+ private WorldUtil() {
+ throw new RuntimeException();
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
new file mode 100644
index 0000000000000000000000000000000000000000..1aa6be257ce594d7a69fdff008cd29014a04fd75
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
@@ -0,0 +1,209 @@
+package ca.spottedleaf.moonrise.paper;
+
+import ca.spottedleaf.moonrise.common.PlatformHooks;
+import com.mojang.datafixers.DSL;
+import com.mojang.datafixers.DataFixer;
+import com.mojang.serialization.Dynamic;
+import net.minecraft.core.BlockPos;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.GenerationChunkHolder;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.Entity;
+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.LevelChunk;
+import net.minecraft.world.level.chunk.ProtoChunk;
+import net.minecraft.world.level.chunk.storage.SerializableChunkData;
+import net.minecraft.world.level.entity.EntityTypeTest;
+import net.minecraft.world.phys.AABB;
+import java.util.List;
+import java.util.function.Predicate;
+
+public final class PaperHooks implements PlatformHooks {
+
+ @Override
+ public String getBrand() {
+ return "Paper";
+ }
+
+ @Override
+ public int getLightEmission(final BlockState blockState, final BlockGetter world, final BlockPos pos) {
+ return blockState.getLightEmission();
+ }
+
+ @Override
+ public Predicate<BlockState> maybeHasLightEmission() {
+ return (final BlockState state) -> {
+ return state.getLightEmission() != 0;
+ };
+ }
+
+ @Override
+ public boolean hasCurrentlyLoadingChunk() {
+ return false;
+ }
+
+ @Override
+ public LevelChunk getCurrentlyLoadingChunk(final GenerationChunkHolder holder) {
+ return null;
+ }
+
+ @Override
+ public void setCurrentlyLoading(final GenerationChunkHolder holder, final LevelChunk levelChunk) {
+
+ }
+
+ @Override
+ public void chunkFullStatusComplete(final LevelChunk newChunk, final ProtoChunk original) {
+
+ }
+
+ @Override
+ public boolean allowAsyncTicketUpdates() {
+ return true;
+ }
+
+ @Override
+ public void onChunkHolderTicketChange(final ServerLevel world, final ChunkHolder holder, final int oldLevel, final int newLevel) {
+
+ }
+
+ @Override
+ public void chunkUnloadFromWorld(final LevelChunk chunk) {
+
+ }
+
+ @Override
+ public void chunkSyncSave(final ServerLevel world, final ChunkAccess chunk, final SerializableChunkData data) {
+
+ }
+
+ @Override
+ public void onChunkWatch(final ServerLevel world, final LevelChunk chunk, final ServerPlayer player) {
+
+ }
+
+ @Override
+ public void onChunkUnWatch(final ServerLevel world, final ChunkPos chunk, final ServerPlayer player) {
+
+ }
+
+ @Override
+ public void addToGetEntities(final Level world, final Entity entity, final AABB boundingBox, final Predicate<? super Entity> predicate, final List<Entity> into) {
+
+ }
+
+ @Override
+ public <T extends Entity> void addToGetEntities(final Level world, final EntityTypeTest<Entity, T> entityTypeTest, final AABB boundingBox, final Predicate<? super T> predicate, final List<? super T> into, final int maxCount) {
+
+ }
+
+ @Override
+ public void entityMove(final Entity entity, final long oldSection, final long newSection) {
+
+ }
+
+ @Override
+ public boolean screenEntity(final ServerLevel world, final Entity entity, final boolean fromDisk, final boolean event) {
+ return true;
+ }
+
+ @Override
+ public boolean configFixMC224294() {
+ return true;
+ }
+
+ @Override
+ public boolean configAutoConfigSendDistance() {
+ return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance;
+ }
+
+ @Override
+ public double configPlayerMaxLoadRate() {
+ return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate;
+ }
+
+ @Override
+ public double configPlayerMaxGenRate() {
+ return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate;
+ }
+
+ @Override
+ public double configPlayerMaxSendRate() {
+ return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate;
+ }
+
+ @Override
+ public int configPlayerMaxConcurrentLoads() {
+ return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads;
+ }
+
+ @Override
+ public int configPlayerMaxConcurrentGens() {
+ return io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates;
+ }
+
+ @Override
+ public long configAutoSaveInterval(final ServerLevel world) {
+ return world.paperConfig().chunks.autoSaveInterval.value();
+ }
+
+ @Override
+ public int configMaxAutoSavePerTick(final ServerLevel world) {
+ return world.paperConfig().chunks.maxAutoSaveChunksPerTick;
+ }
+
+ @Override
+ public boolean configFixMC159283() {
+ return true;
+ }
+
+ @Override
+ public boolean forceNoSave(final ChunkAccess chunk) {
+ return chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave;
+ }
+
+ @Override
+ public CompoundTag convertNBT(final DSL.TypeReference type, final DataFixer dataFixer, final CompoundTag nbt,
+ final int fromVersion, final int toVersion) {
+ return (CompoundTag)dataFixer.update(
+ type, new Dynamic<>(NbtOps.INSTANCE, nbt), fromVersion, toVersion
+ ).getValue();
+ }
+
+ @Override
+ public boolean hasMainChunkLoadHook() {
+ return false;
+ }
+
+ @Override
+ public void mainChunkLoad(final ChunkAccess chunk, final SerializableChunkData chunkData) {
+
+ }
+
+ @Override
+ public List<Entity> modifySavedEntities(final ServerLevel world, final int chunkX, final int chunkZ, final List<Entity> entities) {
+ return entities;
+ }
+
+ @Override
+ public void unloadEntity(final Entity entity) {
+ entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, org.bukkit.event.entity.EntityRemoveEvent.Cause.UNLOAD);
+ }
+
+ @Override
+ public void postLoadProtoChunk(final ServerLevel world, final ProtoChunk chunk) {
+ net.minecraft.world.level.chunk.status.ChunkStatusTasks.postLoadProtoChunk(world, chunk.getEntities());
+ }
+
+ @Override
+ public int modifyEntityTrackingRange(final Entity entity, final int currentRange) {
+ return org.spigotmc.TrackingRange.getEntityTrackingRange(entity, currentRange);
+ }
+}
diff --git a/src/main/java/com/mojang/logging/LogUtils.java b/src/main/java/com/mojang/logging/LogUtils.java
index 46cab7a8c7b87ab01b26074b04f5a02b3907cfc4..49019b4a9bc4e634d54a9b0acaf9229a5c896f85 100644
--- a/src/main/java/com/mojang/logging/LogUtils.java
+++ b/src/main/java/com/mojang/logging/LogUtils.java
@@ -61,4 +61,9 @@ public class LogUtils {
public static Logger getLogger() {
return LoggerFactory.getLogger(STACK_WALKER.getCallerClass());
}
+ // Paper start
+ public static Logger getClassLogger() {
+ return LoggerFactory.getLogger(STACK_WALKER.getCallerClass().getSimpleName());
+ }
+ // Paper end
}
diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
index f0d470d7770e119f734b9e72021c806d0ea8ecbd..c3fe4481dd35f80815716e48beeeb07b1f51e30b 100644
--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
@@ -217,7 +217,7 @@ public class GlobalConfiguration extends ConfigurationPart {
@PostProcess
private void postProcess() {
-
+ ca.spottedleaf.moonrise.common.util.MoonriseCommon.adjustWorkerThreads(this.workerThreads, this.ioThreads);
}
}
diff --git a/src/main/java/io/papermc/paper/util/IntervalledCounter.java b/src/main/java/io/papermc/paper/util/IntervalledCounter.java
new file mode 100644
index 0000000000000000000000000000000000000000..197224e31175252d8438a8df585bbb65f2288d7f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/IntervalledCounter.java
@@ -0,0 +1,129 @@
+package io.papermc.paper.util;
+
+public final class IntervalledCounter {
+
+ private static final int INITIAL_SIZE = 8;
+
+ protected long[] times;
+ protected long[] counts;
+ protected final long interval;
+ protected long minTime;
+ protected long sum;
+ protected int head; // inclusive
+ protected int tail; // exclusive
+
+ public IntervalledCounter(final long interval) {
+ this.times = new long[INITIAL_SIZE];
+ this.counts = new long[INITIAL_SIZE];
+ this.interval = interval;
+ }
+
+ public void updateCurrentTime() {
+ this.updateCurrentTime(System.nanoTime());
+ }
+
+ public void updateCurrentTime(final long currentTime) {
+ long sum = this.sum;
+ int head = this.head;
+ final int tail = this.tail;
+ final long minTime = currentTime - this.interval;
+
+ final int arrayLen = this.times.length;
+
+ // guard against overflow by using subtraction
+ while (head != tail && this.times[head] - minTime < 0) {
+ sum -= this.counts[head];
+ // there are two ways we can do this:
+ // 1. free the count when adding
+ // 2. free it now
+ // option #2
+ this.counts[head] = 0;
+ if (++head >= arrayLen) {
+ head = 0;
+ }
+ }
+
+ this.sum = sum;
+ this.head = head;
+ this.minTime = minTime;
+ }
+
+ public void addTime(final long currTime) {
+ this.addTime(currTime, 1L);
+ }
+
+ public void addTime(final long currTime, final long count) {
+ // guard against overflow by using subtraction
+ if (currTime - this.minTime < 0) {
+ return;
+ }
+ int nextTail = (this.tail + 1) % this.times.length;
+ if (nextTail == this.head) {
+ this.resize();
+ nextTail = (this.tail + 1) % this.times.length;
+ }
+
+ this.times[this.tail] = currTime;
+ this.counts[this.tail] += count;
+ this.sum += count;
+ this.tail = nextTail;
+ }
+
+ public void updateAndAdd(final long count) {
+ final long currTime = System.nanoTime();
+ this.updateCurrentTime(currTime);
+ this.addTime(currTime, count);
+ }
+
+ public void updateAndAdd(final long count, final long currTime) {
+ this.updateCurrentTime(currTime);
+ this.addTime(currTime, count);
+ }
+
+ private void resize() {
+ final long[] oldElements = this.times;
+ final long[] oldCounts = this.counts;
+ final long[] newElements = new long[this.times.length * 2];
+ final long[] newCounts = new long[this.times.length * 2];
+ this.times = newElements;
+ this.counts = newCounts;
+
+ final int head = this.head;
+ final int tail = this.tail;
+ final int size = tail >= head ? (tail - head) : (tail + (oldElements.length - head));
+ this.head = 0;
+ this.tail = size;
+
+ if (tail >= head) {
+ // sequentially ordered from [head, tail)
+ System.arraycopy(oldElements, head, newElements, 0, size);
+ System.arraycopy(oldCounts, head, newCounts, 0, size);
+ } else {
+ // ordered from [head, length)
+ // then followed by [0, tail)
+
+ System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head);
+ System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail);
+
+ System.arraycopy(oldCounts, head, newCounts, 0, oldCounts.length - head);
+ System.arraycopy(oldCounts, 0, newCounts, oldCounts.length - head, tail);
+ }
+ }
+
+ // returns in units per second
+ public double getRate() {
+ return (double)this.sum / ((double)this.interval * 1.0E-9);
+ }
+
+ public long getInterval() {
+ return this.interval;
+ }
+
+ public long getSum() {
+ return this.sum;
+ }
+
+ public int totalDataPoints() {
+ return this.tail >= this.head ? (this.tail - this.head) : (this.tail + (this.counts.length - this.head));
+ }
+}
diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..e85e544506b4c762503a1cb490e6c0f5b1d563f4
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/MCUtil.java
@@ -0,0 +1,220 @@
+package io.papermc.paper.util;
+
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import io.papermc.paper.adventure.PaperAdventure;
+import io.papermc.paper.math.BlockPosition;
+import io.papermc.paper.math.FinePosition;
+import io.papermc.paper.math.Position;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import net.kyori.adventure.key.Key;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Holder;
+import net.minecraft.core.Vec3i;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.sounds.SoundEvent;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.phys.Vec3;
+import org.bukkit.Location;
+import org.bukkit.NamespacedKey;
+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
+import org.bukkit.craftbukkit.util.Waitable;
+
+public final class MCUtil {
+ public static final java.util.concurrent.Executor MAIN_EXECUTOR = (run) -> {
+ if (!isMainThread()) {
+ MinecraftServer.getServer().execute(run);
+ } else {
+ run.run();
+ }
+ };
+ public static final ThreadPoolExecutor asyncExecutor = new ThreadPoolExecutor(
+ 2, 2, 60L, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(),
+ new ThreadFactoryBuilder()
+ .setNameFormat("Paper Async Task Handler Thread - %1$d")
+ .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(MinecraftServer.LOGGER))
+ .build()
+ );
+
+ private MCUtil() {
+ }
+
+ public static List<ChunkPos> getSpiralOutChunks(BlockPos blockposition, int radius) {
+ List<ChunkPos> list = com.google.common.collect.Lists.newArrayList();
+
+ list.add(new ChunkPos(blockposition.getX() >> 4, blockposition.getZ() >> 4));
+ for (int r = 1; r <= radius; r++) {
+ int x = -r;
+ int z = r;
+
+ // Iterates the edge of half of the box; then negates for other half.
+ while (x <= r && z > -r) {
+ list.add(new ChunkPos((blockposition.getX() + (x << 4)) >> 4, (blockposition.getZ() + (z << 4)) >> 4));
+ list.add(new ChunkPos((blockposition.getX() - (x << 4)) >> 4, (blockposition.getZ() - (z << 4)) >> 4));
+
+ if (x < r) {
+ x++;
+ } else {
+ z--;
+ }
+ }
+ }
+ return list;
+ }
+
+ public static <T> CompletableFuture<T> ensureMain(CompletableFuture<T> future) {
+ return future.thenApplyAsync(r -> r, MAIN_EXECUTOR);
+ }
+
+ public static <T> void thenOnMain(CompletableFuture<T> future, Consumer<T> consumer) {
+ future.thenAcceptAsync(consumer, MAIN_EXECUTOR);
+ }
+
+ public static <T> void thenOnMain(CompletableFuture<T> future, BiConsumer<T, Throwable> consumer) {
+ future.whenCompleteAsync(consumer, MAIN_EXECUTOR);
+ }
+
+ public static boolean isMainThread() {
+ return MinecraftServer.getServer().isSameThread();
+ }
+
+ public static void ensureMain(Runnable run) {
+ ensureMain(null, run);
+ }
+
+ /**
+ * Ensures the target code is running on the main thread.
+ */
+ public static void ensureMain(String reason, Runnable run) {
+ if (!isMainThread()) {
+ if (reason != null) {
+ MinecraftServer.LOGGER.warn("Asynchronous " + reason + "!", new IllegalStateException());
+ }
+ MinecraftServer.getServer().processQueue.add(run);
+ return;
+ }
+ run.run();
+ }
+
+ public static <T> T ensureMain(Supplier<T> run) {
+ return ensureMain(null, run);
+ }
+
+ /**
+ * Ensures the target code is running on the main thread.
+ */
+ public static <T> T ensureMain(String reason, Supplier<T> run) {
+ if (!isMainThread()) {
+ if (reason != null) {
+ MinecraftServer.LOGGER.warn("Asynchronous " + reason + "! Blocking thread until it returns ", new IllegalStateException());
+ }
+ Waitable<T> wait = new Waitable<>() {
+ @Override
+ protected T evaluate() {
+ return run.get();
+ }
+ };
+ MinecraftServer.getServer().processQueue.add(wait);
+ try {
+ return wait.get();
+ } catch (InterruptedException | ExecutionException e) {
+ MinecraftServer.LOGGER.warn("Encountered exception", e);
+ }
+ return null;
+ }
+ return run.get();
+ }
+
+ public static double distance(double x1, double y1, double z1, double x2, double y2, double z2) {
+ return Math.sqrt(distanceSq(x1, y1, z1, x2, y2, z2));
+ }
+
+ public static double distanceSq(double x1, double y1, double z1, double x2, double y2, double z2) {
+ return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2);
+ }
+
+ public static Location toLocation(Level world, double x, double y, double z) {
+ return new Location(world.getWorld(), x, y, z);
+ }
+
+ public static Location toLocation(Level world, BlockPos pos) {
+ return new Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ());
+ }
+
+ public static BlockPos toBlockPosition(Location loc) {
+ return new BlockPos(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
+ }
+
+ public static BlockPos toBlockPos(Position pos) {
+ return new BlockPos(pos.blockX(), pos.blockY(), pos.blockZ());
+ }
+
+ public static FinePosition toPosition(Vec3 vector) {
+ return Position.fine(vector.x, vector.y, vector.z);
+ }
+
+ public static BlockPosition toPosition(Vec3i vector) {
+ return Position.block(vector.getX(), vector.getY(), vector.getZ());
+ }
+
+ public static Vec3 toVec3(Position position) {
+ return new Vec3(position.x(), position.y(), position.z());
+ }
+
+ public static boolean isEdgeOfChunk(BlockPos pos) {
+ final int modX = pos.getX() & 15;
+ final int modZ = pos.getZ() & 15;
+ return (modX == 0 || modX == 15 || modZ == 0 || modZ == 15);
+ }
+
+ public static void scheduleAsyncTask(Runnable run) {
+ asyncExecutor.execute(run);
+ }
+
+ public static <T> ResourceKey<T> toResourceKey(
+ final ResourceKey<? extends net.minecraft.core.Registry<T>> registry,
+ final NamespacedKey namespacedKey
+ ) {
+ return ResourceKey.create(registry, CraftNamespacedKey.toMinecraft(namespacedKey));
+ }
+
+ public static NamespacedKey fromResourceKey(final ResourceKey<?> key) {
+ return CraftNamespacedKey.fromMinecraft(key.location());
+ }
+
+ public static Holder<SoundEvent> keyToSound(Key key) {
+ ResourceLocation soundId = PaperAdventure.asVanilla(key);
+ return BuiltInRegistries.SOUND_EVENT.wrapAsHolder(BuiltInRegistries.SOUND_EVENT.getOptional(soundId).orElse(SoundEvent.createVariableRangeEvent(soundId)));
+ }
+
+ public static <A, M> List<A> transformUnmodifiable(final List<? extends M> nms, final Function<? super M, ? extends A> converter) {
+ return Collections.unmodifiableList(Lists.transform(nms, converter::apply));
+ }
+
+ public static <A, M> Collection<A> transformUnmodifiable(final Collection<? extends M> nms, final Function<? super M, ? extends A> converter) {
+ return Collections.unmodifiableCollection(Collections2.transform(nms, converter::apply));
+ }
+
+ public static <A, M, C extends Collection<M>> void addAndConvert(final C target, final Collection<A> toAdd, final Function<? super A, ? extends M> converter) {
+ for (final A value : toAdd) {
+ target.add(converter.apply(value));
+ }
+ }
+}
diff --git a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..f7114d5b8f2f93f62883e24da29afaf9f74ee1a6
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
@@ -0,0 +1,24 @@
+package io.papermc.paper.util;
+
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.plugin.java.PluginClassLoader;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Optional;
+
+public class StackWalkerUtil {
+
+ @Nullable
+ public static JavaPlugin getFirstPluginCaller() {
+ Optional<JavaPlugin> foundFrame = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
+ .walk(stream -> stream
+ .filter(frame -> frame.getDeclaringClass().getClassLoader() instanceof PluginClassLoader)
+ .map((frame) -> {
+ PluginClassLoader classLoader = (PluginClassLoader) frame.getDeclaringClass().getClassLoader();
+ return classLoader.getPlugin();
+ })
+ .findFirst());
+
+ return foundFrame.orElse(null);
+ }
+}
diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java
index 0842080f1840a83b34c2cc829dfddba78ba12157..2ff5a6517d717bbd4c944572040bd30866347341 100644
--- a/src/main/java/net/minecraft/Util.java
+++ b/src/main/java/net/minecraft/Util.java
@@ -136,7 +136,7 @@ public class Util {
}
public static long getNanos() {
- return timeSource.getAsLong();
+ return System.nanoTime(); // Paper
}
public static long getEpochMillis() {
diff --git a/src/main/java/net/minecraft/nbt/CompoundTag.java b/src/main/java/net/minecraft/nbt/CompoundTag.java
index 6b588a4e639da11edeb933ec2bc4afde8f0b47f1..d721ae6d9b54cbace5b7ade657e9739fc7c42d14 100644
--- a/src/main/java/net/minecraft/nbt/CompoundTag.java
+++ b/src/main/java/net/minecraft/nbt/CompoundTag.java
@@ -235,6 +235,10 @@ public class CompoundTag implements Tag {
this.tags.put(key, NbtUtils.createUUID(value));
}
+
+ /**
+ * You must use {@link #hasUUID(String)} before or else it <b>will</b> throw an NPE.
+ */
public UUID getUUID(String key) {
return NbtUtils.loadUUID(this.get(key));
}
diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
index 821de8e17031bf54edb61fc577d7c5a90aabfd2e..77985072928a1b892fb4f7dec1d0899324780082 100644
--- a/src/main/java/net/minecraft/network/Connection.java
+++ b/src/main/java/net/minecraft/network/Connection.java
@@ -121,6 +121,18 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
BandwidthDebugMonitor bandwidthDebugMonitor;
public String hostname = ""; // CraftBukkit - add field
+ // Paper start - add utility methods
+ public final net.minecraft.server.level.ServerPlayer getPlayer() {
+ if (this.packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl impl) {
+ return impl.player;
+ } else if (this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl impl) {
+ org.bukkit.craftbukkit.entity.CraftPlayer player = impl.getCraftPlayer();
+ return player == null ? null : player.getHandle();
+ }
+ return null;
+ }
+ // Paper end - add utility methods
+
public Connection(PacketFlow side) {
this.receiving = side;
}
diff --git a/src/main/java/net/minecraft/network/PacketEncoder.java b/src/main/java/net/minecraft/network/PacketEncoder.java
index e82263b4b48c4544a5ade6613ea284f4de2b4c80..a58f67973b4ed986065860263c7a42214640520d 100644
--- a/src/main/java/net/minecraft/network/PacketEncoder.java
+++ b/src/main/java/net/minecraft/network/PacketEncoder.java
@@ -31,7 +31,7 @@ public class PacketEncoder<T extends PacketListener> extends MessageToByteEncode
JvmProfiler.INSTANCE.onPacketSent(this.protocolInfo.id(), packetType, channelHandlerContext.channel().remoteAddress(), i);
} catch (Throwable var9) {
- LOGGER.error("Error sending packet {}", packetType, var9);
+ LOGGER.error("Error sending packet {} (skippable? {})", packetType, packet.isSkippable(), var9);
if (packet.isSkippable()) {
throw new SkipPacketException(var9);
}
diff --git a/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java b/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java
index e113cd9d93750cf59712b06db62591876b4efbac..1789c0c71d968b386060bd6dc2630e8a078c32e2 100644
--- a/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java
+++ b/src/main/java/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java
@@ -47,4 +47,14 @@ public record ClientboundCustomQueryPacket(int transactionId, CustomQueryPayload
public void handle(ClientLoginPacketListener listener) {
listener.handleCustomQuery(this);
}
+
+ // Paper start - MC Utils - default query payloads
+ public static record PlayerInfoChannelPayload(ResourceLocation id, FriendlyByteBuf buffer) implements CustomQueryPayload {
+
+ @Override
+ public void write(final FriendlyByteBuf buf) {
+ buf.writeBytes(this.buffer.copy());
+ }
+ }
+ // Paper end - MC Utils - default query payloads
}
diff --git a/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java b/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java
index 3e5a85a7ad6149b04622c254fbc2e174896a4128..3f662692ed4846e026a9d48595e7b3b22404a031 100644
--- a/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java
+++ b/src/main/java/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java
@@ -20,7 +20,17 @@ public record ServerboundCustomQueryAnswerPacket(int transactionId, @Nullable Cu
}
private static CustomQueryAnswerPayload readPayload(int queryId, FriendlyByteBuf buf) {
- return readUnknownPayload(buf);
+ // Paper start - MC Utils - default query payloads
+ FriendlyByteBuf buffer = buf.readNullable((buf2) -> {
+ int i = buf2.readableBytes();
+ if (i >= 0 && i <= MAX_PAYLOAD_SIZE) {
+ return new FriendlyByteBuf(buf2.readBytes(i));
+ } else {
+ throw new IllegalArgumentException("Payload may not be larger than " + MAX_PAYLOAD_SIZE + " bytes");
+ }
+ });
+ return buffer == null ? null : new net.minecraft.network.protocol.login.ServerboundCustomQueryAnswerPacket.QueryAnswerPayload(buffer);
+ // Paper end - MC Utils - default query payloads
}
private static CustomQueryAnswerPayload readUnknownPayload(FriendlyByteBuf buf) {
@@ -47,4 +57,21 @@ public record ServerboundCustomQueryAnswerPacket(int transactionId, @Nullable Cu
public void handle(ServerLoginPacketListener listener) {
listener.handleCustomQueryPacket(this);
}
+
+ // Paper start - MC Utils - default query payloads
+ public static final class QueryAnswerPayload implements CustomQueryAnswerPayload {
+
+ public final FriendlyByteBuf buffer;
+
+ public QueryAnswerPayload(final net.minecraft.network.FriendlyByteBuf buffer) {
+ this.buffer = buffer;
+ }
+
+ @Override
+ public void write(final net.minecraft.network.FriendlyByteBuf buf) {
+ buf.writeBytes(this.buffer.copy());
+ }
+ }
+ // Paper end - MC Utils - default query payloads
+
}
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 8e16bc7da15824723f1d7d4bff87fac181978500..709330ca9caa82a6de71767b3d5c32f97ea1d68b 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -988,6 +988,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
MinecraftServer.LOGGER.error("Failed to unlock level {}", this.storageSource.getLevelId(), ioexception1);
}
// Spigot start
+ io.papermc.paper.util.MCUtil.asyncExecutor.shutdown(); // Paper
+ try { io.papermc.paper.util.MCUtil.asyncExecutor.awaitTermination(30, java.util.concurrent.TimeUnit.SECONDS); // Paper
+ } catch (java.lang.InterruptedException ignored) {} // Paper
if (org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) {
MinecraftServer.LOGGER.info("Saving usercache.json");
this.getProfileCache().save();
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
index 59bc334ade71c106e01e54db8d21fb65563dd3f1..b9ab241b930edc63a39dbbcf14cd0b5edacb9ea9 100644
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -37,9 +37,9 @@ public class ChunkHolder extends GenerationChunkHolder {
public static final ChunkResult<LevelChunk> UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk");
private static final CompletableFuture<ChunkResult<LevelChunk>> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK);
private final LevelHeightAccessor levelHeightAccessor;
- private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture;
- private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture;
- private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture;
+ private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
+ private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
+ private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
public int oldTicketLevel;
private int ticketLevel;
private int queueLevel;
@@ -101,7 +101,7 @@ public class ChunkHolder extends GenerationChunkHolder {
}
@Nullable
- public LevelChunk getTickingChunk() {
+ public final LevelChunk getTickingChunk() { // Paper - final for inline
return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error
}
@@ -361,12 +361,28 @@ public class ChunkHolder extends GenerationChunkHolder {
this.wasAccessibleSinceLastSave |= flag1;
if (!flag && flag1) {
+ int expectCreateCount = ++this.fullChunkCreateCount; // Paper
this.fullChunkFuture = chunkLoadingManager.prepareAccessibleChunk(this);
this.scheduleFullChunkPromotion(chunkLoadingManager, this.fullChunkFuture, executor, FullChunkStatus.FULL);
+ // Paper start - cache ticking ready status
+ this.fullChunkFuture.thenAccept(chunkResult -> {
+ chunkResult.ifSuccess(chunk -> {
+ if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
+ ChunkHolder.this.isFullChunkReady = true;
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkBorder(chunk, this);
+ }
+ });
+ });
+ // Paper end - cache ticking ready status
this.addSaveDependency(this.fullChunkFuture);
}
if (flag && !flag1) {
+ // Paper start
+ if (this.isFullChunkReady) {
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
+ }
+ // Paper end
this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
}
@@ -377,11 +393,25 @@ public class ChunkHolder extends GenerationChunkHolder {
if (!flag2 && flag3) {
this.tickingChunkFuture = chunkLoadingManager.prepareTickingChunk(this);
this.scheduleFullChunkPromotion(chunkLoadingManager, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING);
+ // Paper start - cache ticking ready status
+ this.tickingChunkFuture.thenAccept(chunkResult -> {
+ chunkResult.ifSuccess(chunk -> {
+ // note: Here is a very good place to add callbacks to logic waiting on this.
+ ChunkHolder.this.isTickingReady = true;
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkTicking(chunk, this);
+ });
+ });
+ // Paper end
this.addSaveDependency(this.tickingChunkFuture);
}
if (flag2 && !flag3) {
- this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
+ // Paper start
+ if (this.isTickingReady) {
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
+ }
+ // Paper end
+ this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage
this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
}
@@ -395,11 +425,24 @@ public class ChunkHolder extends GenerationChunkHolder {
this.entityTickingChunkFuture = chunkLoadingManager.prepareEntityTickingChunk(this);
this.scheduleFullChunkPromotion(chunkLoadingManager, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING);
+ // Paper start - cache ticking ready status
+ this.entityTickingChunkFuture.thenAccept(chunkResult -> {
+ chunkResult.ifSuccess(chunk -> {
+ ChunkHolder.this.isEntityTickingReady = true;
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkEntityTicking(chunk, this);
+ });
+ });
+ // Paper end
this.addSaveDependency(this.entityTickingChunkFuture);
}
if (flag4 && !flag5) {
- this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
+ // Paper start
+ if (this.isEntityTickingReady) {
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
+ }
+ // Paper end
+ this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
}
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index ad2d29b106df33965090be521d4f945601965a00..261943f1f188643793a72bd239dfc5fe604e3b99 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -174,6 +174,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
};
// CraftBukkit end
+ // Paper start
+ public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
+ return this.pendingUnloads.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+ // Paper end
+
public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
this.visibleChunkMap = this.updatingChunkMap.clone();
@@ -229,6 +235,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.chunksToEagerlySave.add(pos.toLong());
}
+ // Paper start
+ public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
+ return -1;
+ }
+ // Paper end
+
protected ChunkGenerator generator() {
return this.worldGenContext.generator();
}
@@ -385,9 +397,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
};
stringbuilder.append("Updating:").append(System.lineSeparator());
- this.updatingChunkMap.values().forEach(consumer);
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
stringbuilder.append("Visible:").append(System.lineSeparator());
- this.visibleChunkMap.values().forEach(consumer);
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).forEach(consumer); // Paper
CrashReport crashreport = CrashReport.forThrowable(exception, "Chunk loading");
CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk loading");
@@ -429,6 +441,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
holder.setTicketLevel(level);
} else {
holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this::onLevelChange, this);
+ // Paper start
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderCreate(this.level, holder);
+ // Paper end
}
this.updatingChunkMap.put(pos, holder);
@@ -458,7 +473,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
protected void saveAllChunks(boolean flush) {
if (flush) {
- List<ChunkHolder> list = this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList();
+ List<ChunkHolder> list = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper
MutableBoolean mutableboolean = new MutableBoolean();
do {
@@ -484,7 +499,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
} else {
this.nextChunkSaveTime.clear();
long i = Util.getMillis();
- ObjectIterator objectiterator = this.visibleChunkMap.values().iterator();
+ Iterator<ChunkHolder> objectiterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
while (objectiterator.hasNext()) {
ChunkHolder playerchunk = (ChunkHolder) objectiterator.next();
@@ -509,7 +524,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public boolean hasWork() {
- return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.worldgenTaskDispatcher.hasWork() || this.lightTaskDispatcher.hasWork() || this.distanceManager.hasTickets();
+ return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || ca.spottedleaf.moonrise.common.util.ChunkSystem.hasAnyChunkHolders(this.level) || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.worldgenTaskDispatcher.hasWork() || this.lightTaskDispatcher.hasWork() || this.distanceManager.hasTickets();
}
private void processUnloads(BooleanSupplier shouldKeepTicking) {
@@ -568,8 +583,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.scheduleUnload(pos, chunk);
} else {
ChunkAccess ichunkaccess = chunk.getLatestChunk();
-
- if (this.pendingUnloads.remove(pos, chunk) && ichunkaccess != null) {
+ // Paper start
+ boolean removed;
+ if ((removed = this.pendingUnloads.remove(pos, chunk)) && ichunkaccess != null) {
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunk);
+ // Paper end
LevelChunk chunk1;
if (ichunkaccess instanceof LevelChunk) {
@@ -587,7 +605,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.lightEngine.tryScheduleUpdate();
this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null);
this.nextChunkSaveTime.remove(ichunkaccess.getPos().toLong());
- }
+ } else if (removed) { // Paper start
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunk);
+ } // Paper end
}
};
@@ -936,7 +956,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
}
- protected void setServerViewDistance(int watchDistance) {
+ public void setServerViewDistance(int watchDistance) { // Paper - public
int j = Mth.clamp(watchDistance, 2, 32);
if (j != this.serverViewDistance) {
@@ -953,7 +973,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
- int getPlayerViewDistance(ServerPlayer player) {
+ public int getPlayerViewDistance(ServerPlayer player) { // Paper - public
return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance);
}
@@ -982,7 +1002,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public int size() {
- return this.visibleChunkMap.size();
+ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper
}
public DistanceManager getDistanceManager() {
@@ -990,19 +1010,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
protected Iterable<ChunkHolder> getChunks() {
- return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
+ return Iterables.unmodifiableIterable(ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level)); // Paper
}
void dumpChunks(Writer writer) throws IOException {
CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer);
TickingTracker tickingtracker = this.distanceManager.tickingTracker();
- ObjectBidirectionalIterator objectbidirectionaliterator = this.visibleChunkMap.long2ObjectEntrySet().iterator();
+ Iterator<ChunkHolder> objectbidirectionaliterator = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
while (objectbidirectionaliterator.hasNext()) {
- Entry<ChunkHolder> entry = (Entry) objectbidirectionaliterator.next();
- long i = entry.getLongKey();
+ ChunkHolder playerchunk = objectbidirectionaliterator.next(); // Paper
+ long i = playerchunk.pos.toLong(); // Paper
ChunkPos chunkcoordintpair = new ChunkPos(i);
- ChunkHolder playerchunk = (ChunkHolder) entry.getValue();
+ // Paper - move up
Optional<ChunkAccess> optional = Optional.ofNullable(playerchunk.getLatestChunk());
Optional<LevelChunk> optional1 = optional.flatMap((ichunkaccess) -> {
return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty();
@@ -1445,7 +1465,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
});
}
- private class ChunkDistanceManager extends DistanceManager {
+ public class ChunkDistanceManager extends DistanceManager { // Paper - public
protected ChunkDistanceManager(final Executor workerExecutor, final Executor mainThreadExecutor) {
super(workerExecutor, mainThreadExecutor);
diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
index 019228b1809e3816b0b4dfb9f19b8d42876cc240..6c2339d6a93172e25040c4868a3a47473a1f5336 100644
--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
@@ -385,7 +385,7 @@ public abstract class DistanceManager {
}
public void removeTicketsOnClosing() {
- ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT);
+ ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve
ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
while (objectiterator.hasNext()) {
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
index 9cdcab885a915990a679f3fc9ae6885f7d125bfd..3e35a64b4b92ec25789e85c7445375dd899e1805 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -74,6 +74,13 @@ public class ServerChunkCache extends ChunkSource {
@Nullable
@VisibleForDebug
private NaturalSpawner.SpawnState lastSpawnState;
+ // Paper start
+ private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<net.minecraft.world.level.chunk.LevelChunk> fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>();
+ public int getFullChunksCount() {
+ return this.fullChunks.size();
+ }
+ long chunkFutureAwaitCounter;
+ // Paper end
public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) {
this.level = world;
@@ -104,6 +111,54 @@ public class ServerChunkCache extends ChunkSource {
return chunk.getFullChunkNow() != null;
}
// CraftBukkit end
+ // Paper start
+ public void addLoadedChunk(LevelChunk chunk) {
+ this.fullChunks.put(chunk.coordinateKey, chunk);
+ }
+
+ public void removeLoadedChunk(LevelChunk chunk) {
+ this.fullChunks.remove(chunk.coordinateKey);
+ }
+
+ @Nullable
+ public ChunkAccess getChunkAtImmediately(int x, int z) {
+ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
+ if (holder == null) {
+ return null;
+ }
+
+ return holder.getLatestChunk();
+ }
+
+ public <T> void addTicketAtLevel(TicketType<T> ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) {
+ this.distanceManager.addTicket(ticketType, chunkPos, ticketLevel, identifier);
+ }
+
+ public <T> void removeTicketAtLevel(TicketType<T> ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) {
+ this.distanceManager.removeTicket(ticketType, chunkPos, ticketLevel, identifier);
+ }
+
+ // "real" get chunk if loaded
+ // Note: Partially copied from the getChunkAt method below
+ @Nullable
+ public LevelChunk getChunkAtIfCachedImmediately(int x, int z) {
+ long k = ChunkPos.asLong(x, z);
+
+ // Note: Bypass cache since we need to check ticket level, and to make this MT-Safe
+
+ ChunkHolder playerChunk = this.getVisibleChunkIfPresent(k);
+ if (playerChunk == null) {
+ return null;
+ }
+
+ return playerChunk.getFullChunkNowUnchecked();
+ }
+
+ @Nullable
+ public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) {
+ return this.fullChunks.get(ChunkPos.asLong(x, z));
+ }
+ // Paper end
@Override
public ThreadedLevelLightEngine getLightEngine() {
@@ -299,7 +354,7 @@ public class ServerChunkCache extends ChunkSource {
return this.mainThreadProcessor.pollTask();
}
- boolean runDistanceManagerUpdates() {
+ public boolean runDistanceManagerUpdates() { // Paper - public
boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
boolean flag1 = this.chunkMap.promoteChunkMap();
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index f6a3606b972064c4ec78487374e6197c0c447e27..8978fa74ceae06bef6aad3b74d6544989c687c69 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -239,6 +239,103 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
return this.convertable.dimensionType;
}
+ // Paper start
+ public final boolean areChunksLoadedForMove(AABB axisalignedbb) {
+ // copied code from collision methods, so that we can guarantee that they wont load chunks (we don't override
+ // ICollisionAccess methods for VoxelShapes)
+ // be more strict too, add a block (dumb plugins in move events?)
+ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
+ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
+
+ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
+ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
+
+ int minChunkX = minBlockX >> 4;
+ int maxChunkX = maxBlockX >> 4;
+
+ int minChunkZ = minBlockZ >> 4;
+ int maxChunkZ = maxBlockZ >> 4;
+
+ ServerChunkCache chunkProvider = this.getChunkSource();
+
+ for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
+ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
+ if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.util.Priority priority,
+ java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+ if (Thread.currentThread() != this.thread) {
+ this.getChunkSource().mainThreadProcessor.execute(() -> {
+ this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad);
+ });
+ return;
+ }
+ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
+ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
+
+ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
+ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
+
+ int minChunkX = minBlockX >> 4;
+ int minChunkZ = minBlockZ >> 4;
+
+ int maxChunkX = maxBlockX >> 4;
+ int maxChunkZ = maxBlockZ >> 4;
+
+ this.loadChunks(minChunkX, minChunkZ, maxChunkX, maxChunkZ, priority, onLoad);
+ }
+
+ public final void loadChunks(int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ,
+ ca.spottedleaf.concurrentutil.util.Priority priority,
+ java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+ List<net.minecraft.world.level.chunk.ChunkAccess> ret = new java.util.ArrayList<>();
+ it.unimi.dsi.fastutil.ints.IntArrayList ticketLevels = new it.unimi.dsi.fastutil.ints.IntArrayList();
+ ServerChunkCache chunkProvider = this.getChunkSource();
+
+ int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
+ int[] loadedChunks = new int[1];
+
+ Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++);
+
+ java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> {
+ if (chunk != null) {
+ int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel());
+ ret.add(chunk);
+ ticketLevels.add(ticketLevel);
+ chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier);
+ }
+ if (++loadedChunks[0] == requiredChunks) {
+ try {
+ onLoad.accept(java.util.Collections.unmodifiableList(ret));
+ } finally {
+ for (int i = 0, len = ret.size(); i < len; ++i) {
+ ChunkPos chunkPos = ret.get(i).getPos();
+ int ticketLevel = ticketLevels.getInt(i);
+
+ chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos);
+ chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier);
+ }
+ }
+ }
+ };
+
+ for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
+ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
+ this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer
+ );
+ }
+ }
+ }
+ // Paper end
+
// Add env and gen to constructor, IWorldDataServer -> WorldDataServer
public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess(), iworlddataserver.getGameRules()))); // Paper - create paper world configs
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
index fd97a0e54ad8487b0c6f242fcb626f0b76f88274..785c7e11f92610be58b624d252d1858658496af7 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -309,6 +309,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
public boolean sentListPacket = false;
public String kickLeaveMessage = null; // SPIGOT-3034: Forward leave message to PlayerQuitEvent
// CraftBukkit end
+ public boolean isRealPlayer; // Paper
public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);
diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
index 011eeab289d68b9b535c6438016f1244d0666286..e701f57ac8b0efdf739389f9be7a255220bb3e21 100644
--- a/src/main/java/net/minecraft/server/level/TicketType.java
+++ b/src/main/java/net/minecraft/server/level/TicketType.java
@@ -7,6 +7,7 @@ import net.minecraft.util.Unit;
import net.minecraft.world.level.ChunkPos;
public class TicketType<T> {
+ public static final TicketType<Long> FUTURE_AWAIT = create("future_await", Long::compareTo); // Paper
private final String name;
private final Comparator<T> comparator;
diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
index a1f4ebcd0877a6d0c41493eff5d70a408bf98e59..f1725ef766c35aa623ace58fe8bf31fc9b2bb6b3 100644
--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java
+++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
@@ -169,6 +169,26 @@ public class WorldGenRegion implements WorldGenLevel {
return k < this.generatingStep.directDependencies().size();
}
+ // Paper start - if loaded util
+ @Nullable
+ @Override
+ public ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
+ return this.getChunk(x, z, ChunkStatus.FULL, false);
+ }
+
+ @Override
+ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
+ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+ return chunk == null ? null : chunk.getBlockState(blockposition);
+ }
+
+ @Override
+ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+ return chunk == null ? null : chunk.getFluidState(blockposition);
+ }
+ // Paper end
+
@Override
public BlockState getBlockState(BlockPos pos) {
return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())).getBlockState(pos);
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
index d7095aab94a9f3b9f06a033ba4d414dfae42f620..7bb87d2bdf0ead0fdca38a9685e2e15b249ec2cb 100644
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
@@ -181,6 +181,7 @@ public abstract class PlayerList {
}
public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData) {
+ player.isRealPlayer = true; // Paper
GameProfile gameprofile = player.getGameProfile();
GameProfileCache usercache = this.server.getProfileCache();
// Optional optional; // CraftBukkit - decompile error
diff --git a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
index ae25aec117a7272735c824a00c1ed117fa52a921..d6e942aca1bcc769c390504a4119d6619872c4d4 100644
--- a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
+++ b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
@@ -82,6 +82,13 @@ public abstract class BlockableEventLoop<R extends Runnable> implements Profiler
runnable.run();
}
}
+ // Paper start
+ public void scheduleOnMain(Runnable runnable) {
+ // postToMainThread does not work the same as older versions of mc
+ // This method is actually used to create a TickTask, which can then be posted onto main
+ this.schedule(this.wrapRunnable(runnable));
+ }
+ // Paper end
@Override
public void schedule(R runnable) {
diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
index cf08a224c68b35a005a8ae0a093d2c8f4324bacd..d4bf025ffcc485b22db8093f1e9da1edfaae694b 100644
--- a/src/main/java/net/minecraft/world/entity/Entity.java
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
@@ -343,6 +343,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
return this.level.hasChunk((int) Math.floor(this.getX()) >> 4, (int) Math.floor(this.getZ()) >> 4);
}
// CraftBukkit end
+ // Paper start
+ public final AABB getBoundingBoxAt(double x, double y, double z) {
+ return this.dimensions.makeBoundingBox(x, y, z);
+ }
+ // Paper end
public Entity(EntityType<?> type, Level world) {
this.id = Entity.ENTITY_COUNTER.incrementAndGet();
diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
index 17f706d401154035a7f1ed47b04a38e4eef24263..1d7af970b8f5eaa46fe4cd2b7d076f3cdd371216 100644
--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
@@ -296,6 +296,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
public boolean collides = true;
public Set<UUID> collidableExemptions = new HashSet<>();
public boolean bukkitPickUpLoot;
+ public org.bukkit.craftbukkit.entity.CraftLivingEntity getBukkitLivingEntity() { return (org.bukkit.craftbukkit.entity.CraftLivingEntity) super.getBukkitEntity(); } // Paper
@Override
public float getBukkitYaw() {
diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java
index 0495b02157a40742ebf6817841010aacea179806..b0e45e28d17780b760a81ccbff61581274e033df 100644
--- a/src/main/java/net/minecraft/world/item/ItemStack.java
+++ b/src/main/java/net/minecraft/world/item/ItemStack.java
@@ -1025,6 +1025,25 @@ public final class ItemStack implements DataComponentHolder {
}
}
+ // Paper start - (this is just a good no conflict location)
+ public org.bukkit.inventory.ItemStack asBukkitMirror() {
+ return CraftItemStack.asCraftMirror(this);
+ }
+ public org.bukkit.inventory.ItemStack asBukkitCopy() {
+ return CraftItemStack.asCraftMirror(this.copy());
+ }
+ public static ItemStack fromBukkitCopy(org.bukkit.inventory.ItemStack itemstack) {
+ return CraftItemStack.asNMSCopy(itemstack);
+ }
+ private org.bukkit.craftbukkit.inventory.CraftItemStack bukkitStack;
+ public org.bukkit.inventory.ItemStack getBukkitStack() {
+ if (bukkitStack == null || bukkitStack.handle != this) {
+ bukkitStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this);
+ }
+ return bukkitStack;
+ }
+ // Paper end
+
public void applyComponents(DataComponentPatch changes) {
this.components.applyPatch(changes);
this.getItem().verifyComponentsAfterLoad(this);
@@ -1300,6 +1319,7 @@ public final class ItemStack implements DataComponentHolder {
// CraftBukkit start
@Deprecated
public void setItem(Item item) {
+ this.bukkitStack = null; // Paper
this.item = item;
}
// CraftBukkit end
diff --git a/src/main/java/net/minecraft/world/level/BlockGetter.java b/src/main/java/net/minecraft/world/level/BlockGetter.java
index 1a69fa49d43c904d0f762159129b8ed1eea36a43..6850ac324ee4d202f112dbd057ea1bde9de17ea9 100644
--- a/src/main/java/net/minecraft/world/level/BlockGetter.java
+++ b/src/main/java/net/minecraft/world/level/BlockGetter.java
@@ -12,6 +12,7 @@ import javax.annotation.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.Mth;
+import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
@@ -35,6 +36,15 @@ public interface BlockGetter extends LevelHeightAccessor {
}
BlockState getBlockState(BlockPos pos);
+ // Paper start - if loaded util
+ @Nullable BlockState getBlockStateIfLoaded(BlockPos blockposition);
+
+ default @Nullable Block getBlockIfLoaded(BlockPos blockposition) {
+ BlockState type = this.getBlockStateIfLoaded(blockposition);
+ return type == null ? null : type.getBlock();
+ }
+ @Nullable FluidState getFluidIfLoaded(BlockPos blockposition);
+ // Paper end
FluidState getFluidState(BlockPos pos);
diff --git a/src/main/java/net/minecraft/world/level/ChunkPos.java b/src/main/java/net/minecraft/world/level/ChunkPos.java
index a59d3f56859ba7c07dcb45b1e199e3a7132dcff7..0639e4565c3324d757dec1226adb4e99d841f2c0 100644
--- a/src/main/java/net/minecraft/world/level/ChunkPos.java
+++ b/src/main/java/net/minecraft/world/level/ChunkPos.java
@@ -46,6 +46,7 @@ public class ChunkPos {
public static final int REGION_MAX_INDEX = 31;
public final int x;
public final int z;
+ public final long longKey; // Paper
private static final int HASH_A = 1664525;
private static final int HASH_C = 1013904223;
private static final int HASH_Z_XOR = -559038737;
@@ -53,16 +54,19 @@ public class ChunkPos {
public ChunkPos(int x, int z) {
this.x = x;
this.z = z;
+ this.longKey = asLong(this.x, this.z); // Paper
}
public ChunkPos(BlockPos pos) {
this.x = SectionPos.blockToSectionCoord(pos.getX());
this.z = SectionPos.blockToSectionCoord(pos.getZ());
+ this.longKey = asLong(this.x, this.z); // Paper
}
public ChunkPos(long pos) {
this.x = (int)pos;
this.z = (int)(pos >> 32);
+ this.longKey = asLong(this.x, this.z); // Paper
}
public static ChunkPos minFromRegion(int x, int z) {
@@ -74,7 +78,7 @@ public class ChunkPos {
}
public long toLong() {
- return asLong(this.x, this.z);
+ return longKey; // Paper
}
public static long asLong(int chunkX, int chunkZ) {
diff --git a/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java b/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java
index 8741264b0a9529b40c41a4b9bf25b50d27c830bc..87af0b3cfd3a3170955894028fd667f108ea8121 100644
--- a/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java
+++ b/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java
@@ -17,6 +17,18 @@ public enum EmptyBlockGetter implements BlockGetter {
return null;
}
+ // Paper start - If loaded util
+ @Override
+ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+ return Fluids.EMPTY.defaultFluidState();
+ }
+
+ @Override
+ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
+ return Blocks.AIR.defaultBlockState();
+ }
+ // Paper end
+
@Override
public BlockState getBlockState(BlockPos pos) {
return Blocks.AIR.defaultBlockState();
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
index c8e49c1904c80c4ede40ca5c26efad9b12e247d1..3fb17bbcecf6dc4af3b231835adff25f86e1379f 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
@@ -97,6 +97,7 @@ import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.SpigotTimings; // Spigot
import org.bukkit.craftbukkit.block.CapturedBlockState;
+import org.bukkit.craftbukkit.block.CraftBlockState;
import org.bukkit.craftbukkit.block.data.CraftBlockData;
import org.bukkit.craftbukkit.util.CraftSpawnCategory;
import org.bukkit.entity.SpawnCategory;
@@ -274,6 +275,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
return null;
}
+ // Paper start
+ public net.minecraft.world.phys.BlockHitResult.Type clipDirect(Vec3 start, Vec3 end, net.minecraft.world.phys.shapes.CollisionContext context) {
+ // To be patched over
+ return this.clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, context)).getType();
+ }
+ // Paper end
+
public boolean isInWorldBounds(BlockPos pos) {
return !this.isOutsideBuildHeight(pos) && Level.isInWorldBoundsHorizontal(pos);
}
@@ -290,18 +298,52 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
return y < -20000000 || y >= 20000000;
}
- public LevelChunk getChunkAt(BlockPos pos) {
+ public final LevelChunk getChunkAt(BlockPos pos) { // Paper - help inline
return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
}
@Override
- public LevelChunk getChunk(int chunkX, int chunkZ) {
- return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL);
+ public final LevelChunk getChunk(int chunkX, int chunkZ) { // Paper - final to help inline
+ return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); // Paper - avoid a method jump
}
+ // Paper start - if loaded
+ @Nullable
+ @Override
+ public final ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
+ return ((ServerLevel)this).chunkSource.getChunkAtIfLoadedImmediately(x, z);
+ }
+
+ @Override
@Nullable
+ public final BlockState getBlockStateIfLoaded(BlockPos pos) {
+ // CraftBukkit start - tree generation
+ if (this.captureTreeGeneration) {
+ CraftBlockState previous = this.capturedBlockStates.get(pos);
+ if (previous != null) {
+ return previous.getHandle();
+ }
+ }
+ // CraftBukkit end
+ if (this.isOutsideBuildHeight(pos)) {
+ return Blocks.VOID_AIR.defaultBlockState();
+ } else {
+ ChunkAccess chunk = this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);
+
+ return chunk == null ? null : chunk.getBlockState(pos);
+ }
+ }
+
+ @Override
+ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+
+ return chunk == null ? null : chunk.getFluidState(blockposition);
+ }
+
@Override
public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
+ // Paper end
ChunkAccess ichunkaccess = this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, create);
if (ichunkaccess == null && create) {
@@ -553,7 +595,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
if (this.isOutsideBuildHeight(pos)) {
return Blocks.VOID_AIR.defaultBlockState();
} else {
- LevelChunk chunk = this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
+ ChunkAccess chunk = this.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.FULL, true); // Paper - manually inline to reduce hops and avoid unnecessary null check to reduce total byte code size, this should never return null and if it does we will see it the next line but the real stack trace will matter in the chunk engine
return chunk.getBlockState(pos);
}
diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java
index 35b338fed1df844846ecc876cbbab19e0efbfcb0..5eb8982678110fabb82a93c5ec67c666b7fde017 100644
--- a/src/main/java/net/minecraft/world/level/LevelReader.java
+++ b/src/main/java/net/minecraft/world/level/LevelReader.java
@@ -26,6 +26,9 @@ public interface LevelReader extends BlockAndTintGetter, CollisionGetter, Signal
@Nullable
ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create);
+ @Nullable ChunkAccess getChunkIfLoadedImmediately(int x, int z); // Paper - ifLoaded api (we need this since current impl blocks if the chunk is loading)
+ @Nullable default ChunkAccess getChunkIfLoadedImmediately(BlockPos pos) { return this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);}
+
@Deprecated
boolean hasChunk(int chunkX, int chunkZ);
diff --git a/src/main/java/net/minecraft/world/level/PathNavigationRegion.java b/src/main/java/net/minecraft/world/level/PathNavigationRegion.java
index b682c7c51204f727e21bfc20c691837deb7137ba..5375ea8dee8f74c843964824ad1a374e15711b60 100644
--- a/src/main/java/net/minecraft/world/level/PathNavigationRegion.java
+++ b/src/main/java/net/minecraft/world/level/PathNavigationRegion.java
@@ -8,6 +8,7 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.Registries;
+import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.Biomes;
@@ -66,7 +67,7 @@ public class PathNavigationRegion implements CollisionGetter {
private ChunkAccess getChunk(int chunkX, int chunkZ) {
int i = chunkX - this.centerX;
int j = chunkZ - this.centerZ;
- if (i >= 0 && i < this.chunks.length && j >= 0 && j < this.chunks[i].length) {
+ if (i >= 0 && i < this.chunks.length && j >= 0 && j < this.chunks[i].length) { // Paper - if this changes, update getChunkIfLoaded below
ChunkAccess chunkAccess = this.chunks[i][j];
return (ChunkAccess)(chunkAccess != null ? chunkAccess : new EmptyLevelChunk(this.level, new ChunkPos(chunkX, chunkZ), this.plains.get()));
} else {
@@ -74,6 +75,30 @@ public class PathNavigationRegion implements CollisionGetter {
}
}
+ // Paper start - if loaded util
+ private @Nullable ChunkAccess getChunkIfLoaded(int x, int z) {
+ // Based on getChunk(int, int)
+ int xx = x - this.centerX;
+ int zz = z - this.centerZ;
+
+ if (xx >= 0 && xx < this.chunks.length && zz >= 0 && zz < this.chunks[xx].length) {
+ return this.chunks[xx][zz];
+ }
+ return null;
+ }
+ @Override
+ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+ ChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+ return chunk == null ? null : chunk.getFluidState(blockposition);
+ }
+
+ @Override
+ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
+ ChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+ return chunk == null ? null : chunk.getBlockState(blockposition);
+ }
+ // Paper end
+
@Override
public WorldBorder getWorldBorder() {
return this.level.getWorldBorder();
diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
index 107d387f0f81e1a1400cefbec4758b743a64ca3b..0bae4e8d1e9fcc4608b3ef1c981c65f3b03de22b 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
@@ -883,12 +883,14 @@ public abstract class BlockBehaviour implements FeatureElement {
}
}
+ protected boolean shapeExceedsCube = true; // Paper - moved from actual method to here
public void initCache() {
this.fluidState = ((Block) this.owner).getFluidState(this.asState());
this.isRandomlyTicking = ((Block) this.owner).isRandomlyTicking(this.asState());
if (!this.getBlock().hasDynamicShape()) {
this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState());
}
+ this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here
this.legacySolid = this.calculateSolid();
this.occlusionShape = this.canOcclude ? ((Block) this.owner).getOcclusionShape(this.asState()) : Shapes.empty();
@@ -955,8 +957,8 @@ public abstract class BlockBehaviour implements FeatureElement {
return this.occlusionShape;
}
- public boolean hasLargeCollisionShape() {
- return this.cache == null || this.cache.largeCollisionShape;
+ public final boolean hasLargeCollisionShape() { // Paper
+ return this.shapeExceedsCube; // Paper - moved into shape cache init
}
public boolean useShapeForLightOcclusion() {
diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
index 15e3b8c8a850039d0adda3c345bd5b9288d06299..37795b9e264c571efe9c718fa9996197dca4ed54 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
@@ -65,7 +65,7 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
protected final ShortList[] postProcessing;
private volatile boolean unsaved;
private volatile boolean isLightCorrect;
- protected final ChunkPos chunkPos;
+ protected final ChunkPos chunkPos; public final long coordinateKey; public final int locX; public final int locZ; // Paper - cache coordinate key
private long inhabitedTime;
/** @deprecated */
@Nullable
@@ -91,7 +91,8 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh
// CraftBukkit end
public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry<Biome> biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) {
- this.chunkPos = pos;
+ this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups
+ this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key
this.upgradeData = upgradeData;
this.levelHeightAccessor = heightLimitView;
this.sections = new LevelChunkSection[heightLimitView.getSectionsCount()];
diff --git a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
index a52077f0d93c94b0ea644bc14b9b28e84fd1b154..dcc0acd259920463a4464213b9a5e793603852f9 100644
--- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
@@ -25,6 +25,12 @@ public class EmptyLevelChunk extends LevelChunk {
public BlockState getBlockState(BlockPos pos) {
return Blocks.VOID_AIR.defaultBlockState();
}
+ // Paper start
+ @Override
+ public BlockState getBlockState(final int x, final int y, final int z) {
+ return Blocks.VOID_AIR.defaultBlockState();
+ }
+ // Paper end
@Nullable
@Override
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
index 7252096639078cd0b16997354530bf181a61e999..d76591694c3b167b8b8f17b61a373a43140a8b68 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
@@ -120,6 +120,10 @@ public class LevelChunk extends ChunkAccess {
public boolean needsDecoration;
// CraftBukkit end
+ // Paper start
+ boolean loadedTicketLevel;
+ // Paper end
+
public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) {
this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData());
Iterator iterator = protoChunk.getBlockEntities().values().iterator();
@@ -204,8 +208,25 @@ public class LevelChunk extends ChunkAccess {
}
}
+ // Paper start - Perf: Reduce instructions and provide final method
+ public BlockState getBlockState(final int x, final int y, final int z) {
+ return this.getBlockStateFinal(x, y, z);
+ }
+ public BlockState getBlockStateFinal(final int x, final int y, final int z) {
+ // Copied and modified from below
+ final int sectionIndex = this.getSectionIndex(y);
+ if (sectionIndex < 0 || sectionIndex >= this.sections.length
+ || this.sections[sectionIndex].nonEmptyBlockCount == 0) {
+ return Blocks.AIR.defaultBlockState();
+ }
+ return this.sections[sectionIndex].states.get((y & 15) << 8 | (z & 15) << 4 | x & 15);
+ }
@Override
public BlockState getBlockState(BlockPos pos) {
+ if (true) {
+ return this.getBlockStateFinal(pos.getX(), pos.getY(), pos.getZ());
+ }
+ // Paper end - Perf: Reduce instructions and provide final method
int i = pos.getX();
int j = pos.getY();
int k = pos.getZ();
@@ -247,6 +268,18 @@ public class LevelChunk extends ChunkAccess {
}
}
+ // Paper start - If loaded util
+ @Override
+ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+ return this.getFluidState(blockposition);
+ }
+
+ @Override
+ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
+ return this.getBlockState(blockposition);
+ }
+ // Paper end
+
@Override
public FluidState getFluidState(BlockPos pos) {
return this.getFluidState(pos.getX(), pos.getY(), pos.getZ());
@@ -579,7 +612,11 @@ public class LevelChunk extends ChunkAccess {
// CraftBukkit start
public void loadCallback() {
+ // Paper start
+ this.loadedTicketLevel = true;
+ // Paper end
org.bukkit.Server server = this.level.getCraftServer();
+ this.level.getChunkSource().addLoadedChunk(this); // Paper
if (server != null) {
/*
* If it's a new world, the first few chunks are generated inside
@@ -620,6 +657,10 @@ public class LevelChunk extends ChunkAccess {
server.getPluginManager().callEvent(unloadEvent);
// note: saving can be prevented, but not forced if no saving is actually required
this.mustNotSave = !unloadEvent.isSaveChunk();
+ this.level.getChunkSource().removeLoadedChunk(this); // Paper
+ // Paper start
+ this.loadedTicketLevel = false;
+ // Paper end
}
@Override
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
index 0cf916776ce8735dcfa4c765b18e77037501a322..771529ba28a16664ad19ed9c0f4bf95eeb7da76b 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
@@ -19,7 +19,7 @@ public class LevelChunkSection {
public static final int SECTION_HEIGHT = 16;
public static final int SECTION_SIZE = 4096;
public static final int BIOME_CONTAINER_BITS = 2;
- private short nonEmptyBlockCount;
+ short nonEmptyBlockCount; // Paper - package private
private short tickingBlockCount;
private short tickingFluidCount;
public final PalettedContainer<BlockState> states;
diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
index 8bd5fb4971be46a51534c202e10a362723ad8664..5321109ca638036572df9a7e17eafcef2b4f5112 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
@@ -83,6 +83,18 @@ public class ProtoChunk extends ChunkAccess {
return new ChunkAccess.PackedTicks(this.blockTicks.pack(time), this.fluidTicks.pack(time));
}
+ // Paper start - If loaded util
+ @Override
+ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+ return this.getFluidState(blockposition);
+ }
+
+ @Override
+ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
+ return this.getBlockState(blockposition);
+ }
+ // Paper end
+
@Override
public BlockState getBlockState(BlockPos pos) {
int i = pos.getY();
diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
index 3f552ee8f90566edddb5943311a14309e4bebb61..412caefe776df6c8e931f6a1a48ea2525ec6610e 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
@@ -168,7 +168,7 @@ public class ChunkStatusTasks {
}, context.mainThreadExecutor());
}
- private static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities) {
+ public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> entities) { // Paper - public
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, EntitySpawnReason.LOAD).filter((entity) -> {
diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
index 34933c5324126f9afdc5cba9dea997ace8f01806..4eb0b0969325f39a7ae65492cccd482515a50142 100644
--- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
+++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
@@ -91,6 +91,16 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A
}
private boolean addEntity(T entity, boolean existing) {
+ // Paper start - chunk system hooks
+ // I don't want to know why this is a generic type.
+ Entity entityCasted = (Entity)entity;
+ boolean wasRemoved = entityCasted.isRemoved();
+ boolean screened = ca.spottedleaf.moonrise.common.util.ChunkSystem.screenEntity((net.minecraft.server.level.ServerLevel)entityCasted.level(), entityCasted, existing, true);
+ if ((!wasRemoved && entityCasted.isRemoved()) || !screened) {
+ // removed by callback
+ return false;
+ }
+ // Paper end - chunk system hooks
if (!this.addEntityUuid(entity)) {
return false;
} else {
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index d5451cb1976ca3675dd19b07bd8a2d363f82db86..e5054699f2f7555455b4da20249e253dba7043b4 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -2627,4 +2627,9 @@ public final class CraftServer implements Server {
return this.spigot;
}
// Spigot end
+
+ @Override
+ public double[] getTPS() {
+ return new double[]{0, 0, 0}; // TODO
+ }
}
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index 799444e4101283c972a160742a9e2548e604173f..fb8c641604473a3853b2f8b9cd5c8a65fd2482be 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -256,8 +256,8 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public Chunk[] getLoadedChunks() {
- Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = this.world.getChunkSource().chunkMap.visibleChunkMap;
- return chunks.values().stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(CraftChunk::new).toArray(Chunk[]::new);
+ List<ChunkHolder> chunks = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.world); // Paper
+ return chunks.stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(CraftChunk::new).toArray(Chunk[]::new);
}
@Override
@@ -334,7 +334,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public boolean refreshChunk(int x, int z) {
- ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.visibleChunkMap.get(ChunkPos.asLong(x, z));
+ ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
if (playerChunk == null) return false;
playerChunk.getTickingChunkFuture().thenAccept(either -> {
@@ -2110,4 +2110,51 @@ public class CraftWorld extends CraftRegionAccessor implements World {
return this.spigot;
}
// Spigot end
+ // Paper start
+ @Override
+ public void getChunkAtAsync(int x, int z, boolean gen, boolean urgent, @NotNull Consumer<? super Chunk> cb) {
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(
+ this.getHandle(), x, z, gen, ChunkStatus.FULL, true,
+ urgent ? ca.spottedleaf.concurrentutil.util.Priority.HIGHER : ca.spottedleaf.concurrentutil.util.Priority.NORMAL,
+ (ChunkAccess chunk) -> {
+ cb.accept(chunk == null ? null : new CraftChunk((net.minecraft.world.level.chunk.LevelChunk)chunk));
+ }
+ );
+
+ }
+
+ @Override
+ public void getChunksAtAsync(int minX, int minZ, int maxX, int maxZ, boolean urgent, Runnable cb) {
+ this.getHandle().loadChunks(
+ minX, minZ, maxX, maxZ,
+ urgent ? ca.spottedleaf.concurrentutil.util.Priority.HIGHER : ca.spottedleaf.concurrentutil.util.Priority.NORMAL,
+ (List<ChunkAccess> chunks) -> {
+ cb.run();
+ }
+ );
+ }
+
+ @Override
+ public void setViewDistance(final int viewDistance) {
+ if (viewDistance < 2 || viewDistance > 32) {
+ throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]");
+ }
+ this.getHandle().chunkSource.chunkMap.setServerViewDistance(viewDistance);
+ }
+
+ @Override
+ public void setSimulationDistance(final int simulationDistance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public int getSendViewDistance() {
+ return this.getViewDistance();
+ }
+
+ @Override
+ public void setSendViewDistance(final int viewDistance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+ // Paper end
}
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
index 031eed24638659f2633bef0ad2e178832ca058e9..a3550d84c273c9720491484382a4bc50dc3246a6 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -2447,4 +2447,34 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
return this.spigot;
}
// Spigot end
+
+ @Override
+ public int getViewDistance() {
+ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getLoadViewDistance(this.getHandle()) - 1;
+ }
+
+ @Override
+ public void setViewDistance(final int viewDistance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public int getSimulationDistance() {
+ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getTickViewDistance(this.getHandle());
+ }
+
+ @Override
+ public void setSimulationDistance(final int simulationDistance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public int getSendViewDistance() {
+ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getSendViewDistance(this.getHandle());
+ }
+
+ @Override
+ public void setSendViewDistance(final int viewDistance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
}
diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
index f6e6f0ddef6693c58f28b89cf3df005a8d47e08d..101eea3452c9e387e770b716543c3a4f17b9a737 100644
--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
@@ -31,6 +31,20 @@ import org.jetbrains.annotations.ApiStatus;
@DelegateDeserialization(ItemStack.class)
public final class CraftItemStack extends ItemStack {
+ // Paper start - MC Utils
+ public static net.minecraft.world.item.ItemStack unwrap(ItemStack bukkit) {
+ if (bukkit instanceof CraftItemStack craftItemStack) {
+ return craftItemStack.handle != null ? craftItemStack.handle : net.minecraft.world.item.ItemStack.EMPTY;
+ } else {
+ return asNMSCopy(bukkit);
+ }
+ }
+
+ public static net.minecraft.world.item.ItemStack getOrCloneOnMutation(ItemStack old, ItemStack newInstance) {
+ return old == newInstance ? unwrap(old) : asNMSCopy(newInstance);
+ }
+ // Paper end - MC Utils
+
public static net.minecraft.world.item.ItemStack asNMSCopy(ItemStack original) {
if (original instanceof CraftItemStack) {
CraftItemStack stack = (CraftItemStack) original;
diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftLocation.java b/src/main/java/org/bukkit/craftbukkit/util/CraftLocation.java
index 097996d3955ab5126b71f7bff1dd2c62becb5ffd..a8b46ea5e4b6260c2728c67e8651b74fe6356605 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/CraftLocation.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftLocation.java
@@ -40,6 +40,17 @@ public final class CraftLocation {
return new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ());
}
+ // Paper start
+ public static net.minecraft.core.GlobalPos toGlobalPos(Location location) {
+ return net.minecraft.core.GlobalPos.of(((org.bukkit.craftbukkit.CraftWorld) location.getWorld()).getHandle().dimension(), toBlockPosition(location));
+ }
+
+ public static Location fromGlobalPos(net.minecraft.core.GlobalPos globalPos) {
+ BlockPos pos = globalPos.pos();
+ return new org.bukkit.Location(net.minecraft.server.MinecraftServer.getServer().getLevel(globalPos.dimension()).getWorld(), pos.getX(), pos.getY(), pos.getZ());
+ }
+ // Paper end
+
public static Vec3 toVec3D(Location location) {
return new Vec3(location.getX(), location.getY(), location.getZ());
}
diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
index b8a865305cc61954aeebff4a7cd1d1973c5f46ab..e444662ee4d9405eeea7caa41b9cd6b36586d840 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
@@ -56,6 +56,7 @@ import net.minecraft.world.phys.shapes.VoxelShape;
import net.minecraft.world.ticks.LevelTickAccess;
import net.minecraft.world.ticks.TickPriority;
import org.bukkit.event.entity.CreatureSpawnEvent;
+import org.jetbrains.annotations.Nullable;
public abstract class DelegatedGeneratorAccess implements WorldGenLevel {
@@ -788,4 +789,25 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel {
public boolean isFluidAtPosition(BlockPos pos, Predicate<FluidState> state) {
return this.handle.isFluidAtPosition(pos, state);
}
+
+ // Paper start
+ @Nullable
+ @Override
+ public BlockState getBlockStateIfLoaded(final BlockPos blockposition) {
+ return this.handle.getBlockStateIfLoaded(blockposition);
+ }
+
+ @Nullable
+ @Override
+ public FluidState getFluidIfLoaded(final BlockPos blockposition) {
+ return this.handle.getFluidIfLoaded(blockposition);
+ }
+
+ @Nullable
+ @Override
+ public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) {
+ return this.handle.getChunkIfLoadedImmediately(x, z);
+ }
+ // Paper end
}
+
diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
index e837d76e833d73d888bc1dad3515c2b82bc0e437..4705aed1dd98378c146bf9e346df1a17f719ad36 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
@@ -212,7 +212,23 @@ public class DummyGeneratorAccess implements WorldGenLevel {
public FluidState getFluidState(BlockPos pos) {
return Fluids.EMPTY.defaultFluidState(); // SPIGOT-6634
}
+ // Paper start - if loaded util
+ @javax.annotation.Nullable
+ @Override
+ public ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ @Override
+ public BlockState getBlockStateIfLoaded(BlockPos blockposition) {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+ @Override
+ public FluidState getFluidIfLoaded(BlockPos blockposition) {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+ // Paper end
@Override
public WorldBorder getWorldBorder() {
throw new UnsupportedOperationException("Not supported yet.");
diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java
index fb83cadf384712e2dff1cb001bdbeec8f5e89ada..5baf68732cb0e5ecab9d809df54a42e8252f1624 100644
--- a/src/main/java/org/spigotmc/ActivationRange.java
+++ b/src/main/java/org/spigotmc/ActivationRange.java
@@ -35,6 +35,9 @@ public class ActivationRange
public enum ActivationType
{
+ WATER, // Paper
+ FLYING_MONSTER, // Paper
+ VILLAGER, // Paper
MONSTER,
ANIMAL,
RAIDER,
diff --git a/src/main/resources/META-INF/services/ca.spottedleaf.moonrise.common.PlatformHooks b/src/main/resources/META-INF/services/ca.spottedleaf.moonrise.common.PlatformHooks
new file mode 100644
index 0000000000000000000000000000000000000000..e57c3ca79677b1dfe7cf3db36f0406de7ea5bd0a
--- /dev/null
+++ b/src/main/resources/META-INF/services/ca.spottedleaf.moonrise.common.PlatformHooks
@@ -0,0 +1 @@
+ca.spottedleaf.moonrise.paper.PaperHooks