aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0009-MC-Utils.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/server/0009-MC-Utils.patch')
-rw-r--r--patches/server/0009-MC-Utils.patch2347
1 files changed, 77 insertions, 2270 deletions
diff --git a/patches/server/0009-MC-Utils.patch b/patches/server/0009-MC-Utils.patch
index 1d50fab8f5..c12a8e7c5c 100644
--- a/patches/server/0009-MC-Utils.patch
+++ b/patches/server/0009-MC-Utils.patch
@@ -12,456 +12,6 @@ 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/com/destroystokyo/paper/util/concurrent/WeakSeqLock.java b/src/main/java/com/destroystokyo/paper/util/concurrent/WeakSeqLock.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..4029dc68cf35d63aa70c4a76c35bf65a7fc6358f
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/concurrent/WeakSeqLock.java
-@@ -0,0 +1,68 @@
-+package com.destroystokyo.paper.util.concurrent;
-+
-+import java.util.concurrent.atomic.AtomicLong;
-+
-+/**
-+ * copied from https://github.com/Spottedleaf/ConcurrentUtil/blob/master/src/main/java/ca/spottedleaf/concurrentutil/lock/WeakSeqLock.java
-+ * @author Spottedleaf
-+ */
-+public final class WeakSeqLock {
-+ // TODO when the switch to J11 is made, nuke this class from orbit
-+
-+ protected final AtomicLong lock = new AtomicLong();
-+
-+ public WeakSeqLock() {
-+ //VarHandle.storeStoreFence(); // warn: usages must be checked to ensure this behaviour isn't needed
-+ }
-+
-+ public void acquireWrite() {
-+ // must be release-type write
-+ this.lock.lazySet(this.lock.get() + 1);
-+ }
-+
-+ public boolean canRead(final long read) {
-+ return (read & 1) == 0;
-+ }
-+
-+ public boolean tryAcquireWrite() {
-+ this.acquireWrite();
-+ return true;
-+ }
-+
-+ public void releaseWrite() {
-+ // must be acquire-type write
-+ final long lock = this.lock.get(); // volatile here acts as store-store
-+ this.lock.lazySet(lock + 1);
-+ }
-+
-+ public void abortWrite() {
-+ // must be acquire-type write
-+ final long lock = this.lock.get(); // volatile here acts as store-store
-+ this.lock.lazySet(lock ^ 1);
-+ }
-+
-+ public long acquireRead() {
-+ int failures = 0;
-+ long curr;
-+
-+ for (curr = this.lock.get(); !this.canRead(curr); curr = this.lock.get()) {
-+ // without j11, our only backoff is the yield() call...
-+
-+ if (++failures > 5_000) { /* TODO determine a threshold */
-+ Thread.yield();
-+ }
-+ /* Better waiting is beyond the scope of this lock; if it is needed the lock is being misused */
-+ }
-+
-+ //VarHandle.loadLoadFence(); // volatile acts as the load-load barrier
-+ return curr;
-+ }
-+
-+ public boolean tryReleaseRead(final long read) {
-+ return this.lock.get() == read; // volatile acts as the load-load barrier
-+ }
-+
-+ public long getSequentialCounter() {
-+ return this.lock.get();
-+ }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Int.java b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Int.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..59868f37d14bbc0ece0836095cdad148778995e6
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Int.java
-@@ -0,0 +1,162 @@
-+package com.destroystokyo.paper.util.map;
-+
-+import com.destroystokyo.paper.util.concurrent.WeakSeqLock;
-+import it.unimi.dsi.fastutil.longs.Long2IntMap;
-+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.LongIterator;
-+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
-+import it.unimi.dsi.fastutil.objects.ObjectIterator;
-+
-+/**
-+ * @author Spottedleaf
-+ */
-+public class QueuedChangesMapLong2Int {
-+
-+ protected final Long2IntOpenHashMap updatingMap;
-+ protected final Long2IntOpenHashMap visibleMap;
-+ protected final Long2IntOpenHashMap queuedPuts;
-+ protected final LongOpenHashSet queuedRemove;
-+
-+ protected int queuedDefaultReturnValue;
-+
-+ // we use a seqlock as writes are not common.
-+ protected final WeakSeqLock updatingMapSeqLock = new WeakSeqLock();
-+
-+ public QueuedChangesMapLong2Int() {
-+ this(16, 0.75f);
-+ }
-+
-+ public QueuedChangesMapLong2Int(final int capacity, final float loadFactor) {
-+ this.updatingMap = new Long2IntOpenHashMap(capacity, loadFactor);
-+ this.visibleMap = new Long2IntOpenHashMap(capacity, loadFactor);
-+ this.queuedPuts = new Long2IntOpenHashMap();
-+ this.queuedRemove = new LongOpenHashSet();
-+ }
-+
-+ public void queueDefaultReturnValue(final int dfl) {
-+ this.queuedDefaultReturnValue = dfl;
-+ this.updatingMap.defaultReturnValue(dfl);
-+ }
-+
-+ public int queueUpdate(final long k, final int v) {
-+ this.queuedRemove.remove(k);
-+ this.queuedPuts.put(k, v);
-+
-+ return this.updatingMap.put(k, v);
-+ }
-+
-+ public int queueRemove(final long k) {
-+ this.queuedPuts.remove(k);
-+ this.queuedRemove.add(k);
-+
-+ return this.updatingMap.remove(k);
-+ }
-+
-+ public int getUpdating(final long k) {
-+ return this.updatingMap.get(k);
-+ }
-+
-+ public int getVisible(final long k) {
-+ return this.visibleMap.get(k);
-+ }
-+
-+ public int getVisibleAsync(final long k) {
-+ long readlock;
-+ int ret = 0;
-+
-+ do {
-+ readlock = this.updatingMapSeqLock.acquireRead();
-+ try {
-+ ret = this.visibleMap.get(k);
-+ } catch (final Throwable thr) {
-+ if (thr instanceof ThreadDeath) {
-+ throw (ThreadDeath)thr;
-+ }
-+ // ignore...
-+ continue;
-+ }
-+
-+ } while (!this.updatingMapSeqLock.tryReleaseRead(readlock));
-+
-+ return ret;
-+ }
-+
-+ public boolean performUpdates() {
-+ this.updatingMapSeqLock.acquireWrite();
-+ this.visibleMap.defaultReturnValue(this.queuedDefaultReturnValue);
-+ this.updatingMapSeqLock.releaseWrite();
-+
-+ if (this.queuedPuts.isEmpty() && this.queuedRemove.isEmpty()) {
-+ return false;
-+ }
-+
-+ // update puts
-+ final ObjectIterator<Long2IntMap.Entry> iterator0 = this.queuedPuts.long2IntEntrySet().fastIterator();
-+ while (iterator0.hasNext()) {
-+ final Long2IntMap.Entry entry = iterator0.next();
-+ final long key = entry.getLongKey();
-+ final int val = entry.getIntValue();
-+
-+ this.updatingMapSeqLock.acquireWrite();
-+ try {
-+ this.visibleMap.put(key, val);
-+ } finally {
-+ this.updatingMapSeqLock.releaseWrite();
-+ }
-+ }
-+
-+ this.queuedPuts.clear();
-+
-+ final LongIterator iterator1 = this.queuedRemove.iterator();
-+ while (iterator1.hasNext()) {
-+ final long key = iterator1.nextLong();
-+
-+ this.updatingMapSeqLock.acquireWrite();
-+ try {
-+ this.visibleMap.remove(key);
-+ } finally {
-+ this.updatingMapSeqLock.releaseWrite();
-+ }
-+ }
-+
-+ this.queuedRemove.clear();
-+
-+ return true;
-+ }
-+
-+ public boolean performUpdatesLockMap() {
-+ this.updatingMapSeqLock.acquireWrite();
-+ try {
-+ this.visibleMap.defaultReturnValue(this.queuedDefaultReturnValue);
-+
-+ if (this.queuedPuts.isEmpty() && this.queuedRemove.isEmpty()) {
-+ return false;
-+ }
-+
-+ // update puts
-+ final ObjectIterator<Long2IntMap.Entry> iterator0 = this.queuedPuts.long2IntEntrySet().fastIterator();
-+ while (iterator0.hasNext()) {
-+ final Long2IntMap.Entry entry = iterator0.next();
-+ final long key = entry.getLongKey();
-+ final int val = entry.getIntValue();
-+
-+ this.visibleMap.put(key, val);
-+ }
-+
-+ this.queuedPuts.clear();
-+
-+ final LongIterator iterator1 = this.queuedRemove.iterator();
-+ while (iterator1.hasNext()) {
-+ final long key = iterator1.nextLong();
-+
-+ this.visibleMap.remove(key);
-+ }
-+
-+ this.queuedRemove.clear();
-+
-+ return true;
-+ } finally {
-+ this.updatingMapSeqLock.releaseWrite();
-+ }
-+ }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Object.java b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Object.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..7bab31a312463cc963d9621cdc543a281459bd32
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Object.java
-@@ -0,0 +1,202 @@
-+package com.destroystokyo.paper.util.map;
-+
-+import com.destroystokyo.paper.util.concurrent.WeakSeqLock;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
-+import it.unimi.dsi.fastutil.objects.ObjectBidirectionalIterator;
-+import java.util.ArrayList;
-+import java.util.Collection;
-+import java.util.List;
-+
-+/**
-+ * @author Spottedleaf
-+ */
-+public class QueuedChangesMapLong2Object<V> {
-+
-+ protected static final Object REMOVED = new Object();
-+
-+ protected final Long2ObjectLinkedOpenHashMap<V> updatingMap;
-+ protected final Long2ObjectLinkedOpenHashMap<V> visibleMap;
-+ protected final Long2ObjectLinkedOpenHashMap<Object> queuedChanges;
-+
-+ // we use a seqlock as writes are not common.
-+ protected final WeakSeqLock updatingMapSeqLock = new WeakSeqLock();
-+
-+ public QueuedChangesMapLong2Object() {
-+ this(16, 0.75f); // dfl for fastutil
-+ }
-+
-+ public QueuedChangesMapLong2Object(final int capacity, final float loadFactor) {
-+ this.updatingMap = new Long2ObjectLinkedOpenHashMap<>(capacity, loadFactor);
-+ this.visibleMap = new Long2ObjectLinkedOpenHashMap<>(capacity, loadFactor);
-+ this.queuedChanges = new Long2ObjectLinkedOpenHashMap<>();
-+ }
-+
-+ public V queueUpdate(final long k, final V value) {
-+ this.queuedChanges.put(k, value);
-+ return this.updatingMap.put(k, value);
-+ }
-+
-+ public V queueRemove(final long k) {
-+ this.queuedChanges.put(k, REMOVED);
-+ return this.updatingMap.remove(k);
-+ }
-+
-+ public V getUpdating(final long k) {
-+ return this.updatingMap.get(k);
-+ }
-+
-+ public boolean updatingContainsKey(final long k) {
-+ return this.updatingMap.containsKey(k);
-+ }
-+
-+ public V getVisible(final long k) {
-+ return this.visibleMap.get(k);
-+ }
-+
-+ public boolean visibleContainsKey(final long k) {
-+ return this.visibleMap.containsKey(k);
-+ }
-+
-+ public V getVisibleAsync(final long k) {
-+ long readlock;
-+ V ret = null;
-+
-+ do {
-+ readlock = this.updatingMapSeqLock.acquireRead();
-+
-+ try {
-+ ret = this.visibleMap.get(k);
-+ } catch (final Throwable thr) {
-+ if (thr instanceof ThreadDeath) {
-+ throw (ThreadDeath)thr;
-+ }
-+ // ignore...
-+ continue;
-+ }
-+
-+ } while (!this.updatingMapSeqLock.tryReleaseRead(readlock));
-+
-+ return ret;
-+ }
-+
-+ public boolean visibleContainsKeyAsync(final long k) {
-+ long readlock;
-+ boolean ret = false;
-+
-+ do {
-+ readlock = this.updatingMapSeqLock.acquireRead();
-+
-+ try {
-+ ret = this.visibleMap.containsKey(k);
-+ } catch (final Throwable thr) {
-+ if (thr instanceof ThreadDeath) {
-+ throw (ThreadDeath)thr;
-+ }
-+ // ignore...
-+ continue;
-+ }
-+
-+ } while (!this.updatingMapSeqLock.tryReleaseRead(readlock));
-+
-+ return ret;
-+ }
-+
-+ public Long2ObjectLinkedOpenHashMap<V> getVisibleMap() {
-+ return this.visibleMap;
-+ }
-+
-+ public Long2ObjectLinkedOpenHashMap<V> getUpdatingMap() {
-+ return this.updatingMap;
-+ }
-+
-+ public int getVisibleSize() {
-+ return this.visibleMap.size();
-+ }
-+
-+ public int getVisibleSizeAsync() {
-+ long readlock;
-+ int ret;
-+
-+ do {
-+ readlock = this.updatingMapSeqLock.acquireRead();
-+ ret = this.visibleMap.size();
-+ } while (!this.updatingMapSeqLock.tryReleaseRead(readlock));
-+
-+ return ret;
-+ }
-+
-+ // unlike mojang's impl this cannot be used async since it's not a view of an immutable map
-+ public Collection<V> getUpdatingValues() {
-+ return this.updatingMap.values();
-+ }
-+
-+ public List<V> getUpdatingValuesCopy() {
-+ return new ArrayList<>(this.updatingMap.values());
-+ }
-+
-+ // unlike mojang's impl this cannot be used async since it's not a view of an immutable map
-+ public Collection<V> getVisibleValues() {
-+ return this.visibleMap.values();
-+ }
-+
-+ public List<V> getVisibleValuesCopy() {
-+ return new ArrayList<>(this.visibleMap.values());
-+ }
-+
-+ public boolean performUpdates() {
-+ if (this.queuedChanges.isEmpty()) {
-+ return false;
-+ }
-+
-+ final ObjectBidirectionalIterator<Long2ObjectMap.Entry<Object>> iterator = this.queuedChanges.long2ObjectEntrySet().fastIterator();
-+ while (iterator.hasNext()) {
-+ final Long2ObjectMap.Entry<Object> entry = iterator.next();
-+ final long key = entry.getLongKey();
-+ final Object val = entry.getValue();
-+
-+ this.updatingMapSeqLock.acquireWrite();
-+ try {
-+ if (val == REMOVED) {
-+ this.visibleMap.remove(key);
-+ } else {
-+ this.visibleMap.put(key, (V)val);
-+ }
-+ } finally {
-+ this.updatingMapSeqLock.releaseWrite();
-+ }
-+ }
-+
-+ this.queuedChanges.clear();
-+ return true;
-+ }
-+
-+ public boolean performUpdatesLockMap() {
-+ if (this.queuedChanges.isEmpty()) {
-+ return false;
-+ }
-+
-+ final ObjectBidirectionalIterator<Long2ObjectMap.Entry<Object>> iterator = this.queuedChanges.long2ObjectEntrySet().fastIterator();
-+
-+ try {
-+ this.updatingMapSeqLock.acquireWrite();
-+
-+ while (iterator.hasNext()) {
-+ final Long2ObjectMap.Entry<Object> entry = iterator.next();
-+ final long key = entry.getLongKey();
-+ final Object val = entry.getValue();
-+
-+ if (val == REMOVED) {
-+ this.visibleMap.remove(key);
-+ } else {
-+ this.visibleMap.put(key, (V)val);
-+ }
-+ }
-+ } finally {
-+ this.updatingMapSeqLock.releaseWrite();
-+ }
-+
-+ this.queuedChanges.clear();
-+ return true;
-+ }
-+}
diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java b/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java
new file mode 100644
index 0000000000000000000000000000000000000000..554f4d4e63c1431721989e6f502a32ccc53a8807
@@ -2178,495 +1728,12 @@ index 46cab7a8c7b87ab01b26074b04f5a02b3907cfc4..49019b4a9bc4e634d54a9b0acaf9229a
+ }
+ // Paper end
}
-diff --git a/src/main/java/io/papermc/paper/chunk/SingleThreadChunkRegionManager.java b/src/main/java/io/papermc/paper/chunk/SingleThreadChunkRegionManager.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..a5f706d6f716b2a463ae58adcde69d9e665c7733
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/chunk/SingleThreadChunkRegionManager.java
-@@ -0,0 +1,477 @@
-+package io.papermc.paper.chunk;
-+
-+import io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
-+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
-+import io.papermc.paper.util.MCUtil;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.ChunkPos;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.function.Supplier;
-+
-+public final class SingleThreadChunkRegionManager {
-+
-+ protected final int regionSectionMergeRadius;
-+ protected final int regionSectionChunkSize;
-+ public final int regionChunkShift; // log2(REGION_CHUNK_SIZE)
-+
-+ public final ServerLevel world;
-+ public final String name;
-+
-+ protected final Long2ObjectOpenHashMap<RegionSection> regionsBySection = new Long2ObjectOpenHashMap<>();
-+ protected final ReferenceLinkedOpenHashSet<Region> needsRecalculation = new ReferenceLinkedOpenHashSet<>();
-+ protected final int minSectionRecalcCount;
-+ protected final double maxDeadRegionPercent;
-+ protected final Supplier<RegionData> regionDataSupplier;
-+ protected final Supplier<RegionSectionData> regionSectionDataSupplier;
-+
-+ public SingleThreadChunkRegionManager(final ServerLevel world, final int minSectionRecalcCount,
-+ final double maxDeadRegionPercent, final int sectionMergeRadius,
-+ final int regionSectionChunkShift,
-+ final String name, final Supplier<RegionData> regionDataSupplier,
-+ final Supplier<RegionSectionData> regionSectionDataSupplier) {
-+ this.regionSectionMergeRadius = sectionMergeRadius;
-+ this.regionSectionChunkSize = 1 << regionSectionChunkShift;
-+ this.regionChunkShift = regionSectionChunkShift;
-+ this.world = world;
-+ this.name = name;
-+ this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount);
-+ this.maxDeadRegionPercent = maxDeadRegionPercent;
-+ this.regionDataSupplier = regionDataSupplier;
-+ this.regionSectionDataSupplier = regionSectionDataSupplier;
-+ }
-+
-+ // tested via https://gist.github.com/Spottedleaf/aa7ade3451c37b4cac061fc77074db2f
-+
-+ /*
-+ protected void check() {
-+ ReferenceOpenHashSet<Region<T>> checked = new ReferenceOpenHashSet<>();
-+
-+ for (RegionSection<T> section : this.regionsBySection.values()) {
-+ if (!checked.add(section.region)) {
-+ section.region.check();
-+ }
-+ }
-+ for (Region<T> region : this.needsRecalculation) {
-+ region.check();
-+ }
-+ }
-+ */
-+
-+ protected void addToRecalcQueue(final Region region) {
-+ this.needsRecalculation.add(region);
-+ }
-+
-+ protected void removeFromRecalcQueue(final Region region) {
-+ this.needsRecalculation.remove(region);
-+ }
-+
-+ public RegionSection getRegionSection(final int chunkX, final int chunkZ) {
-+ return this.regionsBySection.get(MCUtil.getCoordinateKey(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift));
-+ }
-+
-+ public Region getRegion(final int chunkX, final int chunkZ) {
-+ final RegionSection section = this.regionsBySection.get(MCUtil.getCoordinateKey(chunkX >> regionChunkShift, chunkZ >> regionChunkShift));
-+ return section != null ? section.region : null;
-+ }
-+
-+ private final List<Region> toMerge = new ArrayList<>();
-+
-+ protected RegionSection getOrCreateAndMergeSection(final int sectionX, final int sectionZ, final RegionSection force) {
-+ final long sectionKey = MCUtil.getCoordinateKey(sectionX, sectionZ);
-+
-+ if (force == null) {
-+ RegionSection region = this.regionsBySection.get(sectionKey);
-+ if (region != null) {
-+ return region;
-+ }
-+ }
-+
-+ int mergeCandidateSectionSize = -1;
-+ Region mergeIntoCandidate = null;
-+
-+ // find optimal candidate to merge into
-+
-+ final int minX = sectionX - this.regionSectionMergeRadius;
-+ final int maxX = sectionX + this.regionSectionMergeRadius;
-+ final int minZ = sectionZ - this.regionSectionMergeRadius;
-+ final int maxZ = sectionZ + this.regionSectionMergeRadius;
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ final RegionSection section = this.regionsBySection.get(MCUtil.getCoordinateKey(currX, currZ));
-+ if (section == null) {
-+ continue;
-+ }
-+ final Region region = section.region;
-+ if (region.dead) {
-+ throw new IllegalStateException("Dead region should not be in live region manager state: " + region);
-+ }
-+ final int sections = region.sections.size();
-+
-+ if (sections > mergeCandidateSectionSize) {
-+ mergeCandidateSectionSize = sections;
-+ mergeIntoCandidate = region;
-+ }
-+ this.toMerge.add(region);
-+ }
-+ }
-+
-+ // merge
-+ if (mergeIntoCandidate != null) {
-+ for (int i = 0; i < this.toMerge.size(); ++i) {
-+ final Region region = this.toMerge.get(i);
-+ if (region.dead || mergeIntoCandidate == region) {
-+ continue;
-+ }
-+ region.mergeInto(mergeIntoCandidate);
-+ }
-+ this.toMerge.clear();
-+ } else {
-+ mergeIntoCandidate = new Region(this);
-+ }
-+
-+ final RegionSection section;
-+ if (force == null) {
-+ this.regionsBySection.put(sectionKey, section = new RegionSection(sectionKey, this));
-+ } else {
-+ final RegionSection existing = this.regionsBySection.putIfAbsent(sectionKey, force);
-+ if (existing != null) {
-+ throw new IllegalStateException("Attempting to override section '" + existing.toStringWithRegion() +
-+ ", with " + force.toStringWithRegion());
-+ }
-+
-+ section = force;
-+ }
-+
-+ mergeIntoCandidate.addRegionSection(section);
-+ //mergeIntoCandidate.check();
-+ //this.check();
-+
-+ return section;
-+ }
-+
-+ public void addChunk(final int chunkX, final int chunkZ) {
-+ this.getOrCreateAndMergeSection(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift, null).addChunk(chunkX, chunkZ);
-+ }
-+
-+ public void removeChunk(final int chunkX, final int chunkZ) {
-+ final RegionSection section = this.regionsBySection.get(
-+ MCUtil.getCoordinateKey(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift)
-+ );
-+ if (section != null) {
-+ section.removeChunk(chunkX, chunkZ);
-+ } else {
-+ throw new IllegalStateException("Cannot remove chunk at (" + chunkX + "," + chunkZ + ") from region state, section does not exist");
-+ }
-+ }
-+
-+ public void recalculateRegions() {
-+ for (int i = 0, len = this.needsRecalculation.size(); i < len; ++i) {
-+ final Region region = this.needsRecalculation.removeFirst();
-+
-+ this.recalculateRegion(region);
-+ //this.check();
-+ }
-+ }
-+
-+ protected void recalculateRegion(final Region region) {
-+ region.markedForRecalc = false;
-+ //region.check();
-+ // clear unused regions
-+ for (final Iterator<RegionSection> iterator = region.deadSections.iterator(); iterator.hasNext();) {
-+ final RegionSection deadSection = iterator.next();
-+
-+ if (deadSection.hasChunks()) {
-+ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!");
-+ }
-+ if (!region.removeRegionSection(deadSection)) {
-+ throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection);
-+ }
-+ if (!this.regionsBySection.remove(deadSection.regionCoordinate, deadSection)) {
-+ throw new IllegalStateException("Cannot remove dead section '" +
-+ deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " +
-+ this.regionsBySection.get(deadSection.regionCoordinate));
-+ }
-+ }
-+ region.deadSections.clear();
-+
-+ // implicitly cover cases where size == 0
-+ if (region.sections.size() < this.minSectionRecalcCount) {
-+ //region.check();
-+ return;
-+ }
-+
-+ // run a test to see if we actually need to recalculate
-+ // TODO
-+
-+ // destroy and rebuild the region
-+ region.dead = true;
-+
-+ // destroy region state
-+ for (final Iterator<RegionSection> iterator = region.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
-+ final RegionSection aliveSection = iterator.next();
-+ if (!aliveSection.hasChunks()) {
-+ throw new IllegalStateException("Alive section '" + aliveSection.toStringWithRegion() + "' has no chunks!");
-+ }
-+ if (!this.regionsBySection.remove(aliveSection.regionCoordinate, aliveSection)) {
-+ throw new IllegalStateException("Cannot remove alive section '" +
-+ aliveSection.toStringWithRegion() + "' from section state! State at section coordinate: " +
-+ this.regionsBySection.get(aliveSection.regionCoordinate));
-+ }
-+ }
-+
-+ // rebuild regions
-+ for (final Iterator<RegionSection> iterator = region.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
-+ final RegionSection aliveSection = iterator.next();
-+ this.getOrCreateAndMergeSection(aliveSection.getSectionX(), aliveSection.getSectionZ(), aliveSection);
-+ }
-+ }
-+
-+ public static final class Region {
-+ protected final IteratorSafeOrderedReferenceSet<RegionSection> sections = new IteratorSafeOrderedReferenceSet<>();
-+ protected final ReferenceOpenHashSet<RegionSection> deadSections = new ReferenceOpenHashSet<>(16, 0.7f);
-+ protected boolean dead;
-+ protected boolean markedForRecalc;
-+
-+ public final SingleThreadChunkRegionManager regionManager;
-+ public final RegionData regionData;
-+
-+ protected Region(final SingleThreadChunkRegionManager regionManager) {
-+ this.regionManager = regionManager;
-+ this.regionData = regionManager.regionDataSupplier.get();
-+ }
-+
-+ public IteratorSafeOrderedReferenceSet.Iterator<RegionSection> getSections() {
-+ return this.sections.iterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS);
-+ }
-+
-+ protected final double getDeadSectionPercent() {
-+ return (double)this.deadSections.size() / (double)this.sections.size();
-+ }
-+
-+ /*
-+ protected void check() {
-+ if (this.dead) {
-+ throw new IllegalStateException("Dead region!");
-+ }
-+ for (final Iterator<RegionSection<T>> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
-+ final RegionSection<T> section = iterator.next();
-+ if (section.region != this) {
-+ throw new IllegalStateException("Region section must point to us!");
-+ }
-+ if (this.regionManager.regionsBySection.get(section.regionCoordinate) != section) {
-+ throw new IllegalStateException("Region section must match the regionmanager state!");
-+ }
-+ }
-+ }
-+ */
-+
-+ // note: it is not true that the region at this point is not in any region. use the region field on the section
-+ // to see if it is currently in another region.
-+ protected final boolean addRegionSection(final RegionSection section) {
-+ if (!this.sections.add(section)) {
-+ return false;
-+ }
-+
-+ section.sectionData.addToRegion(section, section.region, this);
-+
-+ section.region = this;
-+ return true;
-+ }
-+
-+ protected final boolean removeRegionSection(final RegionSection section) {
-+ if (!this.sections.remove(section)) {
-+ return false;
-+ }
-+
-+ section.sectionData.removeFromRegion(section, this);
-+
-+ return true;
-+ }
-+
-+ protected void mergeInto(final Region mergeTarget) {
-+ if (this == mergeTarget) {
-+ throw new IllegalStateException("Cannot merge a region onto itself");
-+ }
-+ if (this.dead) {
-+ throw new IllegalStateException("Source region is dead! Source " + this + ", target " + mergeTarget);
-+ } else if (mergeTarget.dead) {
-+ throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget);
-+ }
-+ this.dead = true;
-+ if (this.markedForRecalc) {
-+ this.regionManager.removeFromRecalcQueue(this);
-+ }
-+
-+ for (final Iterator<RegionSection> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
-+ final RegionSection section = iterator.next();
-+
-+ if (!mergeTarget.addRegionSection(section)) {
-+ throw new IllegalStateException("Target cannot contain source's sections! Source " + this + ", target " + mergeTarget);
-+ }
-+ }
-+
-+ for (final RegionSection deadSection : this.deadSections) {
-+ if (!this.sections.contains(deadSection)) {
-+ throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this);
-+ }
-+ mergeTarget.deadSections.add(deadSection);
-+ }
-+ //mergeTarget.check();
-+ }
-+
-+ protected void markSectionAlive(final RegionSection section) {
-+ this.deadSections.remove(section);
-+ if (this.markedForRecalc && (this.sections.size() < this.regionManager.minSectionRecalcCount || this.getDeadSectionPercent() < this.regionManager.maxDeadRegionPercent)) {
-+ this.regionManager.removeFromRecalcQueue(this);
-+ this.markedForRecalc = false;
-+ }
-+ }
-+
-+ protected void markSectionDead(final RegionSection section) {
-+ this.deadSections.add(section);
-+ if (!this.markedForRecalc && (this.sections.size() >= this.regionManager.minSectionRecalcCount || this.sections.size() == this.deadSections.size()) && this.getDeadSectionPercent() >= this.regionManager.maxDeadRegionPercent) {
-+ this.regionManager.addToRecalcQueue(this);
-+ this.markedForRecalc = true;
-+ }
-+ }
-+
-+ @Override
-+ public String toString() {
-+ final StringBuilder ret = new StringBuilder(128);
-+
-+ ret.append("Region{");
-+ ret.append("dead=").append(this.dead).append(',');
-+ ret.append("markedForRecalc=").append(this.markedForRecalc).append(',');
-+
-+ ret.append("sectionCount=").append(this.sections.size()).append(',');
-+ ret.append("sections=[");
-+ for (final Iterator<RegionSection> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) {
-+ final RegionSection section = iterator.next();
-+ ret.append(section);
-+ if (iterator.hasNext()) {
-+ ret.append(',');
-+ }
-+ }
-+ ret.append(']');
-+
-+ ret.append('}');
-+ return ret.toString();
-+ }
-+ }
-+
-+ public static final class RegionSection {
-+ protected final long regionCoordinate;
-+ protected final long[] chunksBitset;
-+ protected int chunkCount;
-+ protected Region region;
-+
-+ public final SingleThreadChunkRegionManager regionManager;
-+ public final RegionSectionData sectionData;
-+
-+ protected RegionSection(final long regionCoordinate, final SingleThreadChunkRegionManager regionManager) {
-+ this.regionCoordinate = regionCoordinate;
-+ this.regionManager = regionManager;
-+ this.chunksBitset = new long[Math.max(1, regionManager.regionSectionChunkSize * regionManager.regionSectionChunkSize / Long.SIZE)];
-+ this.sectionData = regionManager.regionSectionDataSupplier.get();
-+ }
-+
-+ public int getSectionX() {
-+ return MCUtil.getCoordinateX(this.regionCoordinate);
-+ }
-+
-+ public int getSectionZ() {
-+ return MCUtil.getCoordinateZ(this.regionCoordinate);
-+ }
-+
-+ public Region getRegion() {
-+ return this.region;
-+ }
-+
-+ private int getChunkIndex(final int chunkX, final int chunkZ) {
-+ return (chunkX & (this.regionManager.regionSectionChunkSize - 1)) | ((chunkZ & (this.regionManager.regionSectionChunkSize - 1)) << this.regionManager.regionChunkShift);
-+ }
-+
-+ protected boolean hasChunks() {
-+ return this.chunkCount != 0;
-+ }
-+
-+ protected void addChunk(final int chunkX, final int chunkZ) {
-+ final int index = this.getChunkIndex(chunkX, chunkZ);
-+ final long bitset = this.chunksBitset[index >>> 6]; // index / Long.SIZE
-+ final long after = this.chunksBitset[index >>> 6] = bitset | (1L << (index & (Long.SIZE - 1)));
-+ if (after == bitset) {
-+ throw new IllegalStateException("Cannot add a chunk to a section which already has the chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString());
-+ }
-+ if (++this.chunkCount != 1) {
-+ return;
-+ }
-+ this.region.markSectionAlive(this);
-+ }
-+
-+ protected void removeChunk(final int chunkX, final int chunkZ) {
-+ final int index = this.getChunkIndex(chunkX, chunkZ);
-+ final long before = this.chunksBitset[index >>> 6]; // index / Long.SIZE
-+ final long bitset = this.chunksBitset[index >>> 6] = before & ~(1L << (index & (Long.SIZE - 1)));
-+ if (before == bitset) {
-+ throw new IllegalStateException("Cannot remove a chunk from a section which does not have that chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString());
-+ }
-+ if (--this.chunkCount != 0) {
-+ return;
-+ }
-+ this.region.markSectionDead(this);
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return "RegionSection{" +
-+ "regionCoordinate=" + new ChunkPos(this.regionCoordinate).toString() + "," +
-+ "chunkCount=" + this.chunkCount + "," +
-+ "chunksBitset=" + toString(this.chunksBitset) + "," +
-+ "hash=" + this.hashCode() +
-+ "}";
-+ }
-+
-+ public String toStringWithRegion() {
-+ return "RegionSection{" +
-+ "regionCoordinate=" + new ChunkPos(this.regionCoordinate).toString() + "," +
-+ "chunkCount=" + this.chunkCount + "," +
-+ "chunksBitset=" + toString(this.chunksBitset) + "," +
-+ "hash=" + this.hashCode() + "," +
-+ "region=" + this.region +
-+ "}";
-+ }
-+
-+ private static String toString(final long[] array) {
-+ final StringBuilder ret = new StringBuilder();
-+ for (final long value : array) {
-+ // zero pad the hex string
-+ final char[] zeros = new char[Long.SIZE / 4];
-+ Arrays.fill(zeros, '0');
-+ final String string = Long.toHexString(value);
-+ System.arraycopy(string.toCharArray(), 0, zeros, zeros.length - string.length(), string.length());
-+
-+ ret.append(zeros);
-+ }
-+
-+ return ret.toString();
-+ }
-+ }
-+
-+ public static interface RegionData {
-+
-+ }
-+
-+ public static interface RegionSectionData {
-+
-+ public void removeFromRegion(final RegionSection section, final Region from);
-+
-+ // removal from the old region is handled via removeFromRegion
-+ public void addToRegion(final RegionSection section, final Region oldRegion, final Region newRegion);
-+
-+ }
-+}
diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
new file mode 100644
-index 0000000000000000000000000000000000000000..0d0cb3e63acd5156b6f9d6d78cc949b0af36a77b
+index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783075ae17a
--- /dev/null
+++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
-@@ -0,0 +1,303 @@
+@@ -0,0 +1,297 @@
+package io.papermc.paper.chunk.system;
+
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
@@ -2720,12 +1787,16 @@ index 0000000000000000000000000000000000000000..0d0cb3e63acd5156b6f9d6d78cc949b0
+ }
+ scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
+ if (chunk == null) {
-+ onComplete.accept(null);
++ if (onComplete != null) {
++ onComplete.accept(null);
++ }
+ } else {
+ if (chunk.getPersistedStatus().isOrAfter(toStatus)) {
+ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ } else {
-+ onComplete.accept(null);
++ if (onComplete != null) {
++ onComplete.accept(null);
++ }
+ }
+ }
+ });
@@ -2757,8 +1828,6 @@ index 0000000000000000000000000000000000000000..0d0cb3e63acd5156b6f9d6d78cc949b0
+ if (onComplete != null) {
+ onComplete.accept(chunk);
+ }
-+ } catch (final ThreadDeath death) {
-+ throw death;
+ } catch (final Throwable thr) {
+ LOGGER.error("Exception handling chunk load callback", thr);
+ SneakyThrow.sneaky(thr);
@@ -2825,8 +1894,6 @@ index 0000000000000000000000000000000000000000..0d0cb3e63acd5156b6f9d6d78cc949b0
+ if (onComplete != null) {
+ onComplete.accept(chunk);
+ }
-+ } catch (final ThreadDeath death) {
-+ throw death;
+ } catch (final Throwable thr) {
+ LOGGER.error("Exception handling chunk load callback", thr);
+ SneakyThrow.sneaky(thr);
@@ -2905,21 +1972,15 @@ index 0000000000000000000000000000000000000000..0d0cb3e63acd5156b6f9d6d78cc949b0
+ }
+
+ public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
-+ final ChunkMap chunkMap = level.chunkSource.chunkMap;
-+ for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) {
-+ chunkMap.regionManagers.get(index).addChunk(holder.getPos().x, holder.getPos().z);
-+ }
++
+ }
+
+ public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
-+ final ChunkMap chunkMap = level.chunkSource.chunkMap;
-+ for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) {
-+ chunkMap.regionManagers.get(index).removeChunk(holder.getPos().x, holder.getPos().z);
-+ }
++
+ }
+
+ public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
-+ chunk.playerChunk = holder;
++
+ }
+
+ public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
@@ -3368,14 +2429,16 @@ index 0000000000000000000000000000000000000000..16785bd5c0524f6bad0691ca7ecd4514
+}
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..cea9c098ade00ee87b8efc8164ab72f5279758f0
+index 0000000000000000000000000000000000000000..c90acc3bde887b9c8f8d49fcc3195657c721bc14
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/IntervalledCounter.java
-@@ -0,0 +1,115 @@
+@@ -0,0 +1,128 @@
+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;
@@ -3385,8 +2448,8 @@ index 0000000000000000000000000000000000000000..cea9c098ade00ee87b8efc8164ab72f5
+ protected int tail; // exclusive
+
+ public IntervalledCounter(final long interval) {
-+ this.times = new long[8];
-+ this.counts = new long[8];
++ this.times = new long[INITIAL_SIZE];
++ this.counts = new long[INITIAL_SIZE];
+ this.interval = interval;
+ }
+
@@ -3441,13 +2504,13 @@ index 0000000000000000000000000000000000000000..cea9c098ade00ee87b8efc8164ab72f5
+ this.tail = nextTail;
+ }
+
-+ public void updateAndAdd(final int count) {
++ public void updateAndAdd(final long count) {
+ final long currTime = System.nanoTime();
+ this.updateCurrentTime(currTime);
+ this.addTime(currTime, count);
+ }
+
-+ public void updateAndAdd(final int count, final long currTime) {
++ public void updateAndAdd(final long count, final long currTime) {
+ this.updateCurrentTime(currTime);
+ this.addTime(currTime, count);
+ }
@@ -3467,9 +2530,12 @@ index 0000000000000000000000000000000000000000..cea9c098ade00ee87b8efc8164ab72f5
+ 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);
+
@@ -3480,19 +2546,27 @@ index 0000000000000000000000000000000000000000..cea9c098ade00ee87b8efc8164ab72f5
+
+ // returns in units per second
+ public double getRate() {
-+ return this.size() / (this.interval * 1.0e-9);
++ return (double)this.sum / ((double)this.interval * 1.0E-9);
+ }
+
-+ public long size() {
++ 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..eb36bef19e6729c1cc44aefa927317963aba929e
+index 0000000000000000000000000000000000000000..c6c723d9378c593c8608d5940f63c98dff097cd0
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/MCUtil.java
-@@ -0,0 +1,554 @@
+@@ -0,0 +1,550 @@
+package io.papermc.paper.util;
+
+import com.google.common.collect.ImmutableList;
@@ -4020,10 +3094,6 @@ index 0000000000000000000000000000000000000000..eb36bef19e6729c1cc44aefa92731796
+ }
+ }
+
-+ public static int getTicketLevelFor(net.minecraft.world.level.chunk.status.ChunkStatus status) {
-+ return net.minecraft.server.level.ChunkMap.MAX_VIEW_DISTANCE + io.papermc.paper.chunk.system.ChunkSystem.getDistance(status);
-+ }
-+
+ @NotNull
+ public static <T> List<T> copyListAndAdd(@NotNull final List<T> original,
+ @NotNull final T newElement) {
@@ -4469,1033 +3539,6 @@ index 0000000000000000000000000000000000000000..0fd814f1d65c111266a2b20f86561839
+ }
+ }
+}
-diff --git a/src/main/java/io/papermc/paper/util/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/io/papermc/paper/util/misc/Delayed26WayDistancePropagator3D.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..470402573bc31106d5a63e415b958fb7f9c36aa9
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/misc/Delayed26WayDistancePropagator3D.java
-@@ -0,0 +1,297 @@
-+package io.papermc.paper.util.misc;
-+
-+import io.papermc.paper.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/io/papermc/paper/util/misc/Delayed8WayDistancePropagator2D.java b/src/main/java/io/papermc/paper/util/misc/Delayed8WayDistancePropagator2D.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..808d1449ac44ae86a650932365081fbaf178d141
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/misc/Delayed8WayDistancePropagator2D.java
-@@ -0,0 +1,718 @@
-+package io.papermc.paper.util.misc;
-+
-+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;
-+import io.papermc.paper.util.MCUtil;
-+
-+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(MCUtil.getCoordinateKey(x, z));
-+ }
-+
-+ public void setSource(final int x, final int z, final int level) {
-+ this.setSource(MCUtil.getCoordinateKey(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(MCUtil.getCoordinateKey(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 = MCUtil.getCoordinateKey(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 = MCUtil.getCoordinateKey(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/io/papermc/paper/util/player/NearbyPlayers.java b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java
new file mode 100644
index 0000000000000000000000000000000000000000..c3ce8a42dddd76b7189ad5685b23f9d9f8ccadb3
@@ -6081,7 +4124,7 @@ index 3e5a85a7ad6149b04622c254fbc2e174896a4128..3f662692ed4846e026a9d48595e7b3b2
+
}
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 40adb6117b9e0d5f70103113202a07715e403e2a..b1325e090f2c7aff31d27fc38ca7173efe31ed7c 100644
+index 40adb6117b9e0d5f70103113202a07715e403e2a..9eb987f9d86396d6b7e9d4f3834bea3326640ac7 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -310,6 +310,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -6119,18 +4162,6 @@ index 40adb6117b9e0d5f70103113202a07715e403e2a..b1325e090f2c7aff31d27fc38ca7173e
this.profiler.push("tallying");
long j = Util.getNanos() - i;
int k = this.tickCount % 100;
-@@ -1470,6 +1475,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- try {
- worldserver.timings.doTick.startTiming(); // Spigot
- worldserver.tick(shouldKeepTicking);
-+ // Paper start
-+ for (final io.papermc.paper.chunk.SingleThreadChunkRegionManager regionManager : worldserver.getChunkSource().chunkMap.regionManagers) {
-+ regionManager.recalculateRegions();
-+ }
-+ // Paper end
- worldserver.timings.doTick.stopTiming(); // Spigot
- } catch (Throwable throwable) {
- CrashReport crashreport = CrashReport.forThrowable(throwable, "Exception ticking world");
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
index f40a2f348c45a29168ca3d4eef07b5b628060bee..c643bb0daa5cd264fd6ebab7acf0a2bdd7fe7029 100644
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -6347,10 +4378,10 @@ index f40a2f348c45a29168ca3d4eef07b5b628060bee..c643bb0daa5cd264fd6ebab7acf0a2bd
+ // Paper end
}
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b8ef8fe7f 100644
+index 5b920beb39dad8d392b4e5e12a89880720e41942..d3caadb3e884d7d0468daf5eff9abd6629ac4b49 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -170,6 +170,62 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -170,6 +170,37 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
};
// CraftBukkit end
@@ -6379,31 +4410,6 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
+ }
+ // Paper end
+ // Paper start
-+ public final List<io.papermc.paper.chunk.SingleThreadChunkRegionManager> regionManagers = new java.util.ArrayList<>();
-+ public final io.papermc.paper.chunk.SingleThreadChunkRegionManager dataRegionManager;
-+
-+ public static final class DataRegionData implements io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionData {
-+ }
-+
-+ public static final class DataRegionSectionData implements io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSectionData {
-+
-+ @Override
-+ public void removeFromRegion(final io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section,
-+ final io.papermc.paper.chunk.SingleThreadChunkRegionManager.Region from) {
-+ final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData;
-+ final DataRegionData fromData = (DataRegionData)from.regionData;
-+ }
-+
-+ @Override
-+ public void addToRegion(final io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section,
-+ final io.papermc.paper.chunk.SingleThreadChunkRegionManager.Region oldRegion,
-+ final io.papermc.paper.chunk.SingleThreadChunkRegionManager.Region newRegion) {
-+ final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData;
-+ final DataRegionData oldRegionData = oldRegion == null ? null : (DataRegionData)oldRegion.regionData;
-+ final DataRegionData newRegionData = (DataRegionData)newRegion.regionData;
-+ }
-+ }
-+
+ public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
+ return this.pendingUnloads.get(io.papermc.paper.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
@@ -6413,13 +4419,11 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
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();
-@@ -221,8 +277,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -221,8 +252,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world.getServer(), world);
this.setServerViewDistance(viewDistance);
this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, this.mainThreadMailbox);
+ // Paper start
-+ this.dataRegionManager = new io.papermc.paper.chunk.SingleThreadChunkRegionManager(this.level, 2, (1.0 / 3.0), 1, 6, "Data", DataRegionData::new, DataRegionSectionData::new);
-+ this.regionManagers.add(this.dataRegionManager);
+ this.nearbyPlayers = new io.papermc.paper.util.player.NearbyPlayers(this.level);
+ // Paper end
+ }
@@ -6438,7 +4442,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
protected ChunkGenerator generator() {
return this.worldGenContext.generator();
}
-@@ -378,9 +450,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -378,9 +423,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
};
stringbuilder.append("Updating:").append(System.lineSeparator());
@@ -6450,7 +4454,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
CrashReport crashreport = CrashReport.forThrowable(exception, "Chunk loading");
CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk loading");
-@@ -422,8 +494,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -422,8 +467,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
holder.setTicketLevel(level);
} else {
holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this);
@@ -6465,7 +4469,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
this.updatingChunkMap.put(pos, holder);
this.modified = true;
}
-@@ -445,7 +523,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -445,7 +496,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
protected void saveAllChunks(boolean flush) {
if (flush) {
@@ -6474,7 +4478,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
MutableBoolean mutableboolean = new MutableBoolean();
do {
-@@ -468,7 +546,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -468,7 +519,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
});
this.flushWorker();
} else {
@@ -6483,7 +4487,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
}
}
-@@ -487,7 +565,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -487,7 +538,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public boolean hasWork() {
@@ -6492,7 +4496,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
}
private void processUnloads(BooleanSupplier shouldKeepTicking) {
-@@ -504,6 +582,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -504,6 +555,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
this.updatingChunkMap.remove(j);
@@ -6500,7 +4504,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
this.pendingUnloads.put(j, playerchunk);
this.modified = true;
++i;
-@@ -523,7 +602,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -523,7 +575,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
int l = 0;
@@ -6509,7 +4513,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) {
if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) {
-@@ -541,7 +620,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -541,7 +593,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
} else {
ChunkAccess ichunkaccess = holder.getLatestChunk();
@@ -6522,7 +4526,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
LevelChunk chunk;
if (ichunkaccess instanceof LevelChunk) {
-@@ -559,7 +642,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -559,7 +615,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.lightEngine.tryScheduleUpdate();
this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null);
this.chunkSaveCooldowns.remove(ichunkaccess.getPos().toLong());
@@ -6533,7 +4537,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
}
};
-@@ -896,7 +981,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -896,7 +954,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
}
@@ -6542,7 +4546,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
int j = Mth.clamp(watchDistance, 2, 32);
if (j != this.serverViewDistance) {
-@@ -913,7 +998,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -913,7 +971,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
@@ -6551,7 +4555,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance);
}
-@@ -942,7 +1027,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -942,7 +1000,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public int size() {
@@ -6560,7 +4564,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
}
public DistanceManager getDistanceManager() {
-@@ -950,19 +1035,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -950,19 +1008,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
protected Iterable<ChunkHolder> getChunks() {
@@ -6585,7 +4589,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
Optional<ChunkAccess> optional = Optional.ofNullable(playerchunk.getLatestChunk());
Optional<LevelChunk> optional1 = optional.flatMap((ichunkaccess) -> {
return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty();
-@@ -1083,6 +1168,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1083,6 +1141,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
player.setChunkTrackingView(ChunkTrackingView.EMPTY);
this.updateChunkTracking(player);
@@ -6593,7 +4597,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
} else {
SectionPos sectionposition = player.getLastSectionPos();
-@@ -1091,6 +1177,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1091,6 +1150,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.distanceManager.removePlayer(sectionposition, player);
}
@@ -6601,7 +4605,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY);
}
-@@ -1142,6 +1229,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1142,6 +1202,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.updateChunkTracking(player);
}
@@ -6609,7 +4613,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b
}
private void updateChunkTracking(ServerPlayer player) {
-@@ -1385,10 +1473,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1385,10 +1446,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
});
}
@@ -6655,7 +4659,7 @@ index b6cc33943fe7e4667944f3e6f868b3033ea9ca18..27065ffc5473c518acee3a3096b83fac
while (objectiterator.hasNext()) {
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-index d39268911ed7c4d60ee6a82178be23245aae58c4..ab57071cc6ce8b79d883f2426855c1abf577e90d 100644
+index d39268911ed7c4d60ee6a82178be23245aae58c4..e9f53f57c363a32106880ea9aad0ccf5a7342509 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -48,6 +48,7 @@ import net.minecraft.world.level.storage.LevelStorageSource;
@@ -6666,81 +4670,30 @@ index d39268911ed7c4d60ee6a82178be23245aae58c4..ab57071cc6ce8b79d883f2426855c1ab
private static final List<ChunkStatus> CHUNK_STATUSES = ChunkStatus.getStatusList();
private final DistanceManager distanceManager;
final ServerLevel level;
-@@ -66,6 +67,14 @@ public class ServerChunkCache extends ChunkSource {
+@@ -66,6 +67,12 @@ public class ServerChunkCache extends ChunkSource {
@Nullable
@VisibleForDebug
private NaturalSpawner.SpawnState lastSpawnState;
+ // Paper start
+ public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> tickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
+ public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
-+ final com.destroystokyo.paper.util.concurrent.WeakSeqLock loadedChunkMapSeqLock = new com.destroystokyo.paper.util.concurrent.WeakSeqLock();
-+ final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<LevelChunk> loadedChunkMap = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(8192, 0.5f);
++ private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<net.minecraft.world.level.chunk.LevelChunk> fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>();
+ long chunkFutureAwaitCounter;
-+ private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4];
+ // Paper end
public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) {
this.level = world;
-@@ -91,6 +100,124 @@ public class ServerChunkCache extends ChunkSource {
+@@ -91,6 +98,54 @@ public class ServerChunkCache extends ChunkSource {
return chunk.getFullChunkNow() != null;
}
// CraftBukkit end
+ // Paper start
-+ private static int getChunkCacheKey(int x, int z) {
-+ return x & 3 | ((z & 3) << 2);
-+ }
-+
+ public void addLoadedChunk(LevelChunk chunk) {
-+ this.loadedChunkMapSeqLock.acquireWrite();
-+ try {
-+ this.loadedChunkMap.put(chunk.coordinateKey, chunk);
-+ } finally {
-+ this.loadedChunkMapSeqLock.releaseWrite();
-+ }
-+
-+ // rewrite cache if we have to
-+ // we do this since we also cache null chunks
-+ int cacheKey = getChunkCacheKey(chunk.locX, chunk.locZ);
-+
-+ this.lastLoadedChunks[cacheKey] = chunk;
++ this.fullChunks.put(chunk.coordinateKey, chunk);
+ }
+
+ public void removeLoadedChunk(LevelChunk chunk) {
-+ this.loadedChunkMapSeqLock.acquireWrite();
-+ try {
-+ this.loadedChunkMap.remove(chunk.coordinateKey);
-+ } finally {
-+ this.loadedChunkMapSeqLock.releaseWrite();
-+ }
-+
-+ // rewrite cache if we have to
-+ // we do this since we also cache null chunks
-+ int cacheKey = getChunkCacheKey(chunk.locX, chunk.locZ);
-+
-+ LevelChunk cachedChunk = this.lastLoadedChunks[cacheKey];
-+ if (cachedChunk != null && cachedChunk.coordinateKey == chunk.coordinateKey) {
-+ this.lastLoadedChunks[cacheKey] = null;
-+ }
-+ }
-+
-+ public final LevelChunk getChunkAtIfLoadedMainThread(int x, int z) {
-+ int cacheKey = getChunkCacheKey(x, z);
-+
-+ LevelChunk cachedChunk = this.lastLoadedChunks[cacheKey];
-+ if (cachedChunk != null && cachedChunk.locX == x & cachedChunk.locZ == z) {
-+ return cachedChunk;
-+ }
-+
-+ long chunkKey = ChunkPos.asLong(x, z);
-+
-+ cachedChunk = this.loadedChunkMap.get(chunkKey);
-+ // Skipping a null check to avoid extra instructions to improve inline capability
-+ this.lastLoadedChunks[cacheKey] = cachedChunk;
-+ return cachedChunk;
-+ }
-+
-+ public final LevelChunk getChunkAtIfLoadedMainThreadNoCache(int x, int z) {
-+ return this.loadedChunkMap.get(ChunkPos.asLong(x, z));
++ this.fullChunks.remove(chunk.coordinateKey);
+ }
+
+ @Nullable
@@ -6779,34 +4732,13 @@ index d39268911ed7c4d60ee6a82178be23245aae58c4..ab57071cc6ce8b79d883f2426855c1ab
+
+ @Nullable
+ public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) {
-+ long k = ChunkPos.asLong(x, z);
-+
-+ if (Thread.currentThread() == this.mainThread) {
-+ return this.getChunkAtIfLoadedMainThread(x, z);
-+ }
-+
-+ LevelChunk ret = null;
-+ long readlock;
-+ do {
-+ readlock = this.loadedChunkMapSeqLock.acquireRead();
-+ try {
-+ ret = this.loadedChunkMap.get(k);
-+ } catch (Throwable thr) {
-+ if (thr instanceof ThreadDeath) {
-+ throw (ThreadDeath)thr;
-+ }
-+ // re-try, this means a CME occurred...
-+ continue;
-+ }
-+ } while (!this.loadedChunkMapSeqLock.tryReleaseRead(readlock));
-+
-+ return ret;
++ return this.fullChunks.get(ChunkPos.asLong(x, z));
+ }
+ // Paper end
@Override
public ThreadedLevelLightEngine getLightEngine() {
-@@ -286,7 +413,7 @@ public class ServerChunkCache extends ChunkSource {
+@@ -286,7 +341,7 @@ public class ServerChunkCache extends ChunkSource {
return this.mainThreadProcessor.pollTask();
}
@@ -6815,7 +4747,7 @@ index d39268911ed7c4d60ee6a82178be23245aae58c4..ab57071cc6ce8b79d883f2426855c1ab
boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
boolean flag1 = this.chunkMap.promoteChunkMap();
-@@ -299,6 +426,12 @@ public class ServerChunkCache extends ChunkSource {
+@@ -299,6 +354,12 @@ public class ServerChunkCache extends ChunkSource {
}
}
@@ -7466,120 +5398,21 @@ index a52077f0d93c94b0ea644bc14b9b28e84fd1b154..dcc0acd259920463a4464213b9a5e793
@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 b537e7a079497db428db405edfccde74f32f4208..7898e1aaf82f096fa74bd3f5859f0f4303ea677f 100644
+index b537e7a079497db428db405edfccde74f32f4208..c664021dbfffcf0db3247041270ce9a1ee6940de 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-@@ -116,6 +116,109 @@ public class LevelChunk extends ChunkAccess {
+@@ -116,6 +116,10 @@ public class LevelChunk extends ChunkAccess {
public boolean needsDecoration;
// CraftBukkit end
+ // Paper start
-+ public @Nullable net.minecraft.server.level.ChunkHolder playerChunk;
-+
-+ static final int NEIGHBOUR_CACHE_RADIUS = 3;
-+ public static int getNeighbourCacheRadius() {
-+ return NEIGHBOUR_CACHE_RADIUS;
-+ }
-+
+ boolean loadedTicketLevel;
-+ private long neighbourChunksLoadedBitset;
-+ private final LevelChunk[] loadedNeighbourChunks = new LevelChunk[(NEIGHBOUR_CACHE_RADIUS * 2 + 1) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)];
-+
-+ private static int getNeighbourIndex(final int relativeX, final int relativeZ) {
-+ // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)
-+ // optimised variant of the above by moving some of the ops to compile time
-+ return relativeX + (relativeZ * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)) + (NEIGHBOUR_CACHE_RADIUS + NEIGHBOUR_CACHE_RADIUS * ((NEIGHBOUR_CACHE_RADIUS * 2 + 1)));
-+ }
-+
-+ public final LevelChunk getRelativeNeighbourIfLoaded(final int relativeX, final int relativeZ) {
-+ return this.loadedNeighbourChunks[getNeighbourIndex(relativeX, relativeZ)];
-+ }
-+
-+ public final boolean isNeighbourLoaded(final int relativeX, final int relativeZ) {
-+ return (this.neighbourChunksLoadedBitset & (1L << getNeighbourIndex(relativeX, relativeZ))) != 0;
-+ }
-+
-+ public final void setNeighbourLoaded(final int relativeX, final int relativeZ, final LevelChunk chunk) {
-+ if (chunk == null) {
-+ throw new IllegalArgumentException("Chunk must be non-null, neighbour: (" + relativeX + "," + relativeZ + "), chunk: " + this.chunkPos);
-+ }
-+ final long before = this.neighbourChunksLoadedBitset;
-+ final int index = getNeighbourIndex(relativeX, relativeZ);
-+ this.loadedNeighbourChunks[index] = chunk;
-+ this.neighbourChunksLoadedBitset |= (1L << index);
-+ this.onNeighbourChange(before, this.neighbourChunksLoadedBitset);
-+ }
-+
-+ public final void setNeighbourUnloaded(final int relativeX, final int relativeZ) {
-+ final long before = this.neighbourChunksLoadedBitset;
-+ final int index = getNeighbourIndex(relativeX, relativeZ);
-+ this.loadedNeighbourChunks[index] = null;
-+ this.neighbourChunksLoadedBitset &= ~(1L << index);
-+ this.onNeighbourChange(before, this.neighbourChunksLoadedBitset);
-+ }
-+
-+ public final void resetNeighbours() {
-+ final long before = this.neighbourChunksLoadedBitset;
-+ this.neighbourChunksLoadedBitset = 0L;
-+ java.util.Arrays.fill(this.loadedNeighbourChunks, null);
-+ this.onNeighbourChange(before, 0L);
-+ }
-+
-+ protected void onNeighbourChange(final long bitsetBefore, final long bitsetAfter) {
-+
-+ }
-+
-+ public final boolean isAnyNeighborsLoaded() {
-+ return neighbourChunksLoadedBitset != 0;
-+ }
-+ public final boolean areNeighboursLoaded(final int radius) {
-+ return LevelChunk.areNeighboursLoaded(this.neighbourChunksLoadedBitset, radius);
-+ }
-+
-+ public static boolean areNeighboursLoaded(final long bitset, final int radius) {
-+ // index = relativeX + (relativeZ * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)) + (NEIGHBOUR_CACHE_RADIUS + NEIGHBOUR_CACHE_RADIUS * ((NEIGHBOUR_CACHE_RADIUS * 2 + 1)))
-+ switch (radius) {
-+ case 0: {
-+ return (bitset & (1L << getNeighbourIndex(0, 0))) != 0;
-+ }
-+ case 1: {
-+ long mask = 0L;
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ mask |= (1L << getNeighbourIndex(dx, dz));
-+ }
-+ }
-+ return (bitset & mask) == mask;
-+ }
-+ case 2: {
-+ long mask = 0L;
-+ for (int dx = -2; dx <= 2; ++dx) {
-+ for (int dz = -2; dz <= 2; ++dz) {
-+ mask |= (1L << getNeighbourIndex(dx, dz));
-+ }
-+ }
-+ return (bitset & mask) == mask;
-+ }
-+ case 3: {
-+ long mask = 0L;
-+ for (int dx = -3; dx <= 3; ++dx) {
-+ for (int dz = -3; dz <= 3; ++dz) {
-+ mask |= (1L << getNeighbourIndex(dx, dz));
-+ }
-+ }
-+ return (bitset & mask) == mask;
-+ }
-+
-+ default:
-+ throw new IllegalArgumentException("Radius not recognized: " + radius);
-+ }
-+ }
+ // Paper end
+
public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) {
this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData());
Iterator iterator = protoChunk.getBlockEntities().values().iterator();
-@@ -181,8 +284,25 @@ public class LevelChunk extends ChunkAccess {
+@@ -181,8 +185,25 @@ public class LevelChunk extends ChunkAccess {
}
}
@@ -7605,7 +5438,7 @@ index b537e7a079497db428db405edfccde74f32f4208..7898e1aaf82f096fa74bd3f5859f0f43
int i = pos.getX();
int j = pos.getY();
int k = pos.getZ();
-@@ -224,6 +344,18 @@ public class LevelChunk extends ChunkAccess {
+@@ -224,6 +245,18 @@ public class LevelChunk extends ChunkAccess {
}
}
@@ -7624,51 +5457,25 @@ index b537e7a079497db428db405edfccde74f32f4208..7898e1aaf82f096fa74bd3f5859f0f43
@Override
public FluidState getFluidState(BlockPos pos) {
return this.getFluidState(pos.getX(), pos.getY(), pos.getZ());
-@@ -549,7 +681,25 @@ public class LevelChunk extends ChunkAccess {
+@@ -549,7 +582,11 @@ public class LevelChunk extends ChunkAccess {
// CraftBukkit start
public void loadCallback() {
-+ // Paper start - neighbour cache
-+ int chunkX = this.chunkPos.x;
-+ int chunkZ = this.chunkPos.z;
-+ net.minecraft.server.level.ServerChunkCache chunkProvider = this.level.getChunkSource();
-+ for (int dx = -NEIGHBOUR_CACHE_RADIUS; dx <= NEIGHBOUR_CACHE_RADIUS; ++dx) {
-+ for (int dz = -NEIGHBOUR_CACHE_RADIUS; dz <= NEIGHBOUR_CACHE_RADIUS; ++dz) {
-+ LevelChunk neighbour = chunkProvider.getChunkAtIfLoadedMainThreadNoCache(chunkX + dx, chunkZ + dz);
-+ if (neighbour != null) {
-+ neighbour.setNeighbourLoaded(-dx, -dz, this);
-+ // should be in cached already
-+ this.setNeighbourLoaded(dx, dz, neighbour);
-+ }
-+ }
-+ }
-+ this.setNeighbourLoaded(0, 0, this);
++ // Paper start
+ this.loadedTicketLevel = true;
-+ // Paper end - neighbour cache
++ // 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
-@@ -590,6 +740,22 @@ public class LevelChunk extends ChunkAccess {
+@@ -590,6 +627,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 - neighbour cache
-+ int chunkX = this.chunkPos.x;
-+ int chunkZ = this.chunkPos.z;
-+ net.minecraft.server.level.ServerChunkCache chunkProvider = this.level.getChunkSource();
-+ for (int dx = -NEIGHBOUR_CACHE_RADIUS; dx <= NEIGHBOUR_CACHE_RADIUS; ++dx) {
-+ for (int dz = -NEIGHBOUR_CACHE_RADIUS; dz <= NEIGHBOUR_CACHE_RADIUS; ++dz) {
-+ LevelChunk neighbour = chunkProvider.getChunkAtIfLoadedMainThreadNoCache(chunkX + dx, chunkZ + dz);
-+ if (neighbour != null) {
-+ neighbour.setNeighbourUnloaded(-dx, -dz);
-+ }
-+ }
-+ }
++ // Paper start
+ this.loadedTicketLevel = false;
-+ this.resetNeighbours();
+ // Paper end
}
@@ -8150,7 +5957,7 @@ index e08d4a45e313ef1b9005ef00ee0185a188171207..2fc68d129e2fdfd51e310ea5bdfb8332
public static byte toLegacyData(BlockState data) {
diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
-index 5fd6eb754c4edebed6798c65b06507a4e89ca48f..0794d92c42b0db6b367505ae28f09f1fd39fa312 100644
+index 5fd6eb754c4edebed6798c65b06507a4e89ca48f..524b51a0ab808a0629c871ad813115abd4b49dbd 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
@@ -58,6 +58,7 @@ import net.minecraft.world.phys.shapes.VoxelShape;
@@ -8170,19 +5977,19 @@ index 5fd6eb754c4edebed6798c65b06507a4e89ca48f..0794d92c42b0db6b367505ae28f09f1f
+ @Nullable
+ @Override
+ public BlockState getBlockStateIfLoaded(final BlockPos blockposition) {
-+ return null;
++ return this.handle.getBlockStateIfLoaded(blockposition);
+ }
+
+ @Nullable
+ @Override
+ public FluidState getFluidIfLoaded(final BlockPos blockposition) {
-+ return null;
++ return this.handle.getFluidIfLoaded(blockposition);
+ }
+
+ @Nullable
+ @Override
+ public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) {
-+ return null;
++ return this.handle.getChunkIfLoadedImmediately(x, z);
+ }
+ // Paper end
}