From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Aikar Date: Mon, 28 Mar 2016 20:55:47 -0400 Subject: [PATCH] MC Utils == AT == public net.minecraft.server.level.ServerChunkCache mainThread public net.minecraft.server.level.ServerLevel chunkSource public org.bukkit.craftbukkit.inventory.CraftItemStack handle public net.minecraft.server.level.ChunkMap getVisibleChunkIfPresent(J)Lnet/minecraft/server/level/ChunkHolder; public net.minecraft.server.level.ServerChunkCache mainThreadProcessor public net.minecraft.server.level.ServerChunkCache$MainThreadExecutor 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 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 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 { + + protected static final Object REMOVED = new Object(); + + protected final Long2ObjectLinkedOpenHashMap updatingMap; + protected final Long2ObjectLinkedOpenHashMap visibleMap; + protected final Long2ObjectLinkedOpenHashMap 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 getVisibleMap() { + return this.visibleMap; + } + + public Long2ObjectLinkedOpenHashMap 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 getUpdatingValues() { + return this.updatingMap.values(); + } + + public List 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 getVisibleValues() { + return this.visibleMap.values(); + } + + public List getVisibleValuesCopy() { + return new ArrayList<>(this.visibleMap.values()); + } + + public boolean performUpdates() { + if (this.queuedChanges.isEmpty()) { + return false; + } + + final ObjectBidirectionalIterator> iterator = this.queuedChanges.long2ObjectEntrySet().fastIterator(); + while (iterator.hasNext()) { + final Long2ObjectMap.Entry 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> iterator = this.queuedChanges.long2ObjectEntrySet().fastIterator(); + + try { + this.updatingMapSeqLock.acquireWrite(); + + while (iterator.hasNext()) { + final Long2ObjectMap.Entry 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 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java @@ -0,0 +1,128 @@ +package com.destroystokyo.paper.util.maplist; + +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; +import net.minecraft.world.level.chunk.LevelChunk; + +// list with O(1) remove & contains +/** + * @author Spottedleaf + */ +public final class ChunkList implements Iterable { + + protected final Long2IntOpenHashMap chunkToIndex = new Long2IntOpenHashMap(2, 0.8f); + { + this.chunkToIndex.defaultReturnValue(Integer.MIN_VALUE); + } + + protected static final LevelChunk[] EMPTY_LIST = new LevelChunk[0]; + + protected LevelChunk[] chunks = EMPTY_LIST; + protected int count; + + public int size() { + return this.count; + } + + public boolean contains(final LevelChunk chunk) { + return this.chunkToIndex.containsKey(chunk.coordinateKey); + } + + public boolean remove(final LevelChunk chunk) { + final int index = this.chunkToIndex.remove(chunk.coordinateKey); + if (index == Integer.MIN_VALUE) { + return false; + } + + // move the entity at the end to this index + final int endIndex = --this.count; + final LevelChunk end = this.chunks[endIndex]; + if (index != endIndex) { + // not empty after this call + this.chunkToIndex.put(end.coordinateKey, index); // update index + } + this.chunks[index] = end; + this.chunks[endIndex] = null; + + return true; + } + + public boolean add(final LevelChunk chunk) { + final int count = this.count; + final int currIndex = this.chunkToIndex.putIfAbsent(chunk.coordinateKey, count); + + if (currIndex != Integer.MIN_VALUE) { + return false; // already in this list + } + + LevelChunk[] list = this.chunks; + + if (list.length == count) { + // resize required + list = this.chunks = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative + } + + list[count] = chunk; + this.count = count + 1; + + return true; + } + + public LevelChunk getChecked(final int index) { + if (index < 0 || index >= this.count) { + throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count); + } + return this.chunks[index]; + } + + public LevelChunk getUnchecked(final int index) { + return this.chunks[index]; + } + + public LevelChunk[] getRawData() { + return this.chunks; + } + + public void clear() { + this.chunkToIndex.clear(); + Arrays.fill(this.chunks, 0, this.count, null); + this.count = 0; + } + + @Override + public Iterator iterator() { + return new Iterator() { + + LevelChunk lastRet; + int current; + + @Override + public boolean hasNext() { + return this.current < ChunkList.this.count; + } + + @Override + public LevelChunk next() { + if (this.current >= ChunkList.this.count) { + throw new NoSuchElementException(); + } + return this.lastRet = ChunkList.this.chunks[this.current++]; + } + + @Override + public void remove() { + final LevelChunk lastRet = this.lastRet; + + if (lastRet == null) { + throw new IllegalStateException(); + } + this.lastRet = null; + + ChunkList.this.remove(lastRet); + --this.current; + } + }; + } +} diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java b/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java new file mode 100644 index 0000000000000000000000000000000000000000..0133ea6feb1ab88f021f66855669f58367e7420b --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java @@ -0,0 +1,128 @@ +package com.destroystokyo.paper.util.maplist; + +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import net.minecraft.world.entity.Entity; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +// list with O(1) remove & contains +/** + * @author Spottedleaf + */ +public final class EntityList implements Iterable { + + protected final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f); + { + this.entityToIndex.defaultReturnValue(Integer.MIN_VALUE); + } + + protected static final Entity[] EMPTY_LIST = new Entity[0]; + + protected Entity[] entities = EMPTY_LIST; + protected int count; + + public int size() { + return this.count; + } + + public boolean contains(final Entity entity) { + return this.entityToIndex.containsKey(entity.getId()); + } + + public boolean remove(final Entity entity) { + final int index = this.entityToIndex.remove(entity.getId()); + if (index == Integer.MIN_VALUE) { + return false; + } + + // move the entity at the end to this index + final int endIndex = --this.count; + final Entity end = this.entities[endIndex]; + if (index != endIndex) { + // not empty after this call + this.entityToIndex.put(end.getId(), index); // update index + } + this.entities[index] = end; + this.entities[endIndex] = null; + + return true; + } + + public boolean add(final Entity entity) { + final int count = this.count; + final int currIndex = this.entityToIndex.putIfAbsent(entity.getId(), count); + + if (currIndex != Integer.MIN_VALUE) { + return false; // already in this list + } + + Entity[] list = this.entities; + + if (list.length == count) { + // resize required + list = this.entities = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative + } + + list[count] = entity; + this.count = count + 1; + + return true; + } + + public Entity getChecked(final int index) { + if (index < 0 || index >= this.count) { + throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count); + } + return this.entities[index]; + } + + public Entity getUnchecked(final int index) { + return this.entities[index]; + } + + public Entity[] getRawData() { + return this.entities; + } + + public void clear() { + this.entityToIndex.clear(); + Arrays.fill(this.entities, 0, this.count, null); + this.count = 0; + } + + @Override + public Iterator iterator() { + return new Iterator() { + + Entity lastRet; + int current; + + @Override + public boolean hasNext() { + return this.current < EntityList.this.count; + } + + @Override + public Entity next() { + if (this.current >= EntityList.this.count) { + throw new NoSuchElementException(); + } + return this.lastRet = EntityList.this.entities[this.current++]; + } + + @Override + public void remove() { + final Entity lastRet = this.lastRet; + + if (lastRet == null) { + throw new IllegalStateException(); + } + this.lastRet = null; + + EntityList.this.remove(lastRet); + --this.current; + } + }; + } +} diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java b/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java new file mode 100644 index 0000000000000000000000000000000000000000..277cfd9d1e8fff5d9b5e534b75c3c5162d58b0b7 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java @@ -0,0 +1,128 @@ +package com.destroystokyo.paper.util.maplist; + +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.shorts.Short2LongOpenHashMap; +import java.util.Arrays; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.GlobalPalette; + +/** + * @author Spottedleaf + */ +public final class IBlockDataList { + + static final GlobalPalette GLOBAL_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY); + + // map of location -> (index | (location << 16) | (palette id << 32)) + private final Short2LongOpenHashMap map = new Short2LongOpenHashMap(2, 0.8f); + { + this.map.defaultReturnValue(Long.MAX_VALUE); + } + + private static final long[] EMPTY_LIST = new long[0]; + + private long[] byIndex = EMPTY_LIST; + private int size; + + public static int getLocationKey(final int x, final int y, final int z) { + return (x & 15) | (((z & 15) << 4)) | ((y & 255) << (4 + 4)); + } + + public static BlockState getBlockDataFromRaw(final long raw) { + return GLOBAL_PALETTE.valueFor((int)(raw >>> 32)); + } + + public static int getIndexFromRaw(final long raw) { + return (int)(raw & 0xFFFF); + } + + public static int getLocationFromRaw(final long raw) { + return (int)((raw >>> 16) & 0xFFFF); + } + + public static long getRawFromValues(final int index, final int location, final BlockState data) { + return (long)index | ((long)location << 16) | (((long)GLOBAL_PALETTE.idFor(data)) << 32); + } + + public static long setIndexRawValues(final long value, final int index) { + return value & ~(0xFFFF) | (index); + } + + public long add(final int x, final int y, final int z, final BlockState data) { + return this.add(getLocationKey(x, y, z), data); + } + + public long add(final int location, final BlockState data) { + final long curr = this.map.get((short)location); + + if (curr == Long.MAX_VALUE) { + final int index = this.size++; + final long raw = getRawFromValues(index, location, data); + this.map.put((short)location, raw); + + if (index >= this.byIndex.length) { + this.byIndex = Arrays.copyOf(this.byIndex, (int)Math.max(4L, this.byIndex.length * 2L)); + } + + this.byIndex[index] = raw; + return raw; + } else { + final int index = getIndexFromRaw(curr); + final long raw = this.byIndex[index] = getRawFromValues(index, location, data); + + this.map.put((short)location, raw); + + return raw; + } + } + + public long remove(final int x, final int y, final int z) { + return this.remove(getLocationKey(x, y, z)); + } + + public long remove(final int location) { + final long ret = this.map.remove((short)location); + final int index = getIndexFromRaw(ret); + if (ret == Long.MAX_VALUE) { + return ret; + } + + // move the entry at the end to this index + final int endIndex = --this.size; + final long end = this.byIndex[endIndex]; + if (index != endIndex) { + // not empty after this call + this.map.put((short)getLocationFromRaw(end), setIndexRawValues(end, index)); + } + this.byIndex[index] = end; + this.byIndex[endIndex] = 0L; + + return ret; + } + + public int size() { + return this.size; + } + + public long getRaw(final int index) { + return this.byIndex[index]; + } + + public int getLocation(final int index) { + return getLocationFromRaw(this.getRaw(index)); + } + + public BlockState getData(final int index) { + return getBlockDataFromRaw(this.getRaw(index)); + } + + public void clear() { + this.size = 0; + this.map.clear(); + } + + public LongIterator getRawIterator() { + return this.map.values().iterator(); + } +} diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java b/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java new file mode 100644 index 0000000000000000000000000000000000000000..190c5f0b02a3d99054704ae1afbffb3498ddffe1 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/maplist/ReferenceList.java @@ -0,0 +1,125 @@ +package com.destroystokyo.paper.util.maplist; + +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * @author Spottedleaf + */ +public final class ReferenceList implements Iterable { + + protected final Reference2IntOpenHashMap referenceToIndex = new Reference2IntOpenHashMap<>(2, 0.8f); + { + this.referenceToIndex.defaultReturnValue(Integer.MIN_VALUE); + } + + protected static final Object[] EMPTY_LIST = new Object[0]; + + protected Object[] references = EMPTY_LIST; + protected int count; + + public int size() { + return this.count; + } + + public boolean contains(final E obj) { + return this.referenceToIndex.containsKey(obj); + } + + public boolean remove(final E obj) { + final int index = this.referenceToIndex.removeInt(obj); + if (index == Integer.MIN_VALUE) { + return false; + } + + // move the object at the end to this index + final int endIndex = --this.count; + final E end = (E)this.references[endIndex]; + if (index != endIndex) { + // not empty after this call + this.referenceToIndex.put(end, index); // update index + } + this.references[index] = end; + this.references[endIndex] = null; + + return true; + } + + public boolean add(final E obj) { + final int count = this.count; + final int currIndex = this.referenceToIndex.putIfAbsent(obj, count); + + if (currIndex != Integer.MIN_VALUE) { + return false; // already in this list + } + + Object[] list = this.references; + + if (list.length == count) { + // resize required + list = this.references = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative + } + + list[count] = obj; + this.count = count + 1; + + return true; + } + + public E getChecked(final int index) { + if (index < 0 || index >= this.count) { + throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count); + } + return (E)this.references[index]; + } + + public E getUnchecked(final int index) { + return (E)this.references[index]; + } + + public Object[] getRawData() { + return this.references; + } + + public void clear() { + this.referenceToIndex.clear(); + Arrays.fill(this.references, 0, this.count, null); + this.count = 0; + } + + @Override + public Iterator iterator() { + return new Iterator<>() { + private E lastRet; + private int current; + + @Override + public boolean hasNext() { + return this.current < ReferenceList.this.count; + } + + @Override + public E next() { + if (this.current >= ReferenceList.this.count) { + throw new NoSuchElementException(); + } + return this.lastRet = (E)ReferenceList.this.references[this.current++]; + } + + @Override + public void remove() { + final E lastRet = this.lastRet; + + if (lastRet == null) { + throw new IllegalStateException(); + } + this.lastRet = null; + + ReferenceList.this.remove(lastRet); + --this.current; + } + }; + } +} diff --git a/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java new file mode 100644 index 0000000000000000000000000000000000000000..41b9405d6759d865e0d14dd4f95163e9690e967d --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java @@ -0,0 +1,453 @@ +package com.destroystokyo.paper.util.misc; + +import io.papermc.paper.util.IntegerUtil; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; +import io.papermc.paper.util.MCUtil; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.ChunkPos; +import javax.annotation.Nullable; +import java.util.Iterator; + +/** @author Spottedleaf */ +public abstract class AreaMap { + + /* Tested via https://gist.github.com/Spottedleaf/520419c6f41ef348fe9926ce674b7217 */ + + protected final Object2LongOpenHashMap objectToLastCoordinate = new Object2LongOpenHashMap<>(); + protected final Object2IntOpenHashMap objectToViewDistance = new Object2IntOpenHashMap<>(); + + { + this.objectToViewDistance.defaultReturnValue(-1); + this.objectToLastCoordinate.defaultReturnValue(Long.MIN_VALUE); + } + + // we use linked for better iteration. + // map of: coordinate to set of objects in coordinate + protected final Long2ObjectOpenHashMap> areaMap = new Long2ObjectOpenHashMap<>(1024, 0.7f); + protected final PooledLinkedHashSets pooledHashSets; + + protected final ChangeCallback addCallback; + protected final ChangeCallback removeCallback; + protected final ChangeSourceCallback changeSourceCallback; + + public AreaMap() { + this(new PooledLinkedHashSets<>()); + } + + // let users define a "global" or "shared" pooled sets if they wish + public AreaMap(final PooledLinkedHashSets pooledHashSets) { + this(pooledHashSets, null, null); + } + + public AreaMap(final PooledLinkedHashSets pooledHashSets, final ChangeCallback addCallback, final ChangeCallback removeCallback) { + this(pooledHashSets, addCallback, removeCallback, null); + } + public AreaMap(final PooledLinkedHashSets pooledHashSets, final ChangeCallback addCallback, final ChangeCallback removeCallback, final ChangeSourceCallback changeSourceCallback) { + this.pooledHashSets = pooledHashSets; + this.addCallback = addCallback; + this.removeCallback = removeCallback; + this.changeSourceCallback = changeSourceCallback; + } + + @Nullable + public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getObjectsInRange(final long key) { + return this.areaMap.get(key); + } + + @Nullable + public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getObjectsInRange(final ChunkPos chunkPos) { + return this.areaMap.get(MCUtil.getCoordinateKey(chunkPos)); + } + + @Nullable + public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getObjectsInRange(final int chunkX, final int chunkZ) { + return this.areaMap.get(MCUtil.getCoordinateKey(chunkX, chunkZ)); + } + + // Long.MIN_VALUE indicates the object is not mapped + public final long getLastCoordinate(final E object) { + return this.objectToLastCoordinate.getOrDefault(object, Long.MIN_VALUE); + } + + // -1 indicates the object is not mapped + public final int getLastViewDistance(final E object) { + return this.objectToViewDistance.getOrDefault(object, -1); + } + + // returns the total number of mapped chunks + public final int size() { + return this.areaMap.size(); + } + + public final void addOrUpdate(final E object, final int chunkX, final int chunkZ, final int viewDistance) { + final int oldViewDistance = this.objectToViewDistance.put(object, viewDistance); + final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ); + final long oldPos = this.objectToLastCoordinate.put(object, newPos); + + if (oldViewDistance == -1) { + this.addObject(object, chunkX, chunkZ, Integer.MIN_VALUE, Integer.MIN_VALUE, viewDistance); + this.addObjectCallback(object, chunkX, chunkZ, viewDistance); + } else { + this.updateObject(object, oldPos, newPos, oldViewDistance, viewDistance); + this.updateObjectCallback(object, oldPos, newPos, oldViewDistance, viewDistance); + } + //this.validate(object, viewDistance); + } + + public final boolean update(final E object, final int chunkX, final int chunkZ, final int viewDistance) { + final int oldViewDistance = this.objectToViewDistance.replace(object, viewDistance); + if (oldViewDistance == -1) { + return false; + } else { + final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ); + final long oldPos = this.objectToLastCoordinate.put(object, newPos); + this.updateObject(object, oldPos, newPos, oldViewDistance, viewDistance); + this.updateObjectCallback(object, oldPos, newPos, oldViewDistance, viewDistance); + } + //this.validate(object, viewDistance); + return true; + } + + // called after the distance map updates + protected void updateObjectCallback(final E Object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) { + if (newPosition != oldPosition && this.changeSourceCallback != null) { + this.changeSourceCallback.accept(Object, oldPosition, newPosition); + } + } + + public final boolean add(final E object, final int chunkX, final int chunkZ, final int viewDistance) { + final int oldViewDistance = this.objectToViewDistance.putIfAbsent(object, viewDistance); + if (oldViewDistance != -1) { + return false; + } + + final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ); + this.objectToLastCoordinate.put(object, newPos); + this.addObject(object, chunkX, chunkZ, Integer.MIN_VALUE, Integer.MIN_VALUE, viewDistance); + this.addObjectCallback(object, chunkX, chunkZ, viewDistance); + + //this.validate(object, viewDistance); + + return true; + } + + // called after the distance map updates + protected void addObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {} + + public final boolean remove(final E object) { + final long position = this.objectToLastCoordinate.removeLong(object); + final int viewDistance = this.objectToViewDistance.removeInt(object); + + if (viewDistance == -1) { + return false; + } + + final int currentX = MCUtil.getCoordinateX(position); + final int currentZ = MCUtil.getCoordinateZ(position); + + this.removeObject(object, currentX, currentZ, currentX, currentZ, viewDistance); + this.removeObjectCallback(object, currentX, currentZ, viewDistance); + //this.validate(object, -1); + return true; + } + + // called after the distance map updates + protected void removeObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {} + + protected abstract PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getEmptySetFor(final E object); + + // expensive op, only for debug + protected void validate(final E object, final int viewDistance) { + int entiesGot = 0; + int expectedEntries = (2 * viewDistance + 1); + expectedEntries *= expectedEntries; + if (viewDistance < 0) { + expectedEntries = 0; + } + + final long currPosition = this.objectToLastCoordinate.getLong(object); + + final int centerX = MCUtil.getCoordinateX(currPosition); + final int centerZ = MCUtil.getCoordinateZ(currPosition); + + for (Iterator>> iterator = this.areaMap.long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + + final Long2ObjectLinkedOpenHashMap.Entry> entry = iterator.next(); + final long key = entry.getLongKey(); + final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet map = entry.getValue(); + + if (map.referenceCount == 0) { + throw new IllegalStateException("Invalid map"); + } + + if (map.contains(object)) { + ++entiesGot; + + final int chunkX = MCUtil.getCoordinateX(key); + final int chunkZ = MCUtil.getCoordinateZ(key); + + final int dist = Math.max(IntegerUtil.branchlessAbs(chunkX - centerX), IntegerUtil.branchlessAbs(chunkZ - centerZ)); + + if (dist > viewDistance) { + throw new IllegalStateException("Expected view distance " + viewDistance + ", got " + dist); + } + } + } + + if (entiesGot != expectedEntries) { + throw new IllegalStateException("Expected " + expectedEntries + ", got " + entiesGot); + } + } + + private void addObjectTo(final E object, final int chunkX, final int chunkZ, final int currChunkX, + final int currChunkZ, final int prevChunkX, final int prevChunkZ) { + final long key = MCUtil.getCoordinateKey(chunkX, chunkZ); + + PooledLinkedHashSets.PooledObjectLinkedOpenHashSet empty = this.getEmptySetFor(object); + PooledLinkedHashSets.PooledObjectLinkedOpenHashSet current = this.areaMap.putIfAbsent(key, empty); + + if (current != null) { + PooledLinkedHashSets.PooledObjectLinkedOpenHashSet next = this.pooledHashSets.findMapWith(current, object); + if (next == current) { + throw new IllegalStateException("Expected different map: got " + next.toString()); + } + this.areaMap.put(key, next); + + current = next; + // fall through to callback + } else { + current = empty; + } + + if (this.addCallback != null) { + try { + this.addCallback.accept(object, chunkX, chunkZ, currChunkX, currChunkZ, prevChunkX, prevChunkZ, current); + } catch (final Throwable ex) { + if (ex instanceof ThreadDeath) { + throw (ThreadDeath)ex; + } + MinecraftServer.LOGGER.error("Add callback for map threw exception ", ex); + } + } + } + + private void removeObjectFrom(final E object, final int chunkX, final int chunkZ, final int currChunkX, + final int currChunkZ, final int prevChunkX, final int prevChunkZ) { + final long key = MCUtil.getCoordinateKey(chunkX, chunkZ); + + PooledLinkedHashSets.PooledObjectLinkedOpenHashSet current = this.areaMap.get(key); + + if (current == null) { + throw new IllegalStateException("Current map may not be null for " + object + ", (" + chunkX + "," + chunkZ + ")"); + } + + PooledLinkedHashSets.PooledObjectLinkedOpenHashSet next = this.pooledHashSets.findMapWithout(current, object); + + if (next == current) { + throw new IllegalStateException("Current map [" + next.toString() + "] should have contained " + object + ", (" + chunkX + "," + chunkZ + ")"); + } + + if (next != null) { + this.areaMap.put(key, next); + } else { + this.areaMap.remove(key); + } + + if (this.removeCallback != null) { + try { + this.removeCallback.accept(object, chunkX, chunkZ, currChunkX, currChunkZ, prevChunkX, prevChunkZ, next); + } catch (final Throwable ex) { + if (ex instanceof ThreadDeath) { + throw (ThreadDeath)ex; + } + MinecraftServer.LOGGER.error("Remove callback for map threw exception ", ex); + } + } + } + + private void addObject(final E object, final int chunkX, final int chunkZ, final int prevChunkX, final int prevChunkZ, final int viewDistance) { + final int maxX = chunkX + viewDistance; + final int maxZ = chunkZ + viewDistance; + final int minX = chunkX - viewDistance; + final int minZ = chunkZ - viewDistance; + for (int x = minX; x <= maxX; ++x) { + for (int z = minZ; z <= maxZ; ++z) { + this.addObjectTo(object, x, z, chunkX, chunkZ, prevChunkX, prevChunkZ); + } + } + } + + private void removeObject(final E object, final int chunkX, final int chunkZ, final int currentChunkX, final int currentChunkZ, final int viewDistance) { + final int maxX = chunkX + viewDistance; + final int maxZ = chunkZ + viewDistance; + final int minX = chunkX - viewDistance; + final int minZ = chunkZ - viewDistance; + for (int x = minX; x <= maxX; ++x) { + for (int z = minZ; z <= maxZ; ++z) { + this.removeObjectFrom(object, x, z, currentChunkX, currentChunkZ, chunkX, chunkZ); + } + } + } + + /* math sign function except 0 returns 1 */ + protected static int sign(int val) { + return 1 | (val >> (Integer.SIZE - 1)); + } + + private void updateObject(final E object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) { + final int toX = MCUtil.getCoordinateX(newPosition); + final int toZ = MCUtil.getCoordinateZ(newPosition); + final int fromX = MCUtil.getCoordinateX(oldPosition); + final int fromZ = MCUtil.getCoordinateZ(oldPosition); + + final int dx = toX - fromX; + final int dz = toZ - fromZ; + + final int totalX = IntegerUtil.branchlessAbs(fromX - toX); + final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ); + + if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) { + // teleported? + this.removeObject(object, fromX, fromZ, fromX, fromZ, oldViewDistance); + this.addObject(object, toX, toZ, fromX, fromZ, newViewDistance); + return; + } + + if (oldViewDistance != newViewDistance) { + // remove loop + + final int oldMinX = fromX - oldViewDistance; + final int oldMinZ = fromZ - oldViewDistance; + final int oldMaxX = fromX + oldViewDistance; + final int oldMaxZ = fromZ + oldViewDistance; + for (int currX = oldMinX; currX <= oldMaxX; ++currX) { + for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) { + + // only remove if we're outside the new view distance... + if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) { + this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ); + } + } + } + + // add loop + + final int newMinX = toX - newViewDistance; + final int newMinZ = toZ - newViewDistance; + final int newMaxX = toX + newViewDistance; + final int newMaxZ = toZ + newViewDistance; + for (int currX = newMinX; currX <= newMaxX; ++currX) { + for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) { + + // only add if we're outside the old view distance... + if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) { + this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ); + } + } + } + + return; + } + + // x axis is width + // z axis is height + // right refers to the x axis of where we moved + // top refers to the z axis of where we moved + + // same view distance + + // used for relative positioning + final int up = sign(dz); // 1 if dz >= 0, -1 otherwise + final int right = sign(dx); // 1 if dx >= 0, -1 otherwise + + // The area excluded by overlapping the two view distance squares creates four rectangles: + // Two on the left, and two on the right. The ones on the left we consider the "removed" section + // and on the right the "added" section. + // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually + // exclusive to the regions they surround. + + // 4 points of the rectangle + int maxX; // exclusive + int minX; // inclusive + int maxZ; // exclusive + int minZ; // inclusive + + if (dx != 0) { + // handle right addition + + maxX = toX + (oldViewDistance * right) + right; // exclusive + minX = fromX + (oldViewDistance * right) + right; // inclusive + maxZ = fromZ + (oldViewDistance * up) + up; // exclusive + minZ = toZ - (oldViewDistance * up); // inclusive + + for (int currX = minX; currX != maxX; currX += right) { + for (int currZ = minZ; currZ != maxZ; currZ += up) { + this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ); + } + } + } + + if (dz != 0) { + // handle up addition + + maxX = toX + (oldViewDistance * right) + right; // exclusive + minX = toX - (oldViewDistance * right); // inclusive + maxZ = toZ + (oldViewDistance * up) + up; // exclusive + minZ = fromZ + (oldViewDistance * up) + up; // inclusive + + for (int currX = minX; currX != maxX; currX += right) { + for (int currZ = minZ; currZ != maxZ; currZ += up) { + this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ); + } + } + } + + if (dx != 0) { + // handle left removal + + maxX = toX - (oldViewDistance * right); // exclusive + minX = fromX - (oldViewDistance * right); // inclusive + maxZ = fromZ + (oldViewDistance * up) + up; // exclusive + minZ = toZ - (oldViewDistance * up); // inclusive + + for (int currX = minX; currX != maxX; currX += right) { + for (int currZ = minZ; currZ != maxZ; currZ += up) { + this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ); + } + } + } + + if (dz != 0) { + // handle down removal + + maxX = fromX + (oldViewDistance * right) + right; // exclusive + minX = fromX - (oldViewDistance * right); // inclusive + maxZ = toZ - (oldViewDistance * up); // exclusive + minZ = fromZ - (oldViewDistance * up); // inclusive + + for (int currX = minX; currX != maxX; currX += right) { + for (int currZ = minZ; currZ != maxZ; currZ += up) { + this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ); + } + } + } + } + + @FunctionalInterface + public static interface ChangeCallback { + + // if there is no previous position, then prevPos = Integer.MIN_VALUE + void accept(final E object, final int rangeX, final int rangeZ, final int currPosX, final int currPosZ, final int prevPosX, final int prevPosZ, + final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState); + + } + + @FunctionalInterface + public static interface ChangeSourceCallback { + void accept(final E object, final long prevPos, final long newPos); + } +} diff --git a/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java new file mode 100644 index 0000000000000000000000000000000000000000..896c3ff7ddb07f1f6f05f90e1e3fe7fb615071d4 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java @@ -0,0 +1,175 @@ +package com.destroystokyo.paper.util.misc; + +import io.papermc.paper.util.IntegerUtil; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import io.papermc.paper.util.MCUtil; +import net.minecraft.world.level.ChunkPos; + +/** @author Spottedleaf */ +public abstract class DistanceTrackingAreaMap extends AreaMap { + + // use this map only if you need distance tracking, the tracking here is obviously going to hit harder. + + protected final Long2IntOpenHashMap chunkToNearestDistance = new Long2IntOpenHashMap(1024, 0.7f); + { + this.chunkToNearestDistance.defaultReturnValue(-1); + } + + protected final DistanceChangeCallback distanceChangeCallback; + + public DistanceTrackingAreaMap() { + this(new PooledLinkedHashSets<>()); + } + + // let users define a "global" or "shared" pooled sets if they wish + public DistanceTrackingAreaMap(final PooledLinkedHashSets pooledHashSets) { + this(pooledHashSets, null, null, null); + } + + public DistanceTrackingAreaMap(final PooledLinkedHashSets pooledHashSets, final ChangeCallback addCallback, final ChangeCallback removeCallback, + final DistanceChangeCallback distanceChangeCallback) { + super(pooledHashSets, addCallback, removeCallback); + this.distanceChangeCallback = distanceChangeCallback; + } + + // ret -1 if there is nothing mapped + public final int getNearestObjectDistance(final long key) { + return this.chunkToNearestDistance.get(key); + } + + // ret -1 if there is nothing mapped + public final int getNearestObjectDistance(final ChunkPos chunkPos) { + return this.chunkToNearestDistance.get(MCUtil.getCoordinateKey(chunkPos)); + } + + // ret -1 if there is nothing mapped + public final int getNearestObjectDistance(final int chunkX, final int chunkZ) { + return this.chunkToNearestDistance.get(MCUtil.getCoordinateKey(chunkX, chunkZ)); + } + + protected final void recalculateDistance(final int chunkX, final int chunkZ) { + final long key = MCUtil.getCoordinateKey(chunkX, chunkZ); + final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet state = this.areaMap.get(key); + if (state == null) { + final int oldDistance = this.chunkToNearestDistance.remove(key); + // nothing here. + if (oldDistance == -1) { + // nothing was here previously + return; + } + if (this.distanceChangeCallback != null) { + this.distanceChangeCallback.accept(chunkX, chunkZ, oldDistance, -1, null); + } + return; + } + + int newDistance = Integer.MAX_VALUE; + + final Object[] rawData = state.getBackingSet(); + for (int i = 0, len = rawData.length; i < len; ++i) { + final Object raw = rawData[i]; + + if (raw == null) { + continue; + } + + final E object = (E)raw; + final long location = this.objectToLastCoordinate.getLong(object); + + final int distance = Math.max(IntegerUtil.branchlessAbs(chunkX - MCUtil.getCoordinateX(location)), IntegerUtil.branchlessAbs(chunkZ - MCUtil.getCoordinateZ(location))); + + if (distance < newDistance) { + newDistance = distance; + } + } + + final int oldDistance = this.chunkToNearestDistance.put(key, newDistance); + + if (oldDistance != newDistance) { + if (this.distanceChangeCallback != null) { + this.distanceChangeCallback.accept(chunkX, chunkZ, oldDistance, newDistance, state); + } + } + } + + @Override + protected void addObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) { + final int maxX = chunkX + viewDistance; + final int maxZ = chunkZ + viewDistance; + final int minX = chunkX - viewDistance; + final int minZ = chunkZ - viewDistance; + for (int x = minX; x <= maxX; ++x) { + for (int z = minZ; z <= maxZ; ++z) { + this.recalculateDistance(x, z); + } + } + } + + @Override + protected void removeObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) { + final int maxX = chunkX + viewDistance; + final int maxZ = chunkZ + viewDistance; + final int minX = chunkX - viewDistance; + final int minZ = chunkZ - viewDistance; + for (int x = minX; x <= maxX; ++x) { + for (int z = minZ; z <= maxZ; ++z) { + this.recalculateDistance(x, z); + } + } + } + + @Override + protected void updateObjectCallback(final E object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) { + if (oldPosition == newPosition && newViewDistance == oldViewDistance) { + return; + } + + final int toX = MCUtil.getCoordinateX(newPosition); + final int toZ = MCUtil.getCoordinateZ(newPosition); + final int fromX = MCUtil.getCoordinateX(oldPosition); + final int fromZ = MCUtil.getCoordinateZ(oldPosition); + + final int totalX = IntegerUtil.branchlessAbs(fromX - toX); + final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ); + + if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) { + // teleported? + this.removeObjectCallback(object, fromX, fromZ, oldViewDistance); + this.addObjectCallback(object, toX, toZ, newViewDistance); + return; + } + + final int minX = Math.min(fromX - oldViewDistance, toX - newViewDistance); + final int maxX = Math.max(fromX + oldViewDistance, toX + newViewDistance); + final int minZ = Math.min(fromZ - oldViewDistance, toZ - newViewDistance); + final int maxZ = Math.max(fromZ + oldViewDistance, toZ + newViewDistance); + + for (int x = minX; x <= maxX; ++x) { + for (int z = minZ; z <= maxZ; ++z) { + final int distXOld = IntegerUtil.branchlessAbs(x - fromX); + final int distZOld = IntegerUtil.branchlessAbs(z - fromZ); + + if (Math.max(distXOld, distZOld) <= oldViewDistance) { + this.recalculateDistance(x, z); + continue; + } + + final int distXNew = IntegerUtil.branchlessAbs(x - toX); + final int distZNew = IntegerUtil.branchlessAbs(z - toZ); + + if (Math.max(distXNew, distZNew) <= newViewDistance) { + this.recalculateDistance(x, z); + continue; + } + } + } + } + + @FunctionalInterface + public static interface DistanceChangeCallback { + + void accept(final int posX, final int posZ, final int oldNearestDistance, final int newNearestDistance, + final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet state); + + } +} diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java new file mode 100644 index 0000000000000000000000000000000000000000..46954db7ecd35ac4018fdf476df7c8020d7ce6c8 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java @@ -0,0 +1,32 @@ +package com.destroystokyo.paper.util.misc; + +import net.minecraft.server.level.ServerPlayer; + +/** + * @author Spottedleaf + */ +public final class PlayerAreaMap extends AreaMap { + + public PlayerAreaMap() { + super(); + } + + public PlayerAreaMap(final PooledLinkedHashSets pooledHashSets) { + super(pooledHashSets); + } + + public PlayerAreaMap(final PooledLinkedHashSets pooledHashSets, final ChangeCallback addCallback, + final ChangeCallback removeCallback) { + this(pooledHashSets, addCallback, removeCallback, null); + } + + public PlayerAreaMap(final PooledLinkedHashSets pooledHashSets, final ChangeCallback addCallback, + final ChangeCallback removeCallback, final ChangeSourceCallback changeSourceCallback) { + super(pooledHashSets, addCallback, removeCallback, changeSourceCallback); + } + + @Override + protected PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getEmptySetFor(final ServerPlayer player) { + return player.cachedSingleHashSet; + } +} diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java new file mode 100644 index 0000000000000000000000000000000000000000..d05dcea15f7047b58736c7c0e07920a04d6c5abe --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java @@ -0,0 +1,24 @@ +package com.destroystokyo.paper.util.misc; + +import net.minecraft.server.level.ServerPlayer; + +public class PlayerDistanceTrackingAreaMap extends DistanceTrackingAreaMap { + + public PlayerDistanceTrackingAreaMap() { + super(); + } + + public PlayerDistanceTrackingAreaMap(final PooledLinkedHashSets pooledHashSets) { + super(pooledHashSets); + } + + public PlayerDistanceTrackingAreaMap(final PooledLinkedHashSets pooledHashSets, final ChangeCallback addCallback, + final ChangeCallback removeCallback, final DistanceChangeCallback distanceChangeCallback) { + super(pooledHashSets, addCallback, removeCallback, distanceChangeCallback); + } + + @Override + protected PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getEmptySetFor(final ServerPlayer player) { + return player.cachedSingleHashSet; + } +} diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java b/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java new file mode 100644 index 0000000000000000000000000000000000000000..e51104e65a07b6ea7bbbcbb6afb066ef6401cc5b --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java @@ -0,0 +1,287 @@ +package com.destroystokyo.paper.util.misc; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import java.lang.ref.WeakReference; + +/** @author Spottedleaf */ +public class PooledLinkedHashSets { + + /* Tested via https://gist.github.com/Spottedleaf/a93bb7a8993d6ce142d3efc5932bf573 */ + + // we really want to avoid that equals() check as much as possible... + protected final Object2ObjectOpenHashMap, PooledObjectLinkedOpenHashSet> mapPool = new Object2ObjectOpenHashMap<>(128, 0.25f); + + protected void decrementReferenceCount(final PooledObjectLinkedOpenHashSet current) { + if (current.referenceCount == 0) { + throw new IllegalStateException("Cannot decrement reference count for " + current); + } + if (current.referenceCount == -1 || --current.referenceCount > 0) { + return; + } + + this.mapPool.remove(current); + return; + } + + public PooledObjectLinkedOpenHashSet findMapWith(final PooledObjectLinkedOpenHashSet current, final E object) { + final PooledObjectLinkedOpenHashSet cached = current.getAddCache(object); + + if (cached != null) { + decrementReferenceCount(current); + + if (cached.referenceCount == 0) { + // bring the map back from the dead + PooledObjectLinkedOpenHashSet contending = this.mapPool.putIfAbsent(cached, cached); + if (contending != null) { + // a map already exists with the elements we want + if (contending.referenceCount != -1) { + ++contending.referenceCount; + } + current.updateAddCache(object, contending); + return contending; + } + + cached.referenceCount = 1; + } else if (cached.referenceCount != -1) { + ++cached.referenceCount; + } + + return cached; + } + + if (!current.add(object)) { + return current; + } + + // we use get/put since we use a different key on put + PooledObjectLinkedOpenHashSet ret = this.mapPool.get(current); + + if (ret == null) { + ret = new PooledObjectLinkedOpenHashSet<>(current); + current.remove(object); + this.mapPool.put(ret, ret); + ret.referenceCount = 1; + } else { + if (ret.referenceCount != -1) { + ++ret.referenceCount; + } + current.remove(object); + } + + current.updateAddCache(object, ret); + + decrementReferenceCount(current); + return ret; + } + + // rets null if current.size() == 1 + public PooledObjectLinkedOpenHashSet findMapWithout(final PooledObjectLinkedOpenHashSet current, final E object) { + if (current.set.size() == 1) { + decrementReferenceCount(current); + return null; + } + + final PooledObjectLinkedOpenHashSet cached = current.getRemoveCache(object); + + if (cached != null) { + decrementReferenceCount(current); + + if (cached.referenceCount == 0) { + // bring the map back from the dead + PooledObjectLinkedOpenHashSet contending = this.mapPool.putIfAbsent(cached, cached); + if (contending != null) { + // a map already exists with the elements we want + if (contending.referenceCount != -1) { + ++contending.referenceCount; + } + current.updateRemoveCache(object, contending); + return contending; + } + + cached.referenceCount = 1; + } else if (cached.referenceCount != -1) { + ++cached.referenceCount; + } + + return cached; + } + + if (!current.remove(object)) { + return current; + } + + // we use get/put since we use a different key on put + PooledObjectLinkedOpenHashSet ret = this.mapPool.get(current); + + if (ret == null) { + ret = new PooledObjectLinkedOpenHashSet<>(current); + current.add(object); + this.mapPool.put(ret, ret); + ret.referenceCount = 1; + } else { + if (ret.referenceCount != -1) { + ++ret.referenceCount; + } + current.add(object); + } + + current.updateRemoveCache(object, ret); + + decrementReferenceCount(current); + return ret; + } + + static final class RawSetObjectLinkedOpenHashSet extends ObjectOpenHashSet { + + public RawSetObjectLinkedOpenHashSet() { + super(); + } + + public RawSetObjectLinkedOpenHashSet(final int capacity) { + super(capacity); + } + + public RawSetObjectLinkedOpenHashSet(final int capacity, final float loadFactor) { + super(capacity, loadFactor); + } + + @Override + public RawSetObjectLinkedOpenHashSet clone() { + return (RawSetObjectLinkedOpenHashSet)super.clone(); + } + + public E[] getRawSet() { + return this.key; + } + } + + public static final class PooledObjectLinkedOpenHashSet { + + private static final WeakReference NULL_REFERENCE = new WeakReference<>(null); + + final RawSetObjectLinkedOpenHashSet set; + int referenceCount; // -1 if special + int hash; // optimize hashcode + + // add cache + WeakReference lastAddObject = NULL_REFERENCE; + WeakReference> lastAddMap = NULL_REFERENCE; + + // remove cache + WeakReference lastRemoveObject = NULL_REFERENCE; + WeakReference> lastRemoveMap = NULL_REFERENCE; + + public PooledObjectLinkedOpenHashSet(final PooledLinkedHashSets pooledSets) { + this.set = new RawSetObjectLinkedOpenHashSet<>(2, 0.8f); + } + + public PooledObjectLinkedOpenHashSet(final E single) { + this((PooledLinkedHashSets)null); + this.referenceCount = -1; + this.add(single); + } + + public PooledObjectLinkedOpenHashSet(final PooledObjectLinkedOpenHashSet other) { + this.set = other.set.clone(); + this.hash = other.hash; + } + + // from https://github.com/Spottedleaf/ConcurrentUtil/blob/master/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java + // generated by https://github.com/skeeto/hash-prospector + private static int hash0(int x) { + x *= 0x36935555; + x ^= x >>> 16; + return x; + } + + PooledObjectLinkedOpenHashSet getAddCache(final E element) { + final E currentAdd = this.lastAddObject.get(); + + if (currentAdd == null || !(currentAdd == element || currentAdd.equals(element))) { + return null; + } + + return this.lastAddMap.get(); + } + + PooledObjectLinkedOpenHashSet getRemoveCache(final E element) { + final E currentRemove = this.lastRemoveObject.get(); + + if (currentRemove == null || !(currentRemove == element || currentRemove.equals(element))) { + return null; + } + + return this.lastRemoveMap.get(); + } + + void updateAddCache(final E element, final PooledObjectLinkedOpenHashSet map) { + this.lastAddObject = new WeakReference<>(element); + this.lastAddMap = new WeakReference<>(map); + } + + void updateRemoveCache(final E element, final PooledObjectLinkedOpenHashSet map) { + this.lastRemoveObject = new WeakReference<>(element); + this.lastRemoveMap = new WeakReference<>(map); + } + + boolean add(final E element) { + boolean added = this.set.add(element); + + if (added) { + this.hash += hash0(element.hashCode()); + } + + return added; + } + + boolean remove(Object element) { + boolean removed = this.set.remove(element); + + if (removed) { + this.hash -= hash0(element.hashCode()); + } + + return removed; + } + + public boolean contains(final Object element) { + return this.set.contains(element); + } + + public E[] getBackingSet() { + return this.set.getRawSet(); + } + + public int size() { + return this.set.size(); + } + + @Override + public int hashCode() { + return this.hash; + } + + @Override + public boolean equals(final Object other) { + if (!(other instanceof PooledObjectLinkedOpenHashSet)) { + return false; + } + if (this.referenceCount == 0) { + return other == this; + } else { + if (other == this) { + // Unfortunately we are never equal to our own instance while in use! + return false; + } + return this.hash == ((PooledObjectLinkedOpenHashSet)other).hash && this.set.equals(((PooledObjectLinkedOpenHashSet)other).set); + } + } + + @Override + public String toString() { + return "PooledHashSet: size: " + this.set.size() + ", reference count: " + this.referenceCount + ", hash: " + + this.hashCode() + ", identity: " + System.identityHashCode(this) + " map: " + this.set.toString(); + } + } +} diff --git a/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java b/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java new file mode 100644 index 0000000000000000000000000000000000000000..a743703502cea333bd4231b6557de50e8eaf81eb --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java @@ -0,0 +1,85 @@ +package com.destroystokyo.paper.util.pooled; + +import io.papermc.paper.util.MCUtil; +import org.apache.commons.lang3.mutable.MutableInt; + +import java.util.ArrayDeque; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public final class PooledObjects { + + /** + * Wrapper for an object that will be have a cleaner registered for it, and may be automatically returned to pool. + */ + public class AutoReleased { + private final E object; + private final Runnable cleaner; + + public AutoReleased(E object, Runnable cleaner) { + this.object = object; + this.cleaner = cleaner; + } + + public final E getObject() { + return object; + } + + public final Runnable getCleaner() { + return cleaner; + } + } + + public static final PooledObjects POOLED_MUTABLE_INTEGERS = new PooledObjects<>(MutableInt::new, 1024); + + private final Supplier creator; + private final Consumer releaser; + private final int maxPoolSize; + private final ArrayDeque queue; + + public PooledObjects(final Supplier creator, int maxPoolSize) { + this(creator, maxPoolSize, null); + } + public PooledObjects(final Supplier creator, int maxPoolSize, Consumer releaser) { + if (creator == null) { + throw new NullPointerException("Creator must not be null"); + } + if (maxPoolSize <= 0) { + throw new IllegalArgumentException("Max pool size must be greater-than 0"); + } + + this.queue = new ArrayDeque<>(maxPoolSize); + this.maxPoolSize = maxPoolSize; + this.creator = creator; + this.releaser = releaser; + } + + public AutoReleased acquireCleaner(Object holder) { + return acquireCleaner(holder, this::release); + } + + public AutoReleased acquireCleaner(Object holder, Consumer releaser) { + E resource = acquire(); + Runnable cleaner = MCUtil.registerCleaner(holder, resource, releaser); + return new AutoReleased(resource, cleaner); + } + + public final E acquire() { + E value; + synchronized (queue) { + value = this.queue.pollLast(); + } + return value != null ? value : this.creator.get(); + } + + public final void release(final E value) { + if (this.releaser != null) { + this.releaser.accept(value); + } + synchronized (this.queue) { + if (queue.size() < this.maxPoolSize) { + this.queue.addLast(value); + } + } + } +} diff --git a/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java b/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java new file mode 100644 index 0000000000000000000000000000000000000000..b3329c6fcd6758a781a51f5ba8f5052ac1c77b49 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java @@ -0,0 +1,71 @@ +package com.destroystokyo.paper.util.set; + +import java.util.Collection; + +/** + * @author Spottedleaf + */ +public final class OptimizedSmallEnumSet> { + + private final Class enumClass; + private long backingSet; + + public OptimizedSmallEnumSet(final Class clazz) { + if (clazz == null) { + throw new IllegalArgumentException("Null class"); + } + if (!clazz.isEnum()) { + throw new IllegalArgumentException("Class must be enum, not " + clazz.getCanonicalName()); + } + this.enumClass = clazz; + } + + public boolean addUnchecked(final E element) { + final int ordinal = element.ordinal(); + final long key = 1L << ordinal; + + final long prev = this.backingSet; + this.backingSet = prev | key; + + return (prev & key) == 0; + } + + public boolean removeUnchecked(final E element) { + final int ordinal = element.ordinal(); + final long key = 1L << ordinal; + + final long prev = this.backingSet; + this.backingSet = prev & ~key; + + return (prev & key) != 0; + } + + public void clear() { + this.backingSet = 0L; + } + + public int size() { + return Long.bitCount(this.backingSet); + } + + public void addAllUnchecked(final Collection enums) { + for (final E element : enums) { + if (element == null) { + throw new NullPointerException("Null element"); + } + this.backingSet |= (1L << element.ordinal()); + } + } + + public long getBackingSet() { + return this.backingSet; + } + + public boolean hasCommonElements(final OptimizedSmallEnumSet other) { + return (other.backingSet & this.backingSet) != 0; + } + + public boolean hasElement(final E element) { + return (this.backingSet & (1L << element.ordinal())) != 0; + } +} diff --git a/src/main/java/com/mojang/logging/LogUtils.java b/src/main/java/com/mojang/logging/LogUtils.java index 46cab7a8c7b87ab01b26074b04f5a02b3907cfc4..49019b4a9bc4e634d54a9b0acaf9229a5c896f85 100644 --- a/src/main/java/com/mojang/logging/LogUtils.java +++ b/src/main/java/com/mojang/logging/LogUtils.java @@ -61,4 +61,9 @@ public class LogUtils { public static Logger getLogger() { return LoggerFactory.getLogger(STACK_WALKER.getCallerClass()); } + // Paper start + public static Logger getClassLogger() { + return LoggerFactory.getLogger(STACK_WALKER.getCallerClass().getSimpleName()); + } + // Paper end } diff --git a/src/main/java/io/papermc/paper/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 regionsBySection = new Long2ObjectOpenHashMap<>(); + protected final ReferenceLinkedOpenHashSet needsRecalculation = new ReferenceLinkedOpenHashSet<>(); + protected final int minSectionRecalcCount; + protected final double maxDeadRegionPercent; + protected final Supplier regionDataSupplier; + protected final Supplier regionSectionDataSupplier; + + public SingleThreadChunkRegionManager(final ServerLevel world, final int minSectionRecalcCount, + final double maxDeadRegionPercent, final int sectionMergeRadius, + final int regionSectionChunkShift, + final String name, final Supplier regionDataSupplier, + final Supplier 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> checked = new ReferenceOpenHashSet<>(); + + for (RegionSection section : this.regionsBySection.values()) { + if (!checked.add(section.region)) { + section.region.check(); + } + } + for (Region 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 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 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 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 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 sections = new IteratorSafeOrderedReferenceSet<>(); + protected final ReferenceOpenHashSet 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 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> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final RegionSection 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 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 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..8a5e93961dac4d87c81c0e70b6f4124a1f1d2556 --- /dev/null +++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java @@ -0,0 +1,294 @@ +package io.papermc.paper.chunk.system; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import com.destroystokyo.paper.util.SneakyThrow; +import com.mojang.datafixers.util.Either; +import com.mojang.logging.LogUtils; +import io.papermc.paper.util.CoordinateUtils; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.LevelChunk; +import org.bukkit.Bukkit; +import org.slf4j.Logger; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +public final class ChunkSystem { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) { + scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); + } + + public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) { + level.chunkSource.mainThreadProcessor.execute(run); + } + + public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen, + final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority, + final Consumer onComplete) { + if (gen) { + scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + return; + } + scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { + if (chunk == null) { + onComplete.accept(null); + } else { + if (chunk.getStatus().isOrAfter(toStatus)) { + scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + } else { + onComplete.accept(null); + } + } + }); + } + + static final TicketType CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo); + + private static long chunkLoadCounter = 0L; + public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, + final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer onComplete) { + if (!Bukkit.isPrimaryThread()) { + scheduleChunkTask(level, chunkX, chunkZ, () -> { + scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + }, priority); + return; + } + + final int minLevel = 33 + ChunkStatus.getDistance(toStatus); + final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; + final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); + + if (addTicket) { + level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); + } + level.chunkSource.runDistanceManagerUpdates(); + + final Consumer loadCallback = (final ChunkAccess chunk) -> { + try { + if (onComplete != null) { + onComplete.accept(chunk); + } + } catch (final ThreadDeath death) { + throw death; + } catch (final Throwable thr) { + LOGGER.error("Exception handling chunk load callback", thr); + SneakyThrow.sneaky(thr); + } finally { + if (addTicket) { + level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); + level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); + } + } + }; + + final ChunkHolder holder = level.chunkSource.chunkMap.getUpdatingChunkIfPresent(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + if (holder == null || holder.getTicketLevel() > minLevel) { + loadCallback.accept(null); + return; + } + + final CompletableFuture> loadFuture = holder.getOrScheduleFuture(toStatus, level.chunkSource.chunkMap); + + if (loadFuture.isDone()) { + loadCallback.accept(loadFuture.join().left().orElse(null)); + return; + } + + loadFuture.whenCompleteAsync((final Either either, final Throwable thr) -> { + if (thr != null) { + loadCallback.accept(null); + return; + } + loadCallback.accept(either.left().orElse(null)); + }, (final Runnable r) -> { + scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST); + }); + } + + public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, + final ChunkHolder.FullChunkStatus toStatus, final boolean addTicket, + final PrioritisedExecutor.Priority priority, final Consumer onComplete) { + if (toStatus == ChunkHolder.FullChunkStatus.INACCESSIBLE) { + throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); + } + + if (!Bukkit.isPrimaryThread()) { + scheduleChunkTask(level, chunkX, chunkZ, () -> { + scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + }, priority); + return; + } + + final int minLevel = 33 - (toStatus.ordinal() - 1); + final int radius = toStatus.ordinal() - 1; + final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; + final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); + + if (addTicket) { + level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); + } + level.chunkSource.runDistanceManagerUpdates(); + + final Consumer loadCallback = (final LevelChunk chunk) -> { + try { + if (onComplete != null) { + onComplete.accept(chunk); + } + } catch (final ThreadDeath death) { + throw death; + } catch (final Throwable thr) { + LOGGER.error("Exception handling chunk load callback", thr); + SneakyThrow.sneaky(thr); + } finally { + if (addTicket) { + level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); + level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); + } + } + }; + + final ChunkHolder holder = level.chunkSource.chunkMap.getUpdatingChunkIfPresent(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + + if (holder == null || holder.getTicketLevel() > minLevel) { + loadCallback.accept(null); + return; + } + + final CompletableFuture> tickingState; + switch (toStatus) { + case BORDER: { + tickingState = holder.getFullChunkFuture(); + break; + } + case TICKING: { + tickingState = holder.getTickingChunkFuture(); + break; + } + case ENTITY_TICKING: { + tickingState = holder.getEntityTickingChunkFuture(); + break; + } + default: { + throw new IllegalStateException("Cannot reach here"); + } + } + + if (tickingState.isDone()) { + loadCallback.accept(tickingState.join().left().orElse(null)); + return; + } + + tickingState.whenCompleteAsync((final Either either, final Throwable thr) -> { + if (thr != null) { + loadCallback.accept(null); + return; + } + loadCallback.accept(either.left().orElse(null)); + }, (final Runnable r) -> { + scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST); + }); + } + + public static List getVisibleChunkHolders(final ServerLevel level) { + return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values()); + } + + public static List getUpdatingChunkHolders(final ServerLevel level) { + return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values()); + } + + public static int getVisibleChunkHolderCount(final ServerLevel level) { + return level.chunkSource.chunkMap.visibleChunkMap.size(); + } + + public static int getUpdatingChunkHolderCount(final ServerLevel level) { + return level.chunkSource.chunkMap.updatingChunkMap.size(); + } + + public static boolean hasAnyChunkHolders(final ServerLevel level) { + return getUpdatingChunkHolderCount(level) != 0; + } + + public static void onEntityPreAdd(final ServerLevel level, final Entity entity) { + + } + + 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.pos.x, holder.pos.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.pos.x, holder.pos.z); + } + } + + public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { + chunk.playerChunk = holder; + } + + public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { + + } + + public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { + chunk.level.getChunkSource().tickingChunks.add(chunk); + } + + public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { + chunk.level.getChunkSource().tickingChunks.remove(chunk); + } + + public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { + chunk.level.getChunkSource().entityTickingChunks.add(chunk); + } + + public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { + chunk.level.getChunkSource().entityTickingChunks.remove(chunk); + } + + public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { + return level.chunkSource.chunkMap.getUnloadingChunkHolder(chunkX, chunkZ); + } + + public static int getSendViewDistance(final ServerPlayer player) { + return getLoadViewDistance(player); + } + + public static int getLoadViewDistance(final ServerPlayer player) { + final ServerLevel level = player.getLevel(); + if (level == null) { + return Bukkit.getViewDistance() + 1; + } + return level.chunkSource.chunkMap.getEffectiveViewDistance() + 1; + } + + public static int getTickViewDistance(final ServerPlayer player) { + final ServerLevel level = player.getLevel(); + if (level == null) { + return Bukkit.getSimulationDistance(); + } + return level.chunkSource.chunkMap.distanceManager.getSimulationDistance(); + } + + private ChunkSystem() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java new file mode 100644 index 0000000000000000000000000000000000000000..be668387f65a633c6ac497fca632a4767a1bf3a2 --- /dev/null +++ b/src/main/java/io/papermc/paper/util/CachedLists.java @@ -0,0 +1,8 @@ +package io.papermc.paper.util; + +public final class CachedLists { + + public static void reset() { + + } +} diff --git a/src/main/java/io/papermc/paper/util/CoordinateUtils.java b/src/main/java/io/papermc/paper/util/CoordinateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..413e4b6da027876dbbe8eb78f2568a440f431547 --- /dev/null +++ b/src/main/java/io/papermc/paper/util/CoordinateUtils.java @@ -0,0 +1,128 @@ +package io.papermc.paper.util; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; + +public final class CoordinateUtils { + + // dx, dz are relative to the target chunk + // dx, dz in [-radius, radius] + public static int getNeighbourMappedIndex(final int dx, final int dz, final int radius) { + return (dx + radius) + (2 * radius + 1)*(dz + radius); + } + + // the chunk keys are compatible with vanilla + + public static long getChunkKey(final BlockPos pos) { + return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL); + } + + public static long getChunkKey(final Entity entity) { + return ((Mth.lfloor(entity.getZ()) >> 4) << 32) | ((Mth.lfloor(entity.getX()) >> 4) & 0xFFFFFFFFL); + } + + public static long getChunkKey(final ChunkPos pos) { + return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL); + } + + public static long getChunkKey(final SectionPos pos) { + return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL); + } + + public static long getChunkKey(final int x, final int z) { + return ((long)z << 32) | (x & 0xFFFFFFFFL); + } + + public static int getChunkX(final long chunkKey) { + return (int)chunkKey; + } + + public static int getChunkZ(final long chunkKey) { + return (int)(chunkKey >>> 32); + } + + public static int getChunkCoordinate(final double blockCoordinate) { + return Mth.floor(blockCoordinate) >> 4; + } + + // the section keys are compatible with vanilla's + + static final int SECTION_X_BITS = 22; + static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1; + static final int SECTION_Y_BITS = 20; + static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1; + static final int SECTION_Z_BITS = 22; + static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1; + // format is y,z,x (in order of LSB to MSB) + static final int SECTION_Y_SHIFT = 0; + static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS; + static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS; + static final int SECTION_TO_BLOCK_SHIFT = 4; + + public static long getChunkSectionKey(final int x, final int y, final int z) { + return ((x & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getChunkSectionKey(final SectionPos pos) { + return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getChunkSectionKey(final ChunkPos pos, final int y) { + return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getChunkSectionKey(final BlockPos pos) { + return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | + ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | + (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); + } + + public static long getChunkSectionKey(final Entity entity) { + return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | + ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | + ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); + } + + public static int getChunkSectionX(final long key) { + return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS)); + } + + public static int getChunkSectionY(final long key) { + return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS)); + } + + public static int getChunkSectionZ(final long key) { + return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS)); + } + + // the block coordinates are not necessarily compatible with vanilla's + + public static int getBlockCoordinate(final double blockCoordinate) { + return Mth.floor(blockCoordinate); + } + + public static long getBlockKey(final int x, final int y, final int z) { + return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54); + } + + public static long getBlockKey(final BlockPos pos) { + return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54); + } + + public static long getBlockKey(final Entity entity) { + return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54); + } + + private CoordinateUtils() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/io/papermc/paper/util/IntegerUtil.java b/src/main/java/io/papermc/paper/util/IntegerUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..a1bc1d1d0c86217ef18883d281195bc6b27e52ac --- /dev/null +++ b/src/main/java/io/papermc/paper/util/IntegerUtil.java @@ -0,0 +1,226 @@ +package io.papermc.paper.util; + +public final class IntegerUtil { + + public static final int HIGH_BIT_U32 = Integer.MIN_VALUE; + public static final long HIGH_BIT_U64 = Long.MIN_VALUE; + + public static int ceilLog2(final int value) { + return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros + } + + public static long ceilLog2(final long value) { + return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros + } + + public static int floorLog2(final int value) { + // xor is optimized subtract for 2^n -1 + // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1) + return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros + } + + public static int floorLog2(final long value) { + // xor is optimized subtract for 2^n -1 + // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1) + return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros + } + + public static int roundCeilLog2(final int value) { + // optimized variant of 1 << (32 - leading(val - 1)) + // given + // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32) + // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1))) + // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1))) + // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1)) + // HIGH_BIT_32 >>> (-1 + leading(val - 1)) + return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1); + } + + public static long roundCeilLog2(final long value) { + // see logic documented above + return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1); + } + + public static int roundFloorLog2(final int value) { + // optimized variant of 1 << (31 - leading(val)) + // given + // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32) + // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val))) + // HIGH_BIT_32 >> (31 - (31 - leading(val))) + // HIGH_BIT_32 >> (31 - 31 + leading(val)) + return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value); + } + + public static long roundFloorLog2(final long value) { + // see logic documented above + return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value); + } + + public static boolean isPowerOfTwo(final int n) { + // 2^n has one bit + // note: this rets true for 0 still + return IntegerUtil.getTrailingBit(n) == n; + } + + public static boolean isPowerOfTwo(final long n) { + // 2^n has one bit + // note: this rets true for 0 still + return IntegerUtil.getTrailingBit(n) == n; + } + + public static int getTrailingBit(final int n) { + return -n & n; + } + + public static long getTrailingBit(final long n) { + return -n & n; + } + + public static int trailingZeros(final int n) { + return Integer.numberOfTrailingZeros(n); + } + + public static int trailingZeros(final long n) { + return Long.numberOfTrailingZeros(n); + } + + // from hacker's delight (signed division magic value) + public static int getDivisorMultiple(final long numbers) { + return (int)(numbers >>> 32); + } + + // from hacker's delight (signed division magic value) + public static int getDivisorShift(final long numbers) { + return (int)numbers; + } + + // copied from hacker's delight (signed division magic value) + // http://www.hackersdelight.org/hdcodetxt/magic.c.txt + public static long getDivisorNumbers(final int d) { + final int ad = IntegerUtil.branchlessAbs(d); + + if (ad < 2) { + throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d); + } + + final int two31 = 0x80000000; + final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour + + int p = 31; + + // all these variables are UNSIGNED! + int t = two31 + (d >>> 31); + int anc = t - 1 - t%ad; + int q1 = (int)((two31 & mask)/(anc & mask)); + int r1 = two31 - q1*anc; + int q2 = (int)((two31 & mask)/(ad & mask)); + int r2 = two31 - q2*ad; + int delta; + + do { + p = p + 1; + q1 = 2*q1; // Update q1 = 2**p/|nc|. + r1 = 2*r1; // Update r1 = rem(2**p, |nc|). + if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here) + q1 = q1 + 1; + r1 = r1 - anc; + } + q2 = 2*q2; // Update q2 = 2**p/|d|. + r2 = 2*r2; // Update r2 = rem(2**p, |d|). + if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here) + q2 = q2 + 1; + r2 = r2 - ad; + } + delta = ad - r2; + } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0)); + + int magicNum = q2 + 1; + if (d < 0) { + magicNum = -magicNum; + } + int shift = p - 32; + return ((long)magicNum << 32) | shift; + } + + public static int branchlessAbs(final int val) { + // -n = -1 ^ n + 1 + final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0 + return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 + } + + public static long branchlessAbs(final long val) { + // -n = -1 ^ n + 1 + final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0 + return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 + } + + //https://github.com/skeeto/hash-prospector for hash functions + + //score = ~590.47984224483832 + public static int hash0(int x) { + x *= 0x36935555; + x ^= x >>> 16; + return x; + } + + //score = ~310.01596637036749 + public static int hash1(int x) { + x ^= x >>> 15; + x *= 0x356aaaad; + x ^= x >>> 17; + return x; + } + + public static int hash2(int x) { + x ^= x >>> 16; + x *= 0x7feb352d; + x ^= x >>> 15; + x *= 0x846ca68b; + x ^= x >>> 16; + return x; + } + + public static int hash3(int x) { + x ^= x >>> 17; + x *= 0xed5ad4bb; + x ^= x >>> 11; + x *= 0xac4c1b51; + x ^= x >>> 15; + x *= 0x31848bab; + x ^= x >>> 14; + return x; + } + + //score = ~365.79959673201887 + public static long hash1(long x) { + x ^= x >>> 27; + x *= 0xb24924b71d2d354bL; + x ^= x >>> 28; + return x; + } + + //h2 hash + public static long hash2(long x) { + x ^= x >>> 32; + x *= 0xd6e8feb86659fd93L; + x ^= x >>> 32; + x *= 0xd6e8feb86659fd93L; + x ^= x >>> 32; + return x; + } + + public static long hash3(long x) { + x ^= x >>> 45; + x *= 0xc161abe5704b6c79L; + x ^= x >>> 41; + x *= 0xe3e5389aedbc90f7L; + x ^= x >>> 56; + x *= 0x1f9aba75a52db073L; + x ^= x >>> 53; + return x; + } + + private IntegerUtil() { + throw new RuntimeException(); + } +} 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 --- /dev/null +++ b/src/main/java/io/papermc/paper/util/IntervalledCounter.java @@ -0,0 +1,115 @@ +package io.papermc.paper.util; + +public final class IntervalledCounter { + + protected long[] times; + protected long[] counts; + protected final long interval; + protected long minTime; + protected long sum; + protected int head; // inclusive + protected int tail; // exclusive + + public IntervalledCounter(final long interval) { + this.times = new long[8]; + this.counts = new long[8]; + this.interval = interval; + } + + public void updateCurrentTime() { + this.updateCurrentTime(System.nanoTime()); + } + + public void updateCurrentTime(final long currentTime) { + long sum = this.sum; + int head = this.head; + final int tail = this.tail; + final long minTime = currentTime - this.interval; + + final int arrayLen = this.times.length; + + // guard against overflow by using subtraction + while (head != tail && this.times[head] - minTime < 0) { + sum -= this.counts[head]; + // there are two ways we can do this: + // 1. free the count when adding + // 2. free it now + // option #2 + this.counts[head] = 0; + if (++head >= arrayLen) { + head = 0; + } + } + + this.sum = sum; + this.head = head; + this.minTime = minTime; + } + + public void addTime(final long currTime) { + this.addTime(currTime, 1L); + } + + public void addTime(final long currTime, final long count) { + // guard against overflow by using subtraction + if (currTime - this.minTime < 0) { + return; + } + int nextTail = (this.tail + 1) % this.times.length; + if (nextTail == this.head) { + this.resize(); + nextTail = (this.tail + 1) % this.times.length; + } + + this.times[this.tail] = currTime; + this.counts[this.tail] += count; + this.sum += count; + this.tail = nextTail; + } + + public void updateAndAdd(final int count) { + final long currTime = System.nanoTime(); + this.updateCurrentTime(currTime); + this.addTime(currTime, count); + } + + public void updateAndAdd(final int count, final long currTime) { + this.updateCurrentTime(currTime); + this.addTime(currTime, count); + } + + private void resize() { + final long[] oldElements = this.times; + final long[] oldCounts = this.counts; + final long[] newElements = new long[this.times.length * 2]; + final long[] newCounts = new long[this.times.length * 2]; + this.times = newElements; + this.counts = newCounts; + + final int head = this.head; + final int tail = this.tail; + final int size = tail >= head ? (tail - head) : (tail + (oldElements.length - head)); + this.head = 0; + this.tail = size; + + if (tail >= head) { + System.arraycopy(oldElements, head, newElements, 0, size); + System.arraycopy(oldCounts, head, newCounts, 0, size); + } else { + System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head); + System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail); + + System.arraycopy(oldCounts, head, newCounts, 0, oldCounts.length - head); + System.arraycopy(oldCounts, 0, newCounts, oldCounts.length - head, tail); + } + } + + // returns in units per second + public double getRate() { + return this.size() / (this.interval * 1.0e-9); + } + + public long size() { + return this.sum; + } +} 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..902317d2dc198a1cbfc679810bcb2173644354cb --- /dev/null +++ b/src/main/java/io/papermc/paper/util/MCUtil.java @@ -0,0 +1,517 @@ +package io.papermc.paper.util; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.papermc.paper.math.Position; +import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; +import java.lang.ref.Cleaner; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import org.apache.commons.lang.exception.ExceptionUtils; +import org.bukkit.Location; +import org.bukkit.block.BlockFace; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.util.Waitable; +import org.spigotmc.AsyncCatcher; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public final class MCUtil { + public static final ThreadPoolExecutor asyncExecutor = new ThreadPoolExecutor( + 0, 2, 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + new ThreadFactoryBuilder() + .setNameFormat("Paper Async Task Handler Thread - %1$d") + .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(MinecraftServer.LOGGER)) + .build() + ); + public static final ThreadPoolExecutor cleanerExecutor = new ThreadPoolExecutor( + 1, 1, 0L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + new ThreadFactoryBuilder() + .setNameFormat("Paper Object Cleaner") + .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(MinecraftServer.LOGGER)) + .build() + ); + + public static final long INVALID_CHUNK_KEY = getCoordinateKey(Integer.MAX_VALUE, Integer.MAX_VALUE); + + + public static Runnable once(Runnable run) { + AtomicBoolean ran = new AtomicBoolean(false); + return () -> { + if (ran.compareAndSet(false, true)) { + run.run(); + } + }; + } + + public static Runnable once(List list, Consumer cb) { + return once(() -> { + list.forEach(cb); + }); + } + + private static Runnable makeCleanerCallback(Runnable run) { + return once(() -> cleanerExecutor.execute(run)); + } + + /** + * DANGER WILL ROBINSON: Be sure you do not use a lambda that lives in the object being monitored, or leaky leaky! + * @param obj + * @param run + * @return + */ + public static Runnable registerCleaner(Object obj, Runnable run) { + // Wrap callback in its own method above or the lambda will leak object + Runnable cleaner = makeCleanerCallback(run); + CleanerHolder.CLEANER.register(obj, cleaner); + return cleaner; + } + + private static final class CleanerHolder { + private static final Cleaner CLEANER = Cleaner.create(); + } + + /** + * DANGER WILL ROBINSON: Be sure you do not use a lambda that lives in the object being monitored, or leaky leaky! + * @param obj + * @param list + * @param cleaner + * @param + * @return + */ + public static Runnable registerListCleaner(Object obj, List list, Consumer cleaner) { + return registerCleaner(obj, () -> { + list.forEach(cleaner); + list.clear(); + }); + } + + /** + * DANGER WILL ROBINSON: Be sure you do not use a lambda that lives in the object being monitored, or leaky leaky! + * @param obj + * @param resource + * @param cleaner + * @param + * @return + */ + public static Runnable registerCleaner(Object obj, T resource, java.util.function.Consumer cleaner) { + return registerCleaner(obj, () -> cleaner.accept(resource)); + } + + public static List getSpiralOutChunks(BlockPos blockposition, int radius) { + List list = com.google.common.collect.Lists.newArrayList(); + + list.add(new ChunkPos(blockposition.getX() >> 4, blockposition.getZ() >> 4)); + for (int r = 1; r <= radius; r++) { + int x = -r; + int z = r; + + // Iterates the edge of half of the box; then negates for other half. + while (x <= r && z > -r) { + list.add(new ChunkPos((blockposition.getX() + (x << 4)) >> 4, (blockposition.getZ() + (z << 4)) >> 4)); + list.add(new ChunkPos((blockposition.getX() - (x << 4)) >> 4, (blockposition.getZ() - (z << 4)) >> 4)); + + if (x < r) { + x++; + } else { + z--; + } + } + } + return list; + } + + public static int fastFloor(double x) { + int truncated = (int)x; + return x < (double)truncated ? truncated - 1 : truncated; + } + + public static int fastFloor(float x) { + int truncated = (int)x; + return x < (double)truncated ? truncated - 1 : truncated; + } + + public static float normalizeYaw(float f) { + float f1 = f % 360.0F; + + if (f1 >= 180.0F) { + f1 -= 360.0F; + } + + if (f1 < -180.0F) { + f1 += 360.0F; + } + + return f1; + } + + /** + * Quickly generate a stack trace for current location + * + * @return Stacktrace + */ + public static String stack() { + return ExceptionUtils.getFullStackTrace(new Throwable()); + } + + /** + * Quickly generate a stack trace for current location with message + * + * @param str + * @return Stacktrace + */ + public static String stack(String str) { + return ExceptionUtils.getFullStackTrace(new Throwable(str)); + } + + public static long getCoordinateKey(final BlockPos blockPos) { + return ((long)(blockPos.getZ() >> 4) << 32) | ((blockPos.getX() >> 4) & 0xFFFFFFFFL); + } + + public static long getCoordinateKey(final Entity entity) { + return ((long)(MCUtil.fastFloor(entity.getZ()) >> 4) << 32) | ((MCUtil.fastFloor(entity.getX()) >> 4) & 0xFFFFFFFFL); + } + + public static long getCoordinateKey(final ChunkPos pair) { + return ((long)pair.z << 32) | (pair.x & 0xFFFFFFFFL); + } + + public static long getCoordinateKey(final int x, final int z) { + return ((long)z << 32) | (x & 0xFFFFFFFFL); + } + + public static int getCoordinateX(final long key) { + return (int)key; + } + + public static int getCoordinateZ(final long key) { + return (int)(key >>> 32); + } + + public static int getChunkCoordinate(final double coordinate) { + return MCUtil.fastFloor(coordinate) >> 4; + } + + public static int getBlockCoordinate(final double coordinate) { + return MCUtil.fastFloor(coordinate); + } + + public static long getBlockKey(final int x, final int y, final int z) { + return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54); + } + + public static long getBlockKey(final BlockPos pos) { + return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54); + } + + public static long getBlockKey(final Entity entity) { + return getBlockKey(getBlockCoordinate(entity.getX()), getBlockCoordinate(entity.getY()), getBlockCoordinate(entity.getZ())); + } + + // assumes the sets have the same comparator, and if this comparator is null then assume T is Comparable + public static void mergeSortedSets(final java.util.function.Consumer consumer, final java.util.Comparator comparator, final java.util.SortedSet...sets) { + final ObjectRBTreeSet all = new ObjectRBTreeSet<>(comparator); + // note: this is done in log(n!) ~ nlogn time. It could be improved if it were to mimic what mergesort does. + for (java.util.SortedSet set : sets) { + if (set != null) { + all.addAll(set); + } + } + all.forEach(consumer); + } + + private MCUtil() {} + + public static final java.util.concurrent.Executor MAIN_EXECUTOR = (run) -> { + if (!isMainThread()) { + MinecraftServer.getServer().execute(run); + } else { + run.run(); + } + }; + + public static CompletableFuture ensureMain(CompletableFuture future) { + return future.thenApplyAsync(r -> r, MAIN_EXECUTOR); + } + + public static void thenOnMain(CompletableFuture future, Consumer consumer) { + future.thenAcceptAsync(consumer, MAIN_EXECUTOR); + } + public static void thenOnMain(CompletableFuture future, BiConsumer consumer) { + future.whenCompleteAsync(consumer, MAIN_EXECUTOR); + } + + public static boolean isMainThread() { + return MinecraftServer.getServer().isSameThread(); + } + + public static org.bukkit.scheduler.BukkitTask scheduleTask(int ticks, Runnable runnable) { + return scheduleTask(ticks, runnable, null); + } + + public static org.bukkit.scheduler.BukkitTask scheduleTask(int ticks, Runnable runnable, String taskName) { + return MinecraftServer.getServer().server.getScheduler().scheduleInternalTask(runnable, ticks, taskName); + } + + public static void processQueue() { + Runnable runnable; + Queue processQueue = getProcessQueue(); + while ((runnable = processQueue.poll()) != null) { + try { + runnable.run(); + } catch (Exception e) { + MinecraftServer.LOGGER.error("Error executing task", e); + } + } + } + public static T processQueueWhileWaiting(CompletableFuture future) { + try { + if (isMainThread()) { + while (!future.isDone()) { + try { + return future.get(1, TimeUnit.MILLISECONDS); + } catch (TimeoutException ignored) { + processQueue(); + } + } + } + return future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void ensureMain(Runnable run) { + ensureMain(null, run); + } + /** + * Ensures the target code is running on the main thread + * @param reason + * @param run + * @return + */ + public static void ensureMain(String reason, Runnable run) { + if (!isMainThread()) { + if (reason != null) { + MinecraftServer.LOGGER.warn("Asynchronous " + reason + "!", new IllegalStateException()); + } + getProcessQueue().add(run); + return; + } + run.run(); + } + + private static Queue getProcessQueue() { + return MinecraftServer.getServer().processQueue; + } + + public static T ensureMain(Supplier run) { + return ensureMain(null, run); + } + /** + * Ensures the target code is running on the main thread + * @param reason + * @param run + * @param + * @return + */ + public static T ensureMain(String reason, Supplier run) { + if (!isMainThread()) { + if (reason != null) { + MinecraftServer.LOGGER.warn("Asynchronous " + reason + "! Blocking thread until it returns ", new IllegalStateException()); + } + Waitable wait = new Waitable() { + @Override + protected T evaluate() { + return run.get(); + } + }; + getProcessQueue().add(wait); + try { + return wait.get(); + } catch (InterruptedException | ExecutionException e) { + MinecraftServer.LOGGER.warn("Encountered exception", e); + } + return null; + } + return run.get(); + } + + /** + * Calculates distance between 2 entities + * @param e1 + * @param e2 + * @return + */ + public static double distance(Entity e1, Entity e2) { + return Math.sqrt(distanceSq(e1, e2)); + } + + + /** + * Calculates distance between 2 block positions + * @param e1 + * @param e2 + * @return + */ + public static double distance(BlockPos e1, BlockPos e2) { + return Math.sqrt(distanceSq(e1, e2)); + } + + /** + * Gets the distance between 2 positions + * @param x1 + * @param y1 + * @param z1 + * @param x2 + * @param y2 + * @param z2 + * @return + */ + public static double distance(double x1, double y1, double z1, double x2, double y2, double z2) { + return Math.sqrt(distanceSq(x1, y1, z1, x2, y2, z2)); + } + + /** + * Get's the distance squared between 2 entities + * @param e1 + * @param e2 + * @return + */ + public static double distanceSq(Entity e1, Entity e2) { + return distanceSq(e1.getX(),e1.getY(),e1.getZ(), e2.getX(),e2.getY(),e2.getZ()); + } + + /** + * Gets the distance sqaured between 2 block positions + * @param pos1 + * @param pos2 + * @return + */ + public static double distanceSq(BlockPos pos1, BlockPos pos2) { + return distanceSq(pos1.getX(), pos1.getY(), pos1.getZ(), pos2.getX(), pos2.getY(), pos2.getZ()); + } + + /** + * Gets the distance squared between 2 positions + * @param x1 + * @param y1 + * @param z1 + * @param x2 + * @param y2 + * @param z2 + * @return + */ + public static double distanceSq(double x1, double y1, double z1, double x2, double y2, double z2) { + return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2); + } + + /** + * Converts a NMS World/BlockPosition to Bukkit Location + * @param world + * @param x + * @param y + * @param z + * @return + */ + public static Location toLocation(Level world, double x, double y, double z) { + return new Location(world.getWorld(), x, y, z); + } + + /** + * Converts a NMS World/BlockPosition to Bukkit Location + * @param world + * @param pos + * @return + */ + public static Location toLocation(Level world, BlockPos pos) { + return new Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ()); + } + + /** + * Converts an NMS entity's current location to a Bukkit Location + * @param entity + * @return + */ + public static Location toLocation(Entity entity) { + return new Location(entity.getCommandSenderWorld().getWorld(), entity.getX(), entity.getY(), entity.getZ()); + } + + public static org.bukkit.block.Block toBukkitBlock(Level world, BlockPos pos) { + return world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()); + } + + public static BlockPos toBlockPosition(Location loc) { + return new BlockPos(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + } + + public static BlockPos toBlockPos(Position pos) { + return new BlockPos(pos.blockX(), pos.blockY(), pos.blockZ()); + } + + public static boolean isEdgeOfChunk(BlockPos pos) { + final int modX = pos.getX() & 15; + final int modZ = pos.getZ() & 15; + return (modX == 0 || modX == 15 || modZ == 0 || modZ == 15); + } + + /** + * Posts a task to be executed asynchronously + * @param run + */ + public static void scheduleAsyncTask(Runnable run) { + asyncExecutor.execute(run); + } + + @Nonnull + public static ServerLevel getNMSWorld(@Nonnull org.bukkit.World world) { + return ((CraftWorld) world).getHandle(); + } + + public static ServerLevel getNMSWorld(@Nonnull org.bukkit.entity.Entity entity) { + return getNMSWorld(entity.getWorld()); + } + + public static BlockFace toBukkitBlockFace(Direction enumDirection) { + switch (enumDirection) { + case DOWN: + return BlockFace.DOWN; + case UP: + return BlockFace.UP; + case NORTH: + return BlockFace.NORTH; + case SOUTH: + return BlockFace.SOUTH; + case WEST: + return BlockFace.WEST; + case EAST: + return BlockFace.EAST; + default: + return null; + } + } + + public static int getTicketLevelFor(net.minecraft.world.level.chunk.ChunkStatus status) { + return net.minecraft.server.level.ChunkMap.MAX_VIEW_DISTANCE + net.minecraft.world.level.chunk.ChunkStatus.getDistance(status); + } +} diff --git a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..f7114d5b8f2f93f62883e24da29afaf9f74ee1a6 --- /dev/null +++ b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java @@ -0,0 +1,24 @@ +package io.papermc.paper.util; + +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.plugin.java.PluginClassLoader; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +public class StackWalkerUtil { + + @Nullable + public static JavaPlugin getFirstPluginCaller() { + Optional foundFrame = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(stream -> stream + .filter(frame -> frame.getDeclaringClass().getClassLoader() instanceof PluginClassLoader) + .map((frame) -> { + PluginClassLoader classLoader = (PluginClassLoader) frame.getDeclaringClass().getClassLoader(); + return classLoader.getPlugin(); + }) + .findFirst()); + + return foundFrame.orElse(null); + } +} diff --git a/src/main/java/io/papermc/paper/util/WorldUtil.java b/src/main/java/io/papermc/paper/util/WorldUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..67bb91fcfb532a919954cd9d7733d09a6c3fec35 --- /dev/null +++ b/src/main/java/io/papermc/paper/util/WorldUtil.java @@ -0,0 +1,46 @@ +package io.papermc.paper.util; + +import net.minecraft.world.level.LevelHeightAccessor; + +public final class WorldUtil { + + // min, max are inclusive + + public static int getMaxSection(final LevelHeightAccessor world) { + return world.getMaxSection() - 1; // getMaxSection() is exclusive + } + + public static int getMinSection(final LevelHeightAccessor world) { + return world.getMinSection(); + } + + public static int getMaxLightSection(final LevelHeightAccessor world) { + return getMaxSection(world) + 1; + } + + public static int getMinLightSection(final LevelHeightAccessor world) { + return getMinSection(world) - 1; + } + + + + public static int getTotalSections(final LevelHeightAccessor world) { + return getMaxSection(world) - getMinSection(world) + 1; + } + + public static int getTotalLightSections(final LevelHeightAccessor world) { + return getMaxLightSection(world) - getMinLightSection(world) + 1; + } + + public static int getMinBlockY(final LevelHeightAccessor world) { + return getMinSection(world) << 4; + } + + public static int getMaxBlockY(final LevelHeightAccessor world) { + return (getMaxSection(world) << 4) | 15; + } + + private WorldUtil() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java b/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java new file mode 100644 index 0000000000000000000000000000000000000000..0fd814f1d65c111266a2b20f86561839a4cef755 --- /dev/null +++ b/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java @@ -0,0 +1,334 @@ +package io.papermc.paper.util.maplist; + +import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import org.bukkit.Bukkit; +import java.util.Arrays; +import java.util.NoSuchElementException; + +public final class IteratorSafeOrderedReferenceSet { + + public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0; + + protected final Reference2IntLinkedOpenHashMap indexMap; + protected int firstInvalidIndex = -1; + + /* list impl */ + protected E[] listElements; + protected int listSize; + + protected final double maxFragFactor; + + protected int iteratorCount; + + private final boolean threadRestricted; + + public IteratorSafeOrderedReferenceSet() { + this(16, 0.75f, 16, 0.2); + } + + public IteratorSafeOrderedReferenceSet(final boolean threadRestricted) { + this(16, 0.75f, 16, 0.2, threadRestricted); + } + + public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity, + final double maxFragFactor) { + this(setCapacity, setLoadFactor, arrayCapacity, maxFragFactor, false); + } + public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity, + final double maxFragFactor, final boolean threadRestricted) { + this.indexMap = new Reference2IntLinkedOpenHashMap<>(setCapacity, setLoadFactor); + this.indexMap.defaultReturnValue(-1); + this.maxFragFactor = maxFragFactor; + this.listElements = (E[])new Object[arrayCapacity]; + this.threadRestricted = threadRestricted; + } + + /* + public void check() { + int iterated = 0; + ReferenceOpenHashSet check = new ReferenceOpenHashSet<>(); + if (this.listElements != null) { + for (int i = 0; i < this.listSize; ++i) { + Object obj = this.listElements[i]; + if (obj != null) { + iterated++; + if (!check.add((E)obj)) { + throw new IllegalStateException("contains duplicate"); + } + if (!this.contains((E)obj)) { + throw new IllegalStateException("desync"); + } + } + } + } + + if (iterated != this.size()) { + throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size()); + } + + check.clear(); + iterated = 0; + for (final java.util.Iterator iterator = this.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final E element = iterator.next(); + iterated++; + if (!check.add(element)) { + throw new IllegalStateException("contains duplicate (iterator is wrong)"); + } + if (!this.contains(element)) { + throw new IllegalStateException("desync (iterator is wrong)"); + } + } + + if (iterated != this.size()) { + throw new IllegalStateException("Size is mismatched! (iterator is wrong) Got " + iterated + ", expected " + this.size()); + } + } + */ + + protected final boolean allowSafeIteration() { + return !this.threadRestricted || Bukkit.isPrimaryThread(); + } + + protected final double getFragFactor() { + return 1.0 - ((double)this.indexMap.size() / (double)this.listSize); + } + + public int createRawIterator() { + if (this.allowSafeIteration()) { + ++this.iteratorCount; + } + if (this.indexMap.isEmpty()) { + return -1; + } else { + return this.firstInvalidIndex == 0 ? this.indexMap.getInt(this.indexMap.firstKey()) : 0; + } + } + + public int advanceRawIterator(final int index) { + final E[] elements = this.listElements; + int ret = index + 1; + for (int len = this.listSize; ret < len; ++ret) { + if (elements[ret] != null) { + return ret; + } + } + + return -1; + } + + public void finishRawIterator() { + if (this.allowSafeIteration() && --this.iteratorCount == 0) { + if (this.getFragFactor() >= this.maxFragFactor) { + this.defrag(); + } + } + } + + public boolean remove(final E element) { + final int index = this.indexMap.removeInt(element); + if (index >= 0) { + if (this.firstInvalidIndex < 0 || index < this.firstInvalidIndex) { + this.firstInvalidIndex = index; + } + if (this.listElements[index] != element) { + throw new IllegalStateException(); + } + this.listElements[index] = null; + if (this.allowSafeIteration() && this.iteratorCount == 0 && this.getFragFactor() >= this.maxFragFactor) { + this.defrag(); + } + //this.check(); + return true; + } + return false; + } + + public boolean contains(final E element) { + return this.indexMap.containsKey(element); + } + + public boolean add(final E element) { + final int listSize = this.listSize; + + final int previous = this.indexMap.putIfAbsent(element, listSize); + if (previous != -1) { + return false; + } + + if (listSize >= this.listElements.length) { + this.listElements = Arrays.copyOf(this.listElements, listSize * 2); + } + this.listElements[listSize] = element; + this.listSize = listSize + 1; + + //this.check(); + return true; + } + + protected void defrag() { + if (this.firstInvalidIndex < 0) { + return; // nothing to do + } + + if (this.indexMap.isEmpty()) { + Arrays.fill(this.listElements, 0, this.listSize, null); + this.listSize = 0; + this.firstInvalidIndex = -1; + //this.check(); + return; + } + + final E[] backingArray = this.listElements; + + int lastValidIndex; + java.util.Iterator> iterator; + + if (this.firstInvalidIndex == 0) { + iterator = this.indexMap.reference2IntEntrySet().fastIterator(); + lastValidIndex = 0; + } else { + lastValidIndex = this.firstInvalidIndex; + final E key = backingArray[lastValidIndex - 1]; + iterator = this.indexMap.reference2IntEntrySet().fastIterator(new Reference2IntMap.Entry() { + @Override + public int getIntValue() { + throw new UnsupportedOperationException(); + } + + @Override + public int setValue(int i) { + throw new UnsupportedOperationException(); + } + + @Override + public E getKey() { + return key; + } + }); + } + + while (iterator.hasNext()) { + final Reference2IntMap.Entry entry = iterator.next(); + + final int newIndex = lastValidIndex++; + backingArray[newIndex] = entry.getKey(); + entry.setValue(newIndex); + } + + // cleanup end + Arrays.fill(backingArray, lastValidIndex, this.listSize, null); + this.listSize = lastValidIndex; + this.firstInvalidIndex = -1; + //this.check(); + } + + public E rawGet(final int index) { + return this.listElements[index]; + } + + public int size() { + // always returns the correct amount - listSize can be different + return this.indexMap.size(); + } + + public IteratorSafeOrderedReferenceSet.Iterator iterator() { + return this.iterator(0); + } + + public IteratorSafeOrderedReferenceSet.Iterator iterator(final int flags) { + if (this.allowSafeIteration()) { + ++this.iteratorCount; + } + return new BaseIterator<>(this, true, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize); + } + + public java.util.Iterator unsafeIterator() { + return this.unsafeIterator(0); + } + public java.util.Iterator unsafeIterator(final int flags) { + return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize); + } + + public static interface Iterator extends java.util.Iterator { + + public void finishedIterating(); + + } + + protected static final class BaseIterator implements IteratorSafeOrderedReferenceSet.Iterator { + + protected final IteratorSafeOrderedReferenceSet set; + protected final boolean canFinish; + protected final int maxIndex; + protected int nextIndex; + protected E pendingValue; + protected boolean finished; + protected E lastReturned; + + protected BaseIterator(final IteratorSafeOrderedReferenceSet set, final boolean canFinish, final int maxIndex) { + this.set = set; + this.canFinish = canFinish; + this.maxIndex = maxIndex; + } + + @Override + public boolean hasNext() { + if (this.finished) { + return false; + } + if (this.pendingValue != null) { + return true; + } + + final E[] elements = this.set.listElements; + int index, len; + for (index = this.nextIndex, len = Math.min(this.maxIndex, this.set.listSize); index < len; ++index) { + final E element = elements[index]; + if (element != null) { + this.pendingValue = element; + this.nextIndex = index + 1; + return true; + } + } + + this.nextIndex = index; + return false; + } + + @Override + public E next() { + if (!this.hasNext()) { + throw new NoSuchElementException(); + } + final E ret = this.pendingValue; + + this.pendingValue = null; + this.lastReturned = ret; + + return ret; + } + + @Override + public void remove() { + final E lastReturned = this.lastReturned; + if (lastReturned == null) { + throw new IllegalStateException(); + } + this.lastReturned = null; + this.set.remove(lastReturned); + } + + @Override + public void finishedIterating() { + if (this.finished || !this.canFinish) { + throw new IllegalStateException(); + } + this.lastReturned = null; + this.finished = true; + if (this.set.allowSafeIteration()) { + this.set.finishRawIterator(); + } + } + } +} 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 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 empty + = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this); + + } + + public static void main(final String[] args) { + com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap() { + @Override + protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet 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 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 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/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java index b3e0495e0f242c96d4348438c0257c2045b801e5..c5fb6adb353538360ef420faee41565626eea1dc 100644 --- a/src/main/java/net/minecraft/Util.java +++ b/src/main/java/net/minecraft/Util.java @@ -116,7 +116,7 @@ public class Util { } public static long getNanos() { - return timeSource.getAsLong(); + return System.nanoTime(); // Paper } public static long getEpochMillis() { diff --git a/src/main/java/net/minecraft/WorldVersion.java b/src/main/java/net/minecraft/WorldVersion.java index 1357d732b0ee6ef923e537aad28d4ef6af18ab50..a3c526b6478508e3faa759c2e03e14c13a98e15b 100644 --- a/src/main/java/net/minecraft/WorldVersion.java +++ b/src/main/java/net/minecraft/WorldVersion.java @@ -6,6 +6,11 @@ import net.minecraft.world.level.storage.DataVersion; public interface WorldVersion { DataVersion getDataVersion(); + // Paper start + default int getWorldVersion() { + return this.getDataVersion().getVersion(); + } + // Paper end String getId(); diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java index 819562d2c938fa05b8e8a00d1ae1f7c1fc9b00d5..4dffce4dc3434ef6adef7dc3cfac867ad89d9a5d 100644 --- a/src/main/java/net/minecraft/core/BlockPos.java +++ b/src/main/java/net/minecraft/core/BlockPos.java @@ -521,6 +521,7 @@ public class BlockPos extends Vec3i { } } + // Paper start - comment out useless overrides @Override - TODO figure out why this is suddenly important to keep @Override public BlockPos.MutableBlockPos setX(int i) { super.setX(i); @@ -538,6 +539,7 @@ public class BlockPos extends Vec3i { super.setZ(i); return this; } + // Paper end @Override public BlockPos immutable() { diff --git a/src/main/java/net/minecraft/nbt/CompoundTag.java b/src/main/java/net/minecraft/nbt/CompoundTag.java index 29664f7f44ad7dec9cbccd276a14937ca1c4a3bb..64765dab6fed87ffdf4af197d8d5f28a04544db0 100644 --- a/src/main/java/net/minecraft/nbt/CompoundTag.java +++ b/src/main/java/net/minecraft/nbt/CompoundTag.java @@ -123,7 +123,7 @@ public class CompoundTag implements Tag { return "TAG_Compound"; } }; - private final Map tags; + public final Map tags; // Paper protected CompoundTag(Map entries) { this.tags = entries; @@ -199,6 +199,10 @@ public class CompoundTag implements Tag { this.tags.put(key, NbtUtils.createUUID(value)); } + + /** + * You must use {@link #hasUUID(String)} before or else it will throw an NPE. + */ public UUID getUUID(String key) { return NbtUtils.loadUUID(this.get(key)); } diff --git a/src/main/java/net/minecraft/network/PacketEncoder.java b/src/main/java/net/minecraft/network/PacketEncoder.java index e6c4379b0fd7c1338e1713281cd9515cb54acecb..a63e7ee5c42bd51312155feab31c6ec4232e1bc7 100644 --- a/src/main/java/net/minecraft/network/PacketEncoder.java +++ b/src/main/java/net/minecraft/network/PacketEncoder.java @@ -45,7 +45,7 @@ public class PacketEncoder extends MessageToByteEncoder> { JvmProfiler.INSTANCE.onPacketSent(l, i, channelHandlerContext.channel().remoteAddress(), k); } } catch (Throwable var10) { - LOGGER.error("Error receiving packet {}", i, var10); + LOGGER.error("Packet encoding of packet ID {} threw (skippable? {})", i, packet.isSkippable(), var10); // Paper - Give proper error message if (packet.isSkippable()) { throw new SkipPacketException(var10); } else { diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 41a6756144a3b826d32ecb85a71d26761e25ec11..5725631835ea68802c75934cd85d5c1b1a78d358 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -296,6 +296,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { AtomicReference atomicreference = new AtomicReference(); @@ -931,6 +932,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop>> futures; private final LevelHeightAccessor levelHeightAccessor; - private volatile CompletableFuture> fullChunkFuture; - private volatile CompletableFuture> tickingChunkFuture; - private volatile CompletableFuture> entityTickingChunkFuture; + private volatile CompletableFuture> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage + private volatile CompletableFuture> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage + private volatile CompletableFuture> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage private CompletableFuture chunkToSave; @Nullable private final DebugBuffer chunkToSaveHistory; @@ -73,6 +73,18 @@ public class ChunkHolder { private boolean resendLight; private CompletableFuture pendingFullStateConfirmation; + private final ChunkMap chunkMap; // Paper + + // Paper start + public void onChunkAdd() { + + } + + public void onChunkRemove() { + + } + // Paper end + public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) { this.futures = new AtomicReferenceArray(ChunkHolder.CHUNK_STATUSES.size()); this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; @@ -93,8 +105,23 @@ public class ChunkHolder { this.queueLevel = this.oldTicketLevel; this.setTicketLevel(level); this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()]; + this.chunkMap = (ChunkMap)playersWatchingChunkProvider; // Paper } + // Paper start + public @Nullable ChunkAccess getAvailableChunkNow() { + // TODO can we just getStatusFuture(EMPTY)? + for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) { + CompletableFuture> future = this.getFutureIfPresentUnchecked(curr); + Either either = future.getNow(null); + if (either == null || either.left().isEmpty()) { + continue; + } + return either.left().get(); + } + return null; + } + // Paper end // CraftBukkit start public LevelChunk getFullChunkNow() { // Note: We use the oldTicketLevel for isLoaded checks. @@ -119,20 +146,20 @@ public class ChunkHolder { return ChunkHolder.getStatus(this.ticketLevel).isOrAfter(leastStatus) ? this.getFutureIfPresentUnchecked(leastStatus) : ChunkHolder.UNLOADED_CHUNK_FUTURE; } - public CompletableFuture> getTickingChunkFuture() { + public final CompletableFuture> getTickingChunkFuture() { // Paper - final for inline return this.tickingChunkFuture; } - public CompletableFuture> getEntityTickingChunkFuture() { + public final CompletableFuture> getEntityTickingChunkFuture() { // Paper - final for inline return this.entityTickingChunkFuture; } - public CompletableFuture> getFullChunkFuture() { + public final CompletableFuture> getFullChunkFuture() { // Paper - final for inline return this.fullChunkFuture; } @Nullable - public LevelChunk getTickingChunk() { + public final LevelChunk getTickingChunk() { // Paper - final for inline CompletableFuture> completablefuture = this.getTickingChunkFuture(); Either either = (Either) completablefuture.getNow(null); // CraftBukkit - decompile error @@ -140,7 +167,7 @@ public class ChunkHolder { } @Nullable - public LevelChunk getFullChunk() { + public final LevelChunk getFullChunk() { // Paper - final for inline CompletableFuture> completablefuture = this.getFullChunkFuture(); Either either = (Either) completablefuture.getNow(null); // CraftBukkit - decompile error @@ -161,6 +188,21 @@ public class ChunkHolder { return null; } + // Paper start + public ChunkStatus getChunkHolderStatus() { + for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) { + CompletableFuture> future = this.getFutureIfPresentUnchecked(curr); + Either either = future.getNow(null); + if (either == null || !either.left().isPresent()) { + continue; + } + return curr; + } + + return null; + } + // Paper end + @Nullable public ChunkAccess getLastAvailable() { for (int i = ChunkHolder.CHUNK_STATUSES.size() - 1; i >= 0; --i) { @@ -179,7 +221,7 @@ public class ChunkHolder { return null; } - public CompletableFuture getChunkToSave() { + public final CompletableFuture getChunkToSave() { // Paper - final for inline return this.chunkToSave; } @@ -360,11 +402,11 @@ public class ChunkHolder { return ChunkHolder.getFullChunkStatus(this.ticketLevel); } - public ChunkPos getPos() { + public final ChunkPos getPos() { // Paper - final for inline return this.pos; } - public int getTicketLevel() { + public final int getTicketLevel() { // Paper - final for inline return this.ticketLevel; } @@ -453,14 +495,31 @@ public class ChunkHolder { this.wasAccessibleSinceLastSave |= flag3; if (!flag2 && flag3) { + int expectCreateCount = ++this.fullChunkCreateCount; // Paper this.fullChunkFuture = chunkStorage.prepareAccessibleChunk(this); this.scheduleFullChunkPromotion(chunkStorage, this.fullChunkFuture, executor, ChunkHolder.FullChunkStatus.BORDER); + // Paper start - cache ticking ready status + this.fullChunkFuture.thenAccept(either -> { + final Optional left = either.left(); + if (left.isPresent() && ChunkHolder.this.fullChunkCreateCount == expectCreateCount) { + LevelChunk fullChunk = either.left().get(); + ChunkHolder.this.isFullChunkReady = true; + io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(fullChunk, this); + } + }); this.updateChunkToSave(this.fullChunkFuture, "full"); } if (flag2 && !flag3) { + // Paper start + if (this.isFullChunkReady) { + io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().left().get(), this); // Paper + } + // Paper end this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; + ++this.fullChunkCreateCount; // Paper - cache ticking ready status + this.isFullChunkReady = false; // Paper - cache ticking ready status } boolean flag4 = playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.TICKING); @@ -469,11 +528,25 @@ public class ChunkHolder { if (!flag4 && flag5) { this.tickingChunkFuture = chunkStorage.prepareTickingChunk(this); this.scheduleFullChunkPromotion(chunkStorage, this.tickingChunkFuture, executor, ChunkHolder.FullChunkStatus.TICKING); + // Paper start - cache ticking ready status + this.tickingChunkFuture.thenAccept(either -> { + either.ifLeft(chunk -> { + // note: Here is a very good place to add callbacks to logic waiting on this. + ChunkHolder.this.isTickingReady = true; + io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(chunk, this); + }); + }); + // Paper end this.updateChunkToSave(this.tickingChunkFuture, "ticking"); } if (flag4 && !flag5) { - this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); + // Paper start + if (this.isTickingReady) { + io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().left().get(), this); // Paper + } + // Paper end + this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; } @@ -487,11 +560,24 @@ public class ChunkHolder { this.entityTickingChunkFuture = chunkStorage.prepareEntityTickingChunk(this.pos); this.scheduleFullChunkPromotion(chunkStorage, this.entityTickingChunkFuture, executor, ChunkHolder.FullChunkStatus.ENTITY_TICKING); + // Paper start - cache ticking ready status + this.entityTickingChunkFuture.thenAccept(either -> { + either.ifLeft(chunk -> { + ChunkHolder.this.isEntityTickingReady = true; + io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(chunk, this); + }); + }); + // Paper end this.updateChunkToSave(this.entityTickingChunkFuture, "entity ticking"); } if (flag6 && !flag7) { - this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); + // Paper start + if (this.isEntityTickingReady) { + io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().left().get(), this); + } + // Paper end + this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; } @@ -608,4 +694,18 @@ public class ChunkHolder { } }; } + + // Paper start + public final boolean isEntityTickingReady() { + return this.isEntityTickingReady; + } + + public final boolean isTickingReady() { + return this.isTickingReady; + } + + public final boolean isFullChunkReady() { + return this.isFullChunkReady; + } + // 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 9fe2d44fbfb648c342daf0b8f0820090785b8bf7..714b36e4942fda9d6c8a202b9e7a34ef67d3d13c 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -68,6 +68,7 @@ import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket; import net.minecraft.network.protocol.game.ClientboundSetEntityLinkPacket; import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket; import net.minecraft.network.protocol.game.DebugPackets; +import io.papermc.paper.util.MCUtil; import net.minecraft.server.level.progress.ChunkProgressListener; import net.minecraft.server.network.ServerPlayerConnection; import net.minecraft.util.CsvOutput; @@ -176,6 +177,56 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider }; // CraftBukkit end + // Paper start - distance maps + private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>(); + + void addPlayerToDistanceMaps(ServerPlayer player) { + int chunkX = MCUtil.getChunkCoordinate(player.getX()); + int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); + // Note: players need to be explicitly added to distance maps before they can be updated + } + + void removePlayerFromDistanceMaps(ServerPlayer player) { + + } + + void updateMaps(ServerPlayer player) { + int chunkX = MCUtil.getChunkCoordinate(player.getX()); + int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); + // Note: players need to be explicitly added to distance maps before they can be updated + } + // Paper end + // Paper start + public final List 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)); + } + // Paper end + public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory, int viewDistance, boolean dsync) { super(session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync); this.visibleChunkMap = this.updatingChunkMap.clone(); @@ -229,6 +280,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.overworldDataStorage = persistentStateManagerFactory; this.poiManager = new PoiManager(path.resolve("poi"), dataFixer, dsync, iregistrycustom, world); this.setViewDistance(viewDistance); + // 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); + // Paper end } protected ChunkGenerator generator() { @@ -326,6 +381,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } } + // Paper start + public final int getEffectiveViewDistance() { + // TODO this needs to be checked on update + // Mojang currently sets it to +1 of the configured view distance. So subtract one to get the one we really want. + return this.viewDistance - 1; + } + // Paper end + private CompletableFuture, ChunkHolder.ChunkLoadingFailure>> getChunkRangeFuture(ChunkPos centerChunk, int margin, IntFunction distanceToStatus) { List>> list = new ArrayList(); List list1 = new ArrayList(); @@ -413,9 +476,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider }; stringbuilder.append("Updating:").append(System.lineSeparator()); - this.updatingChunkMap.values().forEach(consumer); + io.papermc.paper.chunk.system.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper stringbuilder.append("Visible:").append(System.lineSeparator()); - this.visibleChunkMap.values().forEach(consumer); + io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(consumer); // Paper CrashReport crashreport = CrashReport.forThrowable(exception, "Chunk loading"); CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk loading"); @@ -457,8 +520,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); + // Paper start + io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderCreate(this.level, holder); + // Paper end } + // Paper start + holder.onChunkAdd(); + // Paper end this.updatingChunkMap.put(pos, holder); this.modified = true; } @@ -480,7 +549,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider protected void saveAllChunks(boolean flush) { if (flush) { - List list = (List) this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList()); + List list = (List) io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList()); // Paper MutableBoolean mutableboolean = new MutableBoolean(); do { @@ -509,7 +578,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider }); this.flushWorker(); } else { - this.visibleChunkMap.values().forEach(this::saveChunkIfNeeded); + io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded); } } @@ -528,7 +597,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } public boolean hasWork() { - return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); + return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || io.papermc.paper.chunk.system.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper } private void processUnloads(BooleanSupplier shouldKeepTicking) { @@ -539,6 +608,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j); if (playerchunk != null) { + playerchunk.onChunkRemove(); // Paper this.pendingUnloads.put(j, playerchunk); this.modified = true; ++i; @@ -556,7 +626,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } int l = 0; - ObjectIterator objectiterator = this.visibleChunkMap.values().iterator(); + Iterator objectiterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) { if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) { @@ -574,7 +644,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (completablefuture1 != completablefuture) { this.scheduleUnload(pos, holder); } else { - if (this.pendingUnloads.remove(pos, holder) && ichunkaccess != null) { + // Paper start + boolean removed; + if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) { + io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder); + // Paper end if (ichunkaccess instanceof LevelChunk) { ((LevelChunk) ichunkaccess).setLoaded(false); } @@ -590,7 +664,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()); - } + } else if (removed) { // Paper start + io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder); + } // Paper end } }; @@ -971,7 +1047,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.viewDistance = j; this.distanceManager.updatePlayerTickets(this.viewDistance + 1); - ObjectIterator objectiterator = this.updatingChunkMap.values().iterator(); + Iterator objectiterator = io.papermc.paper.chunk.system.ChunkSystem.getUpdatingChunkHolders(this.level).iterator(); // Paper while (objectiterator.hasNext()) { ChunkHolder playerchunk = (ChunkHolder) objectiterator.next(); @@ -1014,7 +1090,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } public int size() { - return this.visibleChunkMap.size(); + return io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper } public DistanceManager getDistanceManager() { @@ -1022,19 +1098,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } protected Iterable getChunks() { - return Iterables.unmodifiableIterable(this.visibleChunkMap.values()); + return Iterables.unmodifiableIterable(io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level)); // Paper } void dumpChunks(Writer writer) throws IOException { CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer); TickingTracker tickingtracker = this.distanceManager.tickingTracker(); - ObjectBidirectionalIterator objectbidirectionaliterator = this.visibleChunkMap.long2ObjectEntrySet().iterator(); + Iterator objectbidirectionaliterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper while (objectbidirectionaliterator.hasNext()) { - Entry entry = (Entry) objectbidirectionaliterator.next(); - long i = entry.getLongKey(); + ChunkHolder playerchunk = objectbidirectionaliterator.next(); // Paper + long i = playerchunk.pos.toLong(); // Paper ChunkPos chunkcoordintpair = new ChunkPos(i); - ChunkHolder playerchunk = (ChunkHolder) entry.getValue(); + // Paper Optional optional = Optional.ofNullable(playerchunk.getLastAvailable()); Optional optional1 = optional.flatMap((ichunkaccess) -> { return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty(); @@ -1160,6 +1236,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (!flag1) { this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player); } + this.addPlayerToDistanceMaps(player); // Paper - distance maps } else { SectionPos sectionposition = player.getLastSectionPos(); @@ -1167,6 +1244,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (!flag2) { this.distanceManager.removePlayer(sectionposition, player); } + this.removePlayerFromDistanceMaps(player); // Paper - distance maps } for (int k = i - this.viewDistance - 1; k <= i + this.viewDistance + 1; ++k) { @@ -1279,6 +1357,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } } + this.updateMaps(player); // Paper - distance maps + } @Override @@ -1515,7 +1595,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider private class ChunkDistanceManager extends DistanceManager { protected ChunkDistanceManager(Executor workerExecutor, Executor mainThreadExecutor) { - super(workerExecutor, mainThreadExecutor); + super(workerExecutor, mainThreadExecutor, ChunkMap.this); } @Override diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java index 6c98676827ceb6999f340fa2b06a0b3e1cb4cae2..fbe62a31ab199d83a1db0a4e0b1a813824e6f2c2 100644 --- a/src/main/java/net/minecraft/server/level/DistanceManager.java +++ b/src/main/java/net/minecraft/server/level/DistanceManager.java @@ -60,8 +60,9 @@ public abstract class DistanceManager { final Executor mainThreadExecutor; private long ticketTickCounter; private int simulationDistance = 10; + private final ChunkMap chunkMap; // Paper - protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor) { + protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor, ChunkMap chunkMap) { Objects.requireNonNull(mainThreadExecutor); ProcessorHandle mailbox = ProcessorHandle.of("player ticket throttler", mainThreadExecutor::execute); ChunkTaskPriorityQueueSorter chunktaskqueuesorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(mailbox), workerExecutor, 4); @@ -70,6 +71,7 @@ public abstract class DistanceManager { this.ticketThrottlerInput = chunktaskqueuesorter.getProcessor(mailbox, true); this.ticketThrottlerReleaser = chunktaskqueuesorter.getReleaseProcessor(mailbox); this.mainThreadExecutor = mainThreadExecutor; + this.chunkMap = chunkMap; // Paper } protected void purgeStaleTickets() { @@ -319,6 +321,12 @@ public abstract class DistanceManager { this.playerTicketManager.updateViewDistance(viewDistance); } + // Paper start + public int getSimulationDistance() { + return this.simulationDistance; + } + // Paper end + public void updateSimulationDistance(int simulationDistance) { if (simulationDistance != this.simulationDistance) { this.simulationDistance = simulationDistance; @@ -382,7 +390,7 @@ public abstract class DistanceManager { } public void removeTicketsOnClosing() { - ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT); + ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); while (objectiterator.hasNext()) { diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java index b323b8329f534b7020dd595b8b15197c29939590..794ad2dbaea2555d4557124e9d942d3e6919ea09 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -51,6 +51,7 @@ import net.minecraft.world.level.storage.LevelStorageSource; public class ServerChunkCache extends ChunkSource { + public static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); // Paper private static final List CHUNK_STATUSES = ChunkStatus.getStatusList(); private final DistanceManager distanceManager; final ServerLevel level; @@ -69,6 +70,231 @@ public class ServerChunkCache extends ChunkSource { @Nullable @VisibleForDebug private NaturalSpawner.SpawnState lastSpawnState; + // Paper start + final com.destroystokyo.paper.util.concurrent.WeakSeqLock loadedChunkMapSeqLock = new com.destroystokyo.paper.util.concurrent.WeakSeqLock(); + final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap loadedChunkMap = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(8192, 0.5f); + + private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4]; + + 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; + } + + 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)); + } + + public final LevelChunk getChunkAtMainThread(int x, int z) { + LevelChunk ret = this.getChunkAtIfLoadedMainThread(x, z); + if (ret != null) { + return ret; + } + return (LevelChunk)this.getChunk(x, z, ChunkStatus.FULL, true); + } + + long chunkFutureAwaitCounter; // Paper - private -> package private + + public void getEntityTickingChunkAsync(int x, int z, java.util.function.Consumer onLoad) { + io.papermc.paper.chunk.system.ChunkSystem.scheduleTickingState( + this.level, x, z, ChunkHolder.FullChunkStatus.ENTITY_TICKING, true, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, onLoad + ); + } + + public void getTickingChunkAsync(int x, int z, java.util.function.Consumer onLoad) { + io.papermc.paper.chunk.system.ChunkSystem.scheduleTickingState( + this.level, x, z, ChunkHolder.FullChunkStatus.TICKING, true, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, onLoad + ); + } + + public void getFullChunkAsync(int x, int z, java.util.function.Consumer onLoad) { + io.papermc.paper.chunk.system.ChunkSystem.scheduleTickingState( + this.level, x, z, ChunkHolder.FullChunkStatus.BORDER, true, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, onLoad + ); + } + + void chunkLoadAccept(int chunkX, int chunkZ, ChunkAccess chunk, java.util.function.Consumer consumer) { + try { + consumer.accept(chunk); + } catch (Throwable throwable) { + if (throwable instanceof ThreadDeath) { + throw (ThreadDeath)throwable; + } + LOGGER.error("Load callback for chunk " + chunkX + "," + chunkZ + " in world '" + this.level.getWorld().getName() + "' threw an exception", throwable); + } + } + + void getChunkAtAsynchronously(int chunkX, int chunkZ, int ticketLevel, + java.util.function.Consumer consumer) { + if (ticketLevel <= 33) { + this.getFullChunkAsync(chunkX, chunkZ, (java.util.function.Consumer)consumer); + return; + } + + io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad( + this.level, chunkX, chunkZ, ChunkHolder.getStatus(ticketLevel), true, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, consumer + ); + } + + + public final void getChunkAtAsynchronously(int chunkX, int chunkZ, ChunkStatus status, boolean gen, boolean allowSubTicketLevel, java.util.function.Consumer onLoad) { + // try to fire sync + int chunkStatusTicketLevel = 33 + ChunkStatus.getDistance(status); + ChunkHolder playerChunk = this.chunkMap.getUpdatingChunkIfPresent(io.papermc.paper.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); + if (playerChunk != null) { + ChunkStatus holderStatus = playerChunk.getChunkHolderStatus(); + ChunkAccess immediate = playerChunk.getAvailableChunkNow(); + if (immediate != null) { + if (allowSubTicketLevel ? immediate.getStatus().isOrAfter(status) : (playerChunk.getTicketLevel() <= chunkStatusTicketLevel && holderStatus != null && holderStatus.isOrAfter(status))) { + this.chunkLoadAccept(chunkX, chunkZ, immediate, onLoad); + return; + } else { + if (gen || (!allowSubTicketLevel && immediate.getStatus().isOrAfter(status))) { + this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); + return; + } else { + this.chunkLoadAccept(chunkX, chunkZ, null, onLoad); + return; + } + } + } + } + + // need to fire async + + if (gen && !allowSubTicketLevel) { + this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); + return; + } + + this.getChunkAtAsynchronously(chunkX, chunkZ, io.papermc.paper.util.MCUtil.getTicketLevelFor(ChunkStatus.EMPTY), (ChunkAccess chunk) -> { + if (chunk == null) { + throw new IllegalStateException("Chunk cannot be null"); + } + + if (!chunk.getStatus().isOrAfter(status)) { + if (gen) { + this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); + return; + } else { + ServerChunkCache.this.chunkLoadAccept(chunkX, chunkZ, null, onLoad); + return; + } + } else { + if (allowSubTicketLevel) { + ServerChunkCache.this.chunkLoadAccept(chunkX, chunkZ, chunk, onLoad); + return; + } else { + this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); + return; + } + } + }); + } + // Paper end + + // Paper start + @Nullable + public ChunkAccess getChunkAtImmediately(int x, int z) { + ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); + if (holder == null) { + return null; + } + + return holder.getLastAvailable(); + } + + // this will try to avoid chunk neighbours for lighting + public final ChunkAccess getFullStatusChunkAt(int chunkX, int chunkZ) { + LevelChunk ifLoaded = this.getChunkAtIfLoadedImmediately(chunkX, chunkZ); + if (ifLoaded != null) { + return ifLoaded; + } + + ChunkAccess empty = this.getChunk(chunkX, chunkZ, ChunkStatus.EMPTY, true); + if (empty != null && empty.getStatus().isOrAfter(ChunkStatus.FULL)) { + return empty; + } + return this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); + } + + public final ChunkAccess getFullStatusChunkAtIfLoaded(int chunkX, int chunkZ) { + LevelChunk ifLoaded = this.getChunkAtIfLoadedImmediately(chunkX, chunkZ); + if (ifLoaded != null) { + return ifLoaded; + } + + ChunkAccess ret = this.getChunkAtImmediately(chunkX, chunkZ); + if (ret != null && ret.getStatus().isOrAfter(ChunkStatus.FULL)) { + return ret; + } else { + return null; + } + } + + public void addTicketAtLevel(TicketType ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) { + this.distanceManager.addTicket(ticketType, chunkPos, ticketLevel, identifier); + } + + public void removeTicketAtLevel(TicketType ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) { + this.distanceManager.removeTicket(ticketType, chunkPos, ticketLevel, identifier); + } + + public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet tickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); + public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); + // 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 persistentStateManagerFactory) { this.level = world; @@ -121,6 +347,49 @@ public class ServerChunkCache extends ChunkSource { this.lastChunk[0] = chunk; } + // Paper start - "real" get chunk if loaded + // Note: Partially copied from the getChunkAt method below + @Nullable + public LevelChunk getChunkAtIfCachedImmediately(int x, int z) { + long k = ChunkPos.asLong(x, z); + + // Note: Bypass cache since we need to check ticket level, and to make this MT-Safe + + ChunkHolder playerChunk = this.getVisibleChunkIfPresent(k); + if (playerChunk == null) { + return null; + } + + return playerChunk.getFullChunkNowUnchecked(); + } + + @Nullable + public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) { + 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; + } + // Paper end + @Nullable @Override public ChunkAccess getChunk(int x, int z, ChunkStatus leastStatus, boolean create) { @@ -328,6 +597,12 @@ public class ServerChunkCache extends ChunkSource { } } + // Paper start + public boolean isPositionTicking(Entity entity) { + return this.isPositionTicking(ChunkPos.asLong(net.minecraft.util.Mth.floor(entity.getX()) >> 4, net.minecraft.util.Mth.floor(entity.getZ()) >> 4)); + } + // Paper end + public boolean isPositionTicking(long pos) { ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index 3bcbdf37ad9d76ec97ad3f20e7a683e267441ed9..aa164a81d072d9390fa1400120e801979e5d74d0 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -173,6 +173,7 @@ import org.bukkit.event.weather.LightningStrikeEvent; import org.bukkit.event.world.GenericGameEvent; import org.bukkit.event.world.TimeSkipEvent; // CraftBukkit end +import it.unimi.dsi.fastutil.ints.IntArrayList; // Paper public class ServerLevel extends Level implements WorldGenLevel { @@ -224,6 +225,98 @@ public class ServerLevel extends Level implements WorldGenLevel { return convertable.dimensionType; } + // Paper start + public final boolean areChunksLoadedForMove(AABB axisalignedbb) { + // copied code from collision methods, so that we can guarantee that they wont load chunks (we don't override + // ICollisionAccess methods for VoxelShapes) + // be more strict too, add a block (dumb plugins in move events?) + int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; + int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; + + int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; + int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; + + int minChunkX = minBlockX >> 4; + int maxChunkX = maxBlockX >> 4; + + int minChunkZ = minBlockZ >> 4; + int maxChunkZ = maxBlockZ >> 4; + + ServerChunkCache chunkProvider = this.getChunkSource(); + + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { + for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { + if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) { + return false; + } + } + } + + return true; + } + + public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, + java.util.function.Consumer> onLoad) { + if (Thread.currentThread() != this.thread) { + this.getChunkSource().mainThreadProcessor.execute(() -> { + this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad); + }); + return; + } + List ret = new java.util.ArrayList<>(); + IntArrayList ticketLevels = new IntArrayList(); + + int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; + int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; + + int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; + int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; + + int minChunkX = minBlockX >> 4; + int maxChunkX = maxBlockX >> 4; + + int minChunkZ = minBlockZ >> 4; + int maxChunkZ = maxBlockZ >> 4; + + ServerChunkCache chunkProvider = this.getChunkSource(); + + int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); + int[] loadedChunks = new int[1]; + + Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++); + + java.util.function.Consumer consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> { + if (chunk != null) { + int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel()); + ret.add(chunk); + ticketLevels.add(ticketLevel); + chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier); + } + if (++loadedChunks[0] == requiredChunks) { + try { + onLoad.accept(java.util.Collections.unmodifiableList(ret)); + } finally { + for (int i = 0, len = ret.size(); i < len; ++i) { + ChunkPos chunkPos = ret.get(i).getPos(); + int ticketLevel = ticketLevels.getInt(i); + + chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); + chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier); + } + } + } + }; + + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { + for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { + io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad( + this, cx, cz, net.minecraft.world.level.chunk.ChunkStatus.FULL, true, priority, consumer + ); + } + } + } + // Paper end + // Add env and gen to constructor, IWorldDataServer -> WorldDataServer public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { // IRegistryCustom.Dimension iregistrycustom_dimension = minecraftserver.registryAccess(); // CraftBukkit - decompile error diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java index 16ad728a5fd201875c56b27dcedeb19e06d2ea7a..7efd4be91e6ff0abf087bf4d322fd6ac694b7010 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -252,6 +252,8 @@ public class ServerPlayer extends Player { public Integer clientViewDistance; public String kickLeaveMessage = null; // SPIGOT-3034: Forward leave message to PlayerQuitEvent // CraftBukkit end + public boolean isRealPlayer; // Paper + public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet cachedSingleHashSet; // Paper public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile) { super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile); @@ -317,6 +319,8 @@ public class ServerPlayer extends Player { this.setMaxUpStep(1.0F); this.fudgeSpawnLocation(world); + this.cachedSingleHashSet = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper + // CraftBukkit start this.displayName = this.getScoreboardName(); this.bukkitPickUpLoot = true; diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java index 3a4f026c73cdd22d30bdadabbcf24bef969b73e4..0d536d72ac918fbd403397ff369d10143ee9c204 100644 --- a/src/main/java/net/minecraft/server/level/TicketType.java +++ b/src/main/java/net/minecraft/server/level/TicketType.java @@ -7,6 +7,7 @@ import net.minecraft.util.Unit; import net.minecraft.world.level.ChunkPos; public class TicketType { + public static final TicketType FUTURE_AWAIT = create("future_await", Long::compareTo); // Paper private final String name; private final Comparator comparator; diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java index a63d5ba706a5b8e430aedc045bdeb3a410bd0eef..e96a0ca47e4701ba187555bd92c968345bc85677 100644 --- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java +++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java @@ -160,6 +160,26 @@ public class WorldGenRegion implements WorldGenLevel { return chunkX >= this.firstPos.x && chunkX <= this.lastPos.x && chunkZ >= this.firstPos.z && chunkZ <= this.lastPos.z; } + // Paper start - if loaded util + @Nullable + @Override + public ChunkAccess getChunkIfLoadedImmediately(int x, int z) { + return this.getChunk(x, z, ChunkStatus.FULL, false); + } + + @Override + public final BlockState getBlockStateIfLoaded(BlockPos blockposition) { + ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4); + return chunk == null ? null : chunk.getBlockState(blockposition); + } + + @Override + public final FluidState getFluidIfLoaded(BlockPos blockposition) { + ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4); + return chunk == null ? null : chunk.getFluidState(blockposition); + } + // Paper end + @Override public BlockState getBlockState(BlockPos pos) { return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())).getBlockState(pos); diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java index b001044f221b04d185522932b8e3b7591afac3db..6529064505dc3de875be1764f88897736b85975d 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java @@ -182,6 +182,7 @@ public abstract class PlayerList { } public void placeNewPlayer(Connection connection, ServerPlayer player) { + player.isRealPlayer = true; // Paper GameProfile gameprofile = player.getGameProfile(); GameProfileCache usercache = this.server.getProfileCache(); Optional optional = usercache.get(gameprofile.getId()); diff --git a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java index 337e0a7b3c14e1b1a28744920e0dc0a69e0c5a87..f5829ae484d93b547a5437b85a9621346384a11b 100644 --- a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java +++ b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java @@ -78,6 +78,13 @@ public abstract class BlockableEventLoop implements Profiler } } + // Paper start + public void scheduleOnMain(Runnable r0) { + // postToMainThread does not work the same as older versions of mc + // This method is actually used to create a TickTask, which can then be posted onto main + this.tell(this.wrapRunnable(r0)); + } + // Paper end @Override public void tell(R runnable) { diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java index 78ab0d82e3b425ae561e838d7827ed0ae14fa1d2..35125c029abbdab4c7043842b6042ea44b00a2c3 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -318,6 +318,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { return this.level.hasChunk((int) Math.floor(this.getX()) >> 4, (int) Math.floor(this.getZ()) >> 4); } // CraftBukkit end + // Paper start + public final AABB getBoundingBoxAt(double x, double y, double z) { + return this.dimensions.makeBoundingBox(x, y, z); + } + // Paper end public Entity(EntityType type, Level world) { this.id = Entity.ENTITY_COUNTER.incrementAndGet(); diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java index c0859f1ded9679e59b19313352fa474742653255..592e41884ffda0075ec16e5538d5004efeb80f78 100644 --- a/src/main/java/net/minecraft/world/entity/LivingEntity.java +++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java @@ -256,6 +256,7 @@ public abstract class LivingEntity extends Entity implements Attackable { public boolean collides = true; public Set collidableExemptions = new HashSet<>(); public boolean bukkitPickUpLoot; + public org.bukkit.craftbukkit.entity.CraftLivingEntity getBukkitLivingEntity() { return (org.bukkit.craftbukkit.entity.CraftLivingEntity) super.getBukkitEntity(); } // Paper @Override public float getBukkitYaw() { diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java index 1afd00e39bc5423d8bcce37a2caa4d6401c5b5e7..a290487b153a66a3e936ed1183f3c2ce343e59b1 100644 --- a/src/main/java/net/minecraft/world/entity/Mob.java +++ b/src/main/java/net/minecraft/world/entity/Mob.java @@ -272,6 +272,7 @@ public abstract class Mob extends LivingEntity implements Targeting { return this.target; } + public org.bukkit.craftbukkit.entity.CraftMob getBukkitMob() { return (org.bukkit.craftbukkit.entity.CraftMob) super.getBukkitEntity(); } // Paper public void setTarget(@Nullable LivingEntity target) { // CraftBukkit start - fire event this.setTarget(target, EntityTargetEvent.TargetReason.UNKNOWN, true); diff --git a/src/main/java/net/minecraft/world/entity/PathfinderMob.java b/src/main/java/net/minecraft/world/entity/PathfinderMob.java index fbe69ec9cded766c9b76ed4a1bcba6d4f49d6165..6ae3f5cd42dfd424fc3741957995f47ad5ec8941 100644 --- a/src/main/java/net/minecraft/world/entity/PathfinderMob.java +++ b/src/main/java/net/minecraft/world/entity/PathfinderMob.java @@ -18,6 +18,8 @@ public abstract class PathfinderMob extends Mob { super(type, world); } + public org.bukkit.craftbukkit.entity.CraftCreature getBukkitCreature() { return (org.bukkit.craftbukkit.entity.CraftCreature) super.getBukkitEntity(); } // Paper + public float getWalkTargetValue(BlockPos pos) { return this.getWalkTargetValue(pos, this.level); } diff --git a/src/main/java/net/minecraft/world/entity/monster/Monster.java b/src/main/java/net/minecraft/world/entity/monster/Monster.java index a0b5895abc88d297045e05f25bb09527991d43f0..6e0bd0eab0b06a4ac3042496bbb91292544e9f3c 100644 --- a/src/main/java/net/minecraft/world/entity/monster/Monster.java +++ b/src/main/java/net/minecraft/world/entity/monster/Monster.java @@ -27,6 +27,7 @@ import net.minecraft.world.level.ServerLevelAccessor; import net.minecraft.world.level.dimension.DimensionType; public abstract class Monster extends PathfinderMob implements Enemy { + public org.bukkit.craftbukkit.entity.CraftMonster getBukkitMonster() { return (org.bukkit.craftbukkit.entity.CraftMonster) super.getBukkitEntity(); } // Paper protected Monster(EntityType type, Level world) { super(type, world); this.xpReward = 5; diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java index 4a63213a3131af3381769c4adc2735def311a681..83af6f1af55ab9b0b7ad6f635e24b7c4d1d1829b 100644 --- a/src/main/java/net/minecraft/world/item/ItemStack.java +++ b/src/main/java/net/minecraft/world/item/ItemStack.java @@ -773,6 +773,25 @@ public final class ItemStack { return this.tag != null ? this.tag.getList("Enchantments", 10) : new ListTag(); } + // Paper start - (this is just a good no conflict location) + public org.bukkit.inventory.ItemStack asBukkitMirror() { + return CraftItemStack.asCraftMirror(this); + } + public org.bukkit.inventory.ItemStack asBukkitCopy() { + return CraftItemStack.asCraftMirror(this.copy()); + } + public static ItemStack fromBukkitCopy(org.bukkit.inventory.ItemStack itemstack) { + return CraftItemStack.asNMSCopy(itemstack); + } + private org.bukkit.craftbukkit.inventory.CraftItemStack bukkitStack; + public org.bukkit.inventory.ItemStack getBukkitStack() { + if (bukkitStack == null || bukkitStack.handle != this) { + bukkitStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this); + } + return bukkitStack; + } + // Paper end + public void setTag(@Nullable CompoundTag nbt) { this.tag = nbt; if (this.getItem().canBeDepleted()) { @@ -1163,6 +1182,7 @@ public final class ItemStack { // CraftBukkit start @Deprecated public void setItem(Item item) { + this.bukkitStack = null; // Paper this.item = item; } // CraftBukkit end diff --git a/src/main/java/net/minecraft/world/level/BlockGetter.java b/src/main/java/net/minecraft/world/level/BlockGetter.java index 1c71d2c1b16bdba1e14a8230787e4cb4ad530163..42e05380a875c52cd6e1cb337958b431a751698b 100644 --- a/src/main/java/net/minecraft/world/level/BlockGetter.java +++ b/src/main/java/net/minecraft/world/level/BlockGetter.java @@ -9,10 +9,12 @@ import javax.annotation.Nullable; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.util.Mth; +import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Material; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.Vec3; @@ -30,6 +32,19 @@ public interface BlockGetter extends LevelHeightAccessor { } BlockState getBlockState(BlockPos pos); + // Paper start - if loaded util + @Nullable BlockState getBlockStateIfLoaded(BlockPos blockposition); + default @Nullable Material getMaterialIfLoaded(BlockPos blockposition) { + BlockState type = this.getBlockStateIfLoaded(blockposition); + return type == null ? null : type.getMaterial(); + } + + default @Nullable Block getBlockIfLoaded(BlockPos blockposition) { + BlockState type = this.getBlockStateIfLoaded(blockposition); + return type == null ? null : type.getBlock(); + } + @Nullable FluidState getFluidIfLoaded(BlockPos blockposition); + // Paper end FluidState getFluidState(BlockPos pos); diff --git a/src/main/java/net/minecraft/world/level/ChunkPos.java b/src/main/java/net/minecraft/world/level/ChunkPos.java index a3040440ed34a1c2adca2d57d458504a4a48282f..2d41f619577b41d6420159668bbab70fb6c762eb 100644 --- a/src/main/java/net/minecraft/world/level/ChunkPos.java +++ b/src/main/java/net/minecraft/world/level/ChunkPos.java @@ -20,6 +20,7 @@ public class ChunkPos { public static final int REGION_MAX_INDEX = 31; public final int x; public final int z; + public final long longKey; // Paper private static final int HASH_A = 1664525; private static final int HASH_C = 1013904223; private static final int HASH_Z_XOR = -559038737; @@ -27,16 +28,19 @@ public class ChunkPos { public ChunkPos(int x, int z) { this.x = x; this.z = z; + this.longKey = asLong(this.x, this.z); // Paper } public ChunkPos(BlockPos pos) { this.x = SectionPos.blockToSectionCoord(pos.getX()); this.z = SectionPos.blockToSectionCoord(pos.getZ()); + this.longKey = asLong(this.x, this.z); // Paper } public ChunkPos(long pos) { this.x = (int)pos; this.z = (int)(pos >> 32); + this.longKey = asLong(this.x, this.z); // Paper } public static ChunkPos minFromRegion(int x, int z) { @@ -48,10 +52,10 @@ public class ChunkPos { } public long toLong() { - return asLong(this.x, this.z); + return longKey; // Paper } - public static long asLong(int chunkX, int chunkZ) { + public static long asLong(int chunkX, int chunkZ) { return (long)chunkX & 4294967295L | ((long)chunkZ & 4294967295L) << 32; } diff --git a/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java b/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java index 3c707d6674b2594b09503b959a31c1f4ad3981e6..db61b6b0158a9bcc0e1d735e34fe3671f8c89e21 100644 --- a/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java +++ b/src/main/java/net/minecraft/world/level/EmptyBlockGetter.java @@ -17,6 +17,18 @@ public enum EmptyBlockGetter implements BlockGetter { return null; } + // Paper start - If loaded util + @Override + public final FluidState getFluidIfLoaded(BlockPos blockposition) { + return Fluids.EMPTY.defaultFluidState(); + } + + @Override + public final BlockState getBlockStateIfLoaded(BlockPos blockposition) { + return Blocks.AIR.defaultBlockState(); + } + // Paper end + @Override public BlockState getBlockState(BlockPos pos) { return Blocks.AIR.defaultBlockState(); diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java index 40deaa2463876659c0579b5273b5249760e8f8c0..e4ebdf81b7907e1054c356091ebcd35264b015f4 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -90,6 +90,7 @@ import org.bukkit.craftbukkit.CraftServer; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.SpigotTimings; // Spigot import org.bukkit.craftbukkit.block.CapturedBlockState; +import org.bukkit.craftbukkit.block.CraftBlockState; import org.bukkit.craftbukkit.block.data.CraftBlockData; import org.bukkit.craftbukkit.util.CraftSpawnCategory; import org.bukkit.craftbukkit.util.CraftNamespacedKey; @@ -293,18 +294,52 @@ public abstract class Level implements LevelAccessor, AutoCloseable { return y < -20000000 || y >= 20000000; } - public LevelChunk getChunkAt(BlockPos pos) { + public final LevelChunk getChunkAt(BlockPos pos) { // Paper - help inline return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())); } @Override - public LevelChunk getChunk(int chunkX, int chunkZ) { - return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL); + public final LevelChunk getChunk(int chunkX, int chunkZ) { // Paper - final to help inline + return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); // Paper - avoid a method jump } + // Paper start - if loaded @Nullable @Override - public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { + public final ChunkAccess getChunkIfLoadedImmediately(int x, int z) { + return ((ServerLevel)this).chunkSource.getChunkAtIfLoadedImmediately(x, z); + } + + @Override + @Nullable + public final BlockState getBlockStateIfLoaded(BlockPos pos) { + // CraftBukkit start - tree generation + if (this.captureTreeGeneration) { + CraftBlockState previous = this.capturedBlockStates.get(pos); + if (previous != null) { + return previous.getHandle(); + } + } + // CraftBukkit end + if (this.isOutsideBuildHeight(pos)) { + return Blocks.VOID_AIR.defaultBlockState(); + } else { + ChunkAccess chunk = this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4); + + return chunk == null ? null : chunk.getBlockState(pos); + } + } + + @Override + public final FluidState getFluidIfLoaded(BlockPos blockposition) { + ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4); + + return chunk == null ? null : chunk.getFluidState(blockposition); + } + + @Override + public final ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { // Paper - final for inline + // Paper end ChunkAccess ichunkaccess = this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, create); if (ichunkaccess == null && create) { @@ -315,7 +350,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { } @Override - public boolean setBlock(BlockPos pos, BlockState state, int flags) { + public final boolean setBlock(BlockPos pos, BlockState state, int flags) { // Paper - final for inline return this.setBlock(pos, state, flags, 512); } @@ -559,7 +594,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { if (this.isOutsideBuildHeight(pos)) { return Blocks.VOID_AIR.defaultBlockState(); } else { - LevelChunk chunk = this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())); + ChunkAccess chunk = this.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.FULL, true); // Paper - manually inline to reduce hops and avoid unnecessary null check to reduce total byte code size, this should never return null and if it does we will see it the next line but the real stack trace will matter in the chunk engine return chunk.getBlockState(pos); } diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java index f5dbac0a13d23413dbdc48cfacc247ef25ef9444..7fe1b8856bf916796fa6d2a984f0a07a2331e23b 100644 --- a/src/main/java/net/minecraft/world/level/LevelReader.java +++ b/src/main/java/net/minecraft/world/level/LevelReader.java @@ -24,6 +24,7 @@ import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.phys.AABB; public interface LevelReader extends BlockAndTintGetter, CollisionGetter, BiomeManager.NoiseBiomeSource { + @Nullable ChunkAccess getChunkIfLoadedImmediately(int x, int z); // Paper - ifLoaded api (we need this since current impl blocks if the chunk is loading) @Nullable ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create); diff --git a/src/main/java/net/minecraft/world/level/PathNavigationRegion.java b/src/main/java/net/minecraft/world/level/PathNavigationRegion.java index 249b3ed33672a9a9529bd14de978722b62019314..0f1025495237aebe30132ace0832aa5718d6f9bb 100644 --- a/src/main/java/net/minecraft/world/level/PathNavigationRegion.java +++ b/src/main/java/net/minecraft/world/level/PathNavigationRegion.java @@ -9,6 +9,7 @@ import net.minecraft.core.Holder; import net.minecraft.core.SectionPos; import net.minecraft.core.registries.Registries; import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.biome.Biomes; @@ -70,7 +71,7 @@ public class PathNavigationRegion implements BlockGetter, CollisionGetter { private ChunkAccess getChunk(int chunkX, int chunkZ) { int i = chunkX - this.centerX; int j = chunkZ - this.centerZ; - if (i >= 0 && i < this.chunks.length && j >= 0 && j < this.chunks[i].length) { + if (i >= 0 && i < this.chunks.length && j >= 0 && j < this.chunks[i].length) { // Paper - if this changes, update getChunkIfLoaded below ChunkAccess chunkAccess = this.chunks[i][j]; return (ChunkAccess)(chunkAccess != null ? chunkAccess : new EmptyLevelChunk(this.level, new ChunkPos(chunkX, chunkZ), this.plains.get())); } else { @@ -78,6 +79,30 @@ public class PathNavigationRegion implements BlockGetter, CollisionGetter { } } + // Paper start - if loaded util + private @Nullable ChunkAccess getChunkIfLoaded(int x, int z) { + // Based on getChunk(int, int) + int xx = x - this.centerX; + int zz = z - this.centerZ; + + if (xx >= 0 && xx < this.chunks.length && zz >= 0 && zz < this.chunks[xx].length) { + return this.chunks[xx][zz]; + } + return null; + } + @Override + public final FluidState getFluidIfLoaded(BlockPos blockposition) { + ChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4); + return chunk == null ? null : chunk.getFluidState(blockposition); + } + + @Override + public final BlockState getBlockStateIfLoaded(BlockPos blockposition) { + ChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4); + return chunk == null ? null : chunk.getBlockState(blockposition); + } + // Paper end + @Override public WorldBorder getWorldBorder() { return this.level.getWorldBorder(); diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java index 9d753d0cf25150ea0e5972c657320ac8af864c57..2cb3463f3d77a32ada67a6251707d741d18910ca 100644 --- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java @@ -711,14 +711,14 @@ public abstract class BlockBehaviour implements FeatureElement { public abstract static class BlockStateBase extends StateHolder { - private final int lightEmission; - private final boolean useShapeForLightOcclusion; + private final int lightEmission; public final int getEmittedLight() { return this.lightEmission; } // Paper - OBFHELPER + private final boolean useShapeForLightOcclusion; public final boolean isTransparentOnSomeFaces() { return this.useShapeForLightOcclusion; } // Paper - OBFHELPER private final boolean isAir; private final Material material; private final MaterialColor materialColor; public final float destroySpeed; private final boolean requiresCorrectToolForDrops; - private final boolean canOcclude; + private final boolean canOcclude; public final boolean isOpaque() { return this.canOcclude; } // Paper - OBFHELPER private final BlockBehaviour.StatePredicate isRedstoneConductor; private final BlockBehaviour.StatePredicate isSuffocating; private final BlockBehaviour.StatePredicate isViewBlocking; @@ -753,12 +753,20 @@ public abstract class BlockBehaviour implements FeatureElement { this.spawnParticlesOnBreak = blockbase_info.spawnParticlesOnBreak; } + // Paper start + protected boolean shapeExceedsCube = true; + public final boolean shapeExceedsCube() { + return this.shapeExceedsCube; + } + // Paper end + public void initCache() { this.fluidState = ((Block) this.owner).getFluidState(this.asState()); this.isRandomlyTicking = ((Block) this.owner).isRandomlyTicking(this.asState()); if (!this.getBlock().hasDynamicShape()) { this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState()); } + this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here } @@ -794,8 +802,8 @@ public abstract class BlockBehaviour implements FeatureElement { return this.getBlock().getOcclusionShape(this.asState(), world, pos); } - public boolean hasLargeCollisionShape() { - return this.cache == null || this.cache.largeCollisionShape; + public final boolean hasLargeCollisionShape() { // Paper + return this.shapeExceedsCube; // Paper - moved into shape cache init } public boolean useShapeForLightOcclusion() { diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java index db6a64ae4437b76c39e7ddb02adbea27c95fde78..3fdbb777d4722596cc4df79b2d4d7b9c553580fd 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java @@ -58,7 +58,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom protected final ShortList[] postProcessing; protected volatile boolean unsaved; private volatile boolean isLightCorrect; - protected final ChunkPos chunkPos; + protected final ChunkPos chunkPos; public final long coordinateKey; public final int locX; public final int locZ; // Paper - cache coordinate key private long inhabitedTime; /** @deprecated */ @Nullable @@ -83,7 +83,8 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom // CraftBukkit end public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry biome, long inhabitedTime, @Nullable LevelChunkSection[] sectionArrayInitializer, @Nullable BlendingData blendingData) { - this.chunkPos = pos; + this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups + this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key this.upgradeData = upgradeData; this.levelHeightAccessor = heightLimitView; this.sections = new LevelChunkSection[heightLimitView.getSectionsCount()]; diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java index 802a927de07b5ebcdd41bf3dc75c53eca582f1df..0307083079c0a257ecb82b8cb4fb8f91af3816bc 100644 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java @@ -25,6 +25,7 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData; import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ServerChunkCache; import net.minecraft.server.level.ServerLevel; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.entity.Entity; @@ -124,6 +125,109 @@ public class LevelChunk extends ChunkAccess { // CraftBukkit end + // Paper start + public @Nullable 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(); @@ -233,6 +337,18 @@ public class LevelChunk extends ChunkAccess { } } + // Paper start - If loaded util + @Override + public final FluidState getFluidIfLoaded(BlockPos blockposition) { + return this.getFluidState(blockposition); + } + + @Override + public final BlockState getBlockStateIfLoaded(BlockPos blockposition) { + return this.getBlockState(blockposition); + } + // Paper end + @Override public FluidState getFluidState(BlockPos pos) { return this.getFluidState(pos.getX(), pos.getY(), pos.getZ()); @@ -354,6 +470,7 @@ public class LevelChunk extends ChunkAccess { return this.getBlockEntity(pos, LevelChunk.EntityCreationType.CHECK); } + @Deprecated @Nullable public final BlockEntity getTileEntityImmediately(BlockPos pos) { return this.getBlockEntity(pos, EntityCreationType.IMMEDIATE); } // Paper - OBFHELPER @Nullable public BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) { // CraftBukkit start @@ -535,7 +652,25 @@ public class LevelChunk extends ChunkAccess { // CraftBukkit start public void loadCallback() { + // Paper start - neighbour cache + int chunkX = this.chunkPos.x; + int chunkZ = this.chunkPos.z; + 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); + this.loadedTicketLevel = true; + // Paper end - neighbour cache 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 @@ -574,6 +709,22 @@ 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; + 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); + } + } + } + this.loadedTicketLevel = false; + this.resetNeighbours(); + // Paper end } @Override diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java index cb4e43177a735442fa2adda8640263bca8cdcb64..92a64c49b1c7227a5b34488ea15d3d8adb0f9c80 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java @@ -74,6 +74,18 @@ public class ProtoChunk extends ChunkAccess { return new ChunkAccess.TicksToSave(this.blockTicks, this.fluidTicks); } + // Paper start - If loaded util + @Override + public final FluidState getFluidIfLoaded(BlockPos blockposition) { + return this.getFluidState(blockposition); + } + + @Override + public final BlockState getBlockStateIfLoaded(BlockPos blockposition) { + return this.getBlockState(blockposition); + } + // Paper end + @Override public BlockState getBlockState(BlockPos pos) { int i = pos.getY(); diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java index f66369ddaeab5c5ac643c0979dac3ed21337ff71..038abf2ac104ceecaab11b10d466ea69ec86623e 100644 --- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java +++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java @@ -90,6 +90,18 @@ public class PersistentEntitySectionManager implements A } private boolean addEntity(T entity, boolean existing) { + // Paper start - chunk system hooks + if (existing) { + // I don't want to know why this is a generic type. + Entity entityCasted = (Entity)entity; + boolean wasRemoved = entityCasted.isRemoved(); + io.papermc.paper.chunk.system.ChunkSystem.onEntityPreAdd((net.minecraft.server.level.ServerLevel)entityCasted.level, entityCasted); + if (!wasRemoved && entityCasted.isRemoved()) { + // removed by callback + return false; + } + } + // Paper end - chunk system hooks if (!this.addEntityUuid(entity)) { return false; } else { diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index b685b6064a32884e5617f2debb7ea10159d9ce0d..3b9e42adb657d0feb99de4b55dc0c628e9cd5afd 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -233,8 +233,8 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public Chunk[] getLoadedChunks() { - Long2ObjectLinkedOpenHashMap chunks = this.world.getChunkSource().chunkMap.visibleChunkMap; - return chunks.values().stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new); + List chunks = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.world); // Paper + return chunks.stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new); } @Override @@ -309,7 +309,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean refreshChunk(int x, int z) { - ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.visibleChunkMap.get(ChunkPos.asLong(x, z)); + ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); if (playerChunk == null) return false; playerChunk.getTickingChunkFuture().thenAccept(either -> { @@ -1959,4 +1959,32 @@ public class CraftWorld extends CraftRegionAccessor implements World { return this.spigot; } // Spigot end + // Paper start + public java.util.concurrent.CompletableFuture getChunkAtAsync(int x, int z, boolean gen, boolean urgent) { + if (Bukkit.isPrimaryThread()) { + net.minecraft.world.level.chunk.LevelChunk immediate = this.world.getChunkSource().getChunkAtIfLoadedImmediately(x, z); + if (immediate != null) { + return java.util.concurrent.CompletableFuture.completedFuture(immediate.getBukkitChunk()); + } + } + + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority; + if (urgent) { + priority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER; + } else { + priority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL; + } + + java.util.concurrent.CompletableFuture ret = new java.util.concurrent.CompletableFuture<>(); + + io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> { + net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { + net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c; + ret.complete(chunk == null ? null : chunk.getBukkitChunk()); + }); + }); + + return ret; + } + // Paper end } diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java index e24609bb51369c08a68120830ead33706b17dafa..f4cdda9fabb3a13f7cc8b6056815bdbae704db9d 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java @@ -1182,4 +1182,37 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { return this.spigot; } // Spigot end + + // Paper start + @Override + public java.util.concurrent.CompletableFuture teleportAsync(Location location, TeleportCause cause) { + Preconditions.checkArgument(location != null, "location"); + location.checkFinite(); + Location locationClone = location.clone(); // clone so we don't need to worry about mutations after this call. + + net.minecraft.server.level.ServerLevel world = ((CraftWorld)locationClone.getWorld()).getHandle(); + java.util.concurrent.CompletableFuture ret = new java.util.concurrent.CompletableFuture<>(); + + world.loadChunksForMoveAsync(getHandle().getBoundingBoxAt(locationClone.getX(), locationClone.getY(), locationClone.getZ()), + this instanceof CraftPlayer ? ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER : ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, (list) -> { + net.minecraft.server.level.ServerChunkCache chunkProviderServer = world.getChunkSource(); + for (net.minecraft.world.level.chunk.ChunkAccess chunk : list) { + chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.POST_TELEPORT, chunk.getPos(), 33, CraftEntity.this.getEntityId()); + } + net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { + try { + ret.complete(CraftEntity.this.teleport(locationClone, cause) ? Boolean.TRUE : Boolean.FALSE); + } catch (Throwable throwable) { + if (throwable instanceof ThreadDeath) { + throw (ThreadDeath)throwable; + } + net.minecraft.server.MinecraftServer.LOGGER.error("Failed to teleport entity " + CraftEntity.this, throwable); + ret.completeExceptionally(throwable); + } + }); + }); + + return ret; + } + // Paper end } diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java index 2f52148f36e56c503e619634eedd3d46d9f44938..054fcc3713f02e358dfe049491c8d1689ccc750b 100644 --- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java @@ -44,6 +44,7 @@ import org.bukkit.scheduler.BukkitWorker; */ public class CraftScheduler implements BukkitScheduler { + static Plugin MINECRAFT = new MinecraftInternalPlugin(); /** * The start ID for the counter. */ @@ -192,6 +193,11 @@ public class CraftScheduler implements BukkitScheduler { this.runTaskTimer(plugin, (Object) task, delay, period); } + public BukkitTask scheduleInternalTask(Runnable run, int delay, String taskName) { + final CraftTask task = new CraftTask(run, nextId(), taskName); + return handle(task, delay); + } + public BukkitTask runTaskTimer(Plugin plugin, Object runnable, long delay, long period) { CraftScheduler.validate(plugin, runnable); if (delay < 0L) { @@ -415,13 +421,20 @@ public class CraftScheduler implements BukkitScheduler { task.run(); task.timings.stopTiming(); // Spigot } catch (final Throwable throwable) { - task.getOwner().getLogger().log( + // Paper start + String msg = String.format( + "Task #%s for %s generated an exception", + task.getTaskId(), + task.getOwner().getDescription().getFullName()); + if (task.getOwner() == MINECRAFT) { + net.minecraft.server.MinecraftServer.LOGGER.error(msg, throwable); + } else { + task.getOwner().getLogger().log( Level.WARNING, - String.format( - "Task #%s for %s generated an exception", - task.getTaskId(), - task.getOwner().getDescription().getFullName()), + msg, throwable); + } + // Paper end } finally { this.currentTask = null; } diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java index 592a2a513ebf0002abf1255e11012ecc66adecf0..b89846e0f645c79afec018dae1d64a1bda043ed9 100644 --- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java @@ -40,6 +40,21 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot CraftTask(final Object task) { this(null, task, CraftTask.NO_REPEATING, CraftTask.NO_REPEATING); } + // Paper start + public String taskName = null; + boolean internal = false; + CraftTask(final Object task, int id, String taskName) { + this.rTask = (Runnable) task; + this.cTask = null; + this.plugin = CraftScheduler.MINECRAFT; + this.taskName = taskName; + this.internal = true; + this.id = id; + this.period = CraftTask.NO_REPEATING; + this.taskName = taskName; + this.timings = null; // Will be changed in later patch + } + // Paper end CraftTask(final Plugin plugin, final Object task, final int id, final long period) { this.plugin = plugin; diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java new file mode 100644 index 0000000000000000000000000000000000000000..909b2c98e7a9117d2f737245e4661792ffafb744 --- /dev/null +++ b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java @@ -0,0 +1,140 @@ +package org.bukkit.craftbukkit.scheduler; + + +import org.bukkit.Server; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.plugin.PluginBase; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.PluginLoader; +import org.bukkit.plugin.PluginLogger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.InputStream; +import java.util.List; + +public class MinecraftInternalPlugin extends PluginBase { + private boolean enabled = true; + + private final String pluginName; + private PluginDescriptionFile pdf; + + public MinecraftInternalPlugin() { + this.pluginName = "Minecraft"; + pdf = new PluginDescriptionFile(pluginName, "1.0", "nms"); + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public File getDataFolder() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public PluginDescriptionFile getDescription() { + return pdf; + } + + @Override + public FileConfiguration getConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public InputStream getResource(String filename) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void saveConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void saveDefaultConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void saveResource(String resourcePath, boolean replace) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void reloadConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public PluginLogger getLogger() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public PluginLoader getPluginLoader() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public Server getServer() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void onDisable() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void onLoad() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void onEnable() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean isNaggable() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void setNaggable(boolean canNag) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public @Nullable BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + throw new UnsupportedOperationException("Not supported."); + } +} diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java index 1d41e9d1682df8fa000a36eab5196dcca810c769..eff182a54cbb84693d6cad96b51f743b08049b43 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java @@ -102,8 +102,17 @@ public final class CraftMagicNumbers implements UnsafeValues { private static final BiMap FLUIDTYPE_FLUID = HashBiMap.create(); private static final Map MATERIAL_ITEM = new HashMap<>(); private static final Map MATERIAL_BLOCK = new HashMap<>(); + // Paper start + private static final Map> ENTITY_TYPE_ENTITY_TYPES = new HashMap<>(); + private static final Map, org.bukkit.entity.EntityType> ENTITY_TYPES_ENTITY_TYPE = new HashMap<>(); static { + for (org.bukkit.entity.EntityType type : org.bukkit.entity.EntityType.values()) { + if (type == org.bukkit.entity.EntityType.UNKNOWN) continue; + ENTITY_TYPE_ENTITY_TYPES.put(type, BuiltInRegistries.ENTITY_TYPE.get(CraftNamespacedKey.toMinecraft(type.getKey()))); + ENTITY_TYPES_ENTITY_TYPE.put(BuiltInRegistries.ENTITY_TYPE.get(CraftNamespacedKey.toMinecraft(type.getKey())), type); + } + // Paper end for (Block block : BuiltInRegistries.BLOCK) { BLOCK_MATERIAL.put(block, Material.getMaterial(BuiltInRegistries.BLOCK.getKey(block).getPath().toUpperCase(Locale.ROOT))); } @@ -167,6 +176,14 @@ public final class CraftMagicNumbers implements UnsafeValues { public static ResourceLocation key(Material mat) { return CraftNamespacedKey.toMinecraft(mat.getKey()); } + // Paper start + public static net.minecraft.world.entity.EntityType getEntityTypes(org.bukkit.entity.EntityType type) { + return ENTITY_TYPE_ENTITY_TYPES.get(type); + } + public static org.bukkit.entity.EntityType getEntityType(net.minecraft.world.entity.EntityType entityTypes) { + return ENTITY_TYPES_ENTITY_TYPE.get(entityTypes); + } + // Paper end // ======================================================================== public static byte toLegacyData(BlockState data) { diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java index bbc2ab1972d9b88530282225650ec14b521d413f..9a80e0c390d13453a4a79e00d18c20b79afd3c7f 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java +++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java @@ -213,7 +213,23 @@ public class DummyGeneratorAccess implements WorldGenLevel { public FluidState getFluidState(BlockPos pos) { return Fluids.EMPTY.defaultFluidState(); // SPIGOT-6634 } + // Paper start - if loaded util + @javax.annotation.Nullable + @Override + public ChunkAccess getChunkIfLoadedImmediately(int x, int z) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public BlockState getBlockStateIfLoaded(BlockPos blockposition) { + throw new UnsupportedOperationException("Not supported yet."); + } + @Override + public FluidState getFluidIfLoaded(BlockPos blockposition) { + throw new UnsupportedOperationException("Not supported yet."); + } + // Paper end @Override public WorldBorder getWorldBorder() { throw new UnsupportedOperationException("Not supported yet."); diff --git a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java index d40c0d8be1b0153d62021b8bcb6e8b37fd0acb4e..e38e57b1f9ef27020de35d7ddcb36a663140f880 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java +++ b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java @@ -119,6 +119,32 @@ public class UnsafeList extends AbstractList implements List, RandomAcc return this.indexOf(o) >= 0; } + // Paper start + protected transient int maxSize; + public void setSize(int size) { + if (this.maxSize < this.size) { + this.maxSize = this.size; + } + this.size = size; + } + + public void completeReset() { + if (this.data != null) { + Arrays.fill(this.data, 0, Math.max(this.size, this.maxSize), null); + } + this.size = 0; + this.maxSize = 0; + if (this.iterPool != null) { + for (Iterator temp : this.iterPool) { + if (temp == null) { + continue; + } + ((Itr)temp).valid = false; + } + } + } + // Paper end + @Override public void clear() { // Create new array to reset memory usage to initial capacity diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java index b599e098e6d2e6f78b89114e05cb03d716b7c5f4..83eabc34d952bbb13ec4b4bdcf34f647189c0b46 100644 --- a/src/main/java/org/spigotmc/SpigotConfig.java +++ b/src/main/java/org/spigotmc/SpigotConfig.java @@ -118,7 +118,11 @@ public class SpigotConfig } } } - + // Paper start + SpigotConfig.save(); + } + public static void save() { + // Paper end try { SpigotConfig.config.save( CONFIG_FILE );