From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Sun, 2 Oct 2022 21:28:53 -0700 Subject: [PATCH] Threaded Regions Connection thread-safety fixes - send packet - pending addition diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java index f4415f782b32fed25da98e44b172f717c4d46e34..ba7c24b3627a1827721d2462add15fdd4adbed90 100644 --- a/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java +++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java @@ -392,6 +392,24 @@ public class MultiThreadedQueue implements Queue { } } + /** + * Returns whether this queue is currently add-blocked. That is, whether {@link #add(Object)} and friends will return {@code false}. + */ + public boolean isAddBlocked() { + for (LinkedNode tail = this.getTailOpaque();;) { + LinkedNode next = tail.getNextVolatile(); + if (next == null) { + return false; + } + + if (next == tail) { + return true; + } + + tail = next; + } + } + /** * Atomically removes the head from this queue if it exists, otherwise prevents additions to this queue if no * head is removed. diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/AreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/AreaLock.java new file mode 100644 index 0000000000000000000000000000000000000000..6a155b779914828a0d4199bdfcb0d6fca25e1581 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/AreaLock.java @@ -0,0 +1,146 @@ +package ca.spottedleaf.concurrentutil.lock; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.LockSupport; + +public final class AreaLock { + + private final int coordinateShift; + + private final Long2ReferenceOpenHashMap nodesByPosition = new Long2ReferenceOpenHashMap<>(1024, 0.10f); + + public AreaLock(final int coordinateShift) { + this.coordinateShift = coordinateShift; + } + + private static long key(final int x, final int z) { + return ((long)z << 32) | (x & 0xFFFFFFFFL); + } + + public Node lock(final int x, final int z, final int radius) { + final Thread thread = Thread.currentThread(); + final int minX = (x - radius) >> this.coordinateShift; + final int minZ = (z - radius) >> this.coordinateShift; + final int maxX = (x + radius) >> this.coordinateShift; + final int maxZ = (z + radius) >> this.coordinateShift; + + final Node node = new Node(x, z, radius, thread); + + synchronized (this) { + ReferenceOpenHashSet parents = null; + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final Node dependency = this.nodesByPosition.put(key(currX, currZ), node); + if (dependency == null) { + continue; + } + + if (parents == null) { + parents = new ReferenceOpenHashSet<>(); + } + + if (parents.add(dependency)) { + // added a dependency, so we need to add as a child to the dependency + if (dependency.children == null) { + dependency.children = new ArrayList<>(); + } + dependency.children.add(node); + } + } + } + + if (parents == null) { + // no dependencies, so we can just return immediately + return node; + } // else: we need to lock + + node.parents = parents; + } + + while (!node.unlocked) { + LockSupport.park(node); + } + + return node; + } + + public void unlock(final Node node) { + List toUnpark = null; + + final int x = node.x; + final int z = node.z; + final int radius = node.radius; + + final int minX = (x - radius) >> this.coordinateShift; + final int minZ = (z - radius) >> this.coordinateShift; + final int maxX = (x + radius) >> this.coordinateShift; + final int maxZ = (z + radius) >> this.coordinateShift; + + synchronized (this) { + final List children = node.children; + if (children != null) { + // try to unlock children + for (int i = 0, len = children.size(); i < len; ++i) { + final Node child = children.get(i); + if (!child.parents.remove(node)) { + throw new IllegalStateException(); + } + if (child.parents.isEmpty()) { + // we can unlock, as it now has no dependencies in front + child.parents = null; + if (toUnpark == null) { + toUnpark = new ArrayList<>(); + toUnpark.add(child); + } else { + toUnpark.add(child); + } + } + } + } + + // remove node from dependency map + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + // node: we only remove if we match, as a mismatch indicates a child node which of course has not + // yet been unlocked + this.nodesByPosition.remove(key(currX, currZ), node); + } + } + } + + if (toUnpark == null) { + return; + } + + // we move the unpark / unlock logic here because we want to avoid performing work while holding the lock + + for (int i = 0, len = toUnpark.size(); i < len; ++i) { + final Node toUnlock = toUnpark.get(i); + toUnlock.unlocked = true; // must be volatile and before unpark() + LockSupport.unpark(toUnlock.thread); + } + } + + public static final class Node { + + public final int x; + public final int z; + public final int radius; + public final Thread thread; + + private List children; + private ReferenceOpenHashSet parents; + + private volatile boolean unlocked; + + public Node(final int x, final int z, final int radius, final Thread thread) { + this.x = x; + this.z = z; + this.radius = radius; + this.thread = thread; + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java new file mode 100644 index 0000000000000000000000000000000000000000..7869cc177c95e26dd9e1d3db5b50e996956edb24 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java @@ -0,0 +1,664 @@ +package ca.spottedleaf.concurrentutil.map; + +import ca.spottedleaf.concurrentutil.util.ArrayUtil; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import io.papermc.paper.util.IntegerUtil; +import java.lang.invoke.VarHandle; +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public class SWMRInt2IntHashTable { + + protected int size; + + protected TableEntry[] table; + + protected final float loadFactor; + + protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRInt2IntHashTable.class, "size", int.class); + protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRInt2IntHashTable.class, "table", TableEntry[].class); + + /* size */ + + protected final int getSizePlain() { + return (int)SIZE_HANDLE.get(this); + } + + protected final int getSizeOpaque() { + return (int)SIZE_HANDLE.getOpaque(this); + } + + protected final int getSizeAcquire() { + return (int)SIZE_HANDLE.getAcquire(this); + } + + protected final void setSizePlain(final int value) { + SIZE_HANDLE.set(this, value); + } + + protected final void setSizeOpaque(final int value) { + SIZE_HANDLE.setOpaque(this, value); + } + + protected final void setSizeRelease(final int value) { + SIZE_HANDLE.setRelease(this, value); + } + + /* table */ + + protected final TableEntry[] getTablePlain() { + //noinspection unchecked + return (TableEntry[])TABLE_HANDLE.get(this); + } + + protected final TableEntry[] getTableAcquire() { + //noinspection unchecked + return (TableEntry[])TABLE_HANDLE.getAcquire(this); + } + + protected final void setTablePlain(final TableEntry[] table) { + TABLE_HANDLE.set(this, table); + } + + protected final void setTableRelease(final TableEntry[] table) { + TABLE_HANDLE.setRelease(this, table); + } + + protected static final int DEFAULT_CAPACITY = 16; + protected static final float DEFAULT_LOAD_FACTOR = 0.75f; + protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1; + + /** + * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}. + */ + public SWMRInt2IntHashTable() { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs this map with the specified capacity and load factor of {@code 0.75f}. + * @param capacity specified initial capacity, > 0 + */ + public SWMRInt2IntHashTable(final int capacity) { + this(capacity, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs this map with the specified capacity and load factor. + * @param capacity specified capacity, > 0 + * @param loadFactor specified load factor, > 0 && finite + */ + public SWMRInt2IntHashTable(final int capacity, final float loadFactor) { + final int tableSize = getCapacityFor(capacity); + + if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) { + throw new IllegalArgumentException("Invalid load factor: " + loadFactor); + } + + //noinspection unchecked + final TableEntry[] table = new TableEntry[tableSize]; + this.setTablePlain(table); + + if (tableSize == MAXIMUM_CAPACITY) { + this.threshold = -1; + } else { + this.threshold = getTargetCapacity(tableSize, loadFactor); + } + + this.loadFactor = loadFactor; + } + + /** + * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and + * with a load factor of {@code 0.75f}. + * All of the specified map's entries are copied into this map. + * @param other The specified map. + */ + public SWMRInt2IntHashTable(final SWMRInt2IntHashTable other) { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other); + } + + /** + * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and + * with a load factor of {@code 0.75f}. + * All of the specified map's entries are copied into this map. + * @param capacity specified capacity, > 0 + * @param other The specified map. + */ + public SWMRInt2IntHashTable(final int capacity, final SWMRInt2IntHashTable other) { + this(capacity, DEFAULT_LOAD_FACTOR, other); + } + + /** + * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and + * with the specified load factor. + * All of the specified map's entries are copied into this map. + * @param capacity specified capacity, > 0 + * @param loadFactor specified load factor, > 0 && finite + * @param other The specified map. + */ + public SWMRInt2IntHashTable(final int capacity, final float loadFactor, final SWMRInt2IntHashTable other) { + this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor); + this.putAll(other); + } + + public final float getLoadFactor() { + return this.loadFactor; + } + + protected static int getCapacityFor(final int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Invalid capacity: " + capacity); + } + if (capacity >= MAXIMUM_CAPACITY) { + return MAXIMUM_CAPACITY; + } + return IntegerUtil.roundCeilLog2(capacity); + } + + /** Callers must still use acquire when reading the value of the entry. */ + protected final TableEntry getEntryForOpaque(final int key) { + final int hash = SWMRInt2IntHashTable.getHash(key); + final TableEntry[] table = this.getTableAcquire(); + + for (TableEntry curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) { + if (key == curr.key) { + return curr; + } + } + + return null; + } + + protected final TableEntry getEntryForPlain(final int key) { + final int hash = SWMRInt2IntHashTable.getHash(key); + final TableEntry[] table = this.getTablePlain(); + + for (TableEntry curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) { + if (key == curr.key) { + return curr; + } + } + + return null; + } + + /* MT-Safe */ + + /** must be deterministic given a key */ + protected static int getHash(final int key) { + return it.unimi.dsi.fastutil.HashCommon.mix(key); + } + + // rets -1 if capacity*loadFactor is too large + protected static int getTargetCapacity(final int capacity, final float loadFactor) { + final double ret = (double)capacity * (double)loadFactor; + if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) { + return -1; + } + + return (int)ret; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + /* Make no attempt to deal with concurrent modifications */ + if (!(obj instanceof SWMRInt2IntHashTable)) { + return false; + } + final SWMRInt2IntHashTable other = (SWMRInt2IntHashTable)obj; + + if (this.size() != other.size()) { + return false; + } + + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final int value = curr.getValueAcquire(); + + final int otherValue = other.get(curr.key); + if (value != otherValue) { + return false; + } + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + /* Make no attempt to deal with concurrent modifications */ + int hash = 0; + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + hash += curr.hashCode(); + } + } + + return hash; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(64); + builder.append("SingleWriterMultiReaderHashMap:{"); + + this.forEach((final int key, final int value) -> { + builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}"); + }); + + return builder.append('}').toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public SWMRInt2IntHashTable clone() { + return new SWMRInt2IntHashTable(this.getTableAcquire().length, this.loadFactor, this); + } + + /** + * {@inheritDoc} + */ + public void forEach(final Consumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr); + } + } + } + + @FunctionalInterface + public static interface BiIntIntConsumer { + public void accept(final int key, final int value); + } + + /** + * {@inheritDoc} + */ + public void forEach(final BiIntIntConsumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final int value = curr.getValueAcquire(); + + action.accept(curr.key, value); + } + } + } + + /** + * Provides the specified consumer with all keys contained within this map. + * @param action The specified consumer. + */ + public void forEachKey(final IntConsumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr.key); + } + } + } + + /** + * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}. + * @param action The specified consumer. + */ + public void forEachValue(final IntConsumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final int value = curr.getValueAcquire(); + + action.accept(value); + } + } + } + + /** + * {@inheritDoc} + */ + public int get(final int key) { + final TableEntry entry = this.getEntryForOpaque(key); + return entry == null ? 0 : entry.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + public boolean containsKey(final int key) { + final TableEntry entry = this.getEntryForOpaque(key); + return entry != null; + } + + /** + * {@inheritDoc} + */ + public int getOrDefault(final int key, final int defaultValue) { + final TableEntry entry = this.getEntryForOpaque(key); + + return entry == null ? defaultValue : entry.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + public int size() { + return this.getSizeAcquire(); + } + + /** + * {@inheritDoc} + */ + public boolean isEmpty() { + return this.getSizeAcquire() == 0; + } + + /* Non-MT-Safe */ + + protected int threshold; + + protected final void checkResize(final int minCapacity) { + if (minCapacity <= this.threshold || this.threshold < 0) { + return; + } + + final TableEntry[] table = this.getTablePlain(); + int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity); + if (newCapacity < 0) { + newCapacity = MAXIMUM_CAPACITY; + } + if (newCapacity <= table.length) { + if (newCapacity == MAXIMUM_CAPACITY) { + return; + } + newCapacity = table.length << 1; + } + + //noinspection unchecked + final TableEntry[] newTable = new TableEntry[newCapacity]; + final int indexMask = newCapacity - 1; + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry entry = table[i]; entry != null; entry = entry.getNextPlain()) { + final int key = entry.key; + final int hash = SWMRInt2IntHashTable.getHash(key); + final int index = hash & indexMask; + + /* we need to create a new entry since there could be reading threads */ + final TableEntry insert = new TableEntry(key, entry.getValuePlain()); + + final TableEntry prev = newTable[index]; + + newTable[index] = insert; + insert.setNextPlain(prev); + } + } + + if (newCapacity == MAXIMUM_CAPACITY) { + this.threshold = -1; /* No more resizing */ + } else { + this.threshold = getTargetCapacity(newCapacity, this.loadFactor); + } + this.setTableRelease(newTable); /* use release to publish entries in table */ + } + + protected final int addToSize(final int num) { + final int newSize = this.getSizePlain() + num; + + this.setSizeOpaque(newSize); + this.checkResize(newSize); + + return newSize; + } + + protected final int removeFromSize(final int num) { + final int newSize = this.getSizePlain() - num; + + this.setSizeOpaque(newSize); + + return newSize; + } + + protected final int put(final int key, final int value, final boolean onlyIfAbsent) { + final TableEntry[] table = this.getTablePlain(); + final int hash = SWMRInt2IntHashTable.getHash(key); + final int index = hash & (table.length - 1); + + final TableEntry head = table[index]; + if (head == null) { + final TableEntry insert = new TableEntry(key, value); + ArrayUtil.setRelease(table, index, insert); + this.addToSize(1); + return 0; + } + + for (TableEntry curr = head;;) { + if (key == curr.key) { + if (onlyIfAbsent) { + return curr.getValuePlain(); + } + + final int currVal = curr.getValuePlain(); + curr.setValueRelease(value); + return currVal; + } + + final TableEntry next = curr.getNextPlain(); + if (next != null) { + curr = next; + continue; + } + + final TableEntry insert = new TableEntry(key, value); + + curr.setNextRelease(insert); + this.addToSize(1); + return 0; + } + } + + /** + * {@inheritDoc} + */ + public int put(final int key, final int value) { + return this.put(key, value, false); + } + + /** + * {@inheritDoc} + */ + public int putIfAbsent(final int key, final int value) { + return this.put(key, value, true); + } + + protected final int remove(final int key, final int hash) { + final TableEntry[] table = this.getTablePlain(); + final int index = (table.length - 1) & hash; + + final TableEntry head = table[index]; + if (head == null) { + return 0; + } + + if (head.key == key) { + ArrayUtil.setRelease(table, index, head.getNextPlain()); + this.removeFromSize(1); + + return head.getValuePlain(); + } + + for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { + if (key == curr.key) { + prev.setNextRelease(curr.getNextPlain()); + this.removeFromSize(1); + + return curr.getValuePlain(); + } + } + + return 0; + } + + /** + * {@inheritDoc} + */ + public int remove(final int key) { + return this.remove(key, SWMRInt2IntHashTable.getHash(key)); + } + + /** + * {@inheritDoc} + */ + public void putAll(final SWMRInt2IntHashTable map) { + Validate.notNull(map, "Null map"); + + final int size = map.size(); + this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */ + map.forEach(this::put); + } + + /** + * {@inheritDoc} + *

+ * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself + * is release ordered, that is, after the clear operation is performed a release fence is performed. + *

+ */ + public void clear() { + Arrays.fill(this.getTablePlain(), null); + this.setSizeRelease(0); + } + + public static final class TableEntry { + + protected final int key; + protected int value; + + protected TableEntry next; + + protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class); + protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class); + + /* value */ + + protected final int getValuePlain() { + //noinspection unchecked + return (int)VALUE_HANDLE.get(this); + } + + protected final int getValueAcquire() { + //noinspection unchecked + return (int)VALUE_HANDLE.getAcquire(this); + } + + protected final void setValueRelease(final int to) { + VALUE_HANDLE.setRelease(this, to); + } + + /* next */ + + protected final TableEntry getNextPlain() { + //noinspection unchecked + return (TableEntry)NEXT_HANDLE.get(this); + } + + protected final TableEntry getNextOpaque() { + //noinspection unchecked + return (TableEntry)NEXT_HANDLE.getOpaque(this); + } + + protected final void setNextPlain(final TableEntry next) { + NEXT_HANDLE.set(this, next); + } + + protected final void setNextRelease(final TableEntry next) { + NEXT_HANDLE.setRelease(this, next); + } + + protected TableEntry(final int key, final int value) { + this.key = key; + this.value = value; + } + + public int getKey() { + return this.key; + } + + public int getValue() { + return this.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + public int setValue(final int value) { + final int curr = this.getValuePlain(); + + this.setValueRelease(value); + return curr; + } + + protected static int hash(final int key, final int value) { + return SWMRInt2IntHashTable.getHash(key) ^ SWMRInt2IntHashTable.getHash(value); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return hash(this.key, this.getValueAcquire()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof TableEntry)) { + return false; + } + final TableEntry other = (TableEntry)obj; + final int otherKey = other.getKey(); + final int thisKey = this.getKey(); + final int otherValue = other.getValueAcquire(); + final int thisVal = this.getValueAcquire(); + return (thisKey == otherKey) && (thisVal == otherValue); + } + } + +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java index 1e98f778ffa0a7bb00ebccaaa8bde075183e41f0..aebe82cbe8bc20e5f4260a871d7b620e5092b2c9 100644 --- a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java +++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java @@ -534,6 +534,44 @@ public class SWMRLong2ObjectHashTable { return null; } + protected final V remove(final long key, final int hash, final V expect) { + final TableEntry[] table = this.getTablePlain(); + final int index = (table.length - 1) & hash; + + final TableEntry head = table[index]; + if (head == null) { + return null; + } + + if (head.key == key) { + final V val = head.value; + if (val == expect || val.equals(expect)) { + ArrayUtil.setRelease(table, index, head.getNextPlain()); + this.removeFromSize(1); + + return head.getValuePlain(); + } else { + return null; + } + } + + for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { + if (key == curr.key) { + final V val = curr.value; + if (val == expect || val.equals(expect)) { + prev.setNextRelease(curr.getNextPlain()); + this.removeFromSize(1); + + return curr.getValuePlain(); + } else { + return null; + } + } + } + + return null; + } + /** * {@inheritDoc} */ @@ -541,6 +579,10 @@ public class SWMRLong2ObjectHashTable { return this.remove(key, SWMRLong2ObjectHashTable.getHash(key)); } + public boolean remove(final long key, final V expect) { + return this.remove(key, SWMRLong2ObjectHashTable.getHash(key), expect) != null; + } + /** * {@inheritDoc} */ diff --git a/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java new file mode 100644 index 0000000000000000000000000000000000000000..0ce825d7af2a1dbeac5c22640534ee1901edce20 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java @@ -0,0 +1,543 @@ +package ca.spottedleaf.concurrentutil.scheduler; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.TimeUtil; +import com.mojang.logging.LogUtils; +import io.papermc.paper.util.TraceUtil; +import io.papermc.paper.util.set.LinkedSortedSet; +import org.slf4j.Logger; +import java.lang.invoke.VarHandle; +import java.util.BitSet; +import java.util.Comparator; +import java.util.PriorityQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.LockSupport; +import java.util.function.BooleanSupplier; + +public class SchedulerThreadPool { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public static final long DEADLINE_NOT_SET = Long.MIN_VALUE; + + private static final Comparator TICK_COMPARATOR_BY_TIME = (final SchedulableTick t1, final SchedulableTick t2) -> { + final int timeCompare = TimeUtil.compareTimes(t1.scheduledStart, t2.scheduledStart); + if (timeCompare != 0) { + return timeCompare; + } + + return Long.compare(t1.id, t2.id); + }; + + private final TickThreadRunner[] runners; + private final Thread[] threads; + private final LinkedSortedSet awaiting = new LinkedSortedSet<>(TICK_COMPARATOR_BY_TIME); + private final PriorityQueue queued = new PriorityQueue<>(TICK_COMPARATOR_BY_TIME); + private final BitSet idleThreads; + + private final Object scheduleLock = new Object(); + + private volatile boolean halted; + + public SchedulerThreadPool(final int threads, final ThreadFactory threadFactory) { + final BitSet idleThreads = new BitSet(threads); + for (int i = 0; i < threads; ++i) { + idleThreads.set(i); + } + this.idleThreads = idleThreads; + + final TickThreadRunner[] runners = new TickThreadRunner[threads]; + final Thread[] t = new Thread[threads]; + for (int i = 0; i < threads; ++i) { + runners[i] = new TickThreadRunner(i, this); + t[i] = threadFactory.newThread(runners[i]); + } + + this.threads = t; + this.runners = runners; + } + + /** + * Starts the threads in this pool. + */ + public void start() { + for (final Thread thread : this.threads) { + thread.start(); + } + } + + /** + * Attempts to prevent further execution of tasks, optionally waiting for the scheduler threads to die. + * + * @param sync Whether to wait for the scheduler threads to die. + * @param maxWaitNS The maximum time, in ns, to wait for the scheduler threads to die. + * @return {@code true} if sync was false, or if sync was true and the scheduler threads died before the timeout. + * Otherwise, returns {@code false} if the time elapsed exceeded the maximum wait time. + */ + public boolean halt(final boolean sync, final long maxWaitNS) { + this.halted = true; + for (final Thread thread : this.threads) { + // force response to halt + LockSupport.unpark(thread); + } + final long time = System.nanoTime(); + if (sync) { + // start at 10 * 0.5ms -> 5ms + for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { + boolean allDead = true; + for (final Thread thread : this.threads) { + if (thread.isAlive()) { + allDead = false; + break; + } + } + if (allDead) { + return true; + } + if ((System.nanoTime() - time) >= maxWaitNS) { + return false; + } + } + } + + return true; + } + + public void dumpAliveThreadTraces(final String reason) { + for (final Thread thread : this.threads) { + if (thread.isAlive()) { + TraceUtil.dumpTraceForThread(thread, reason); + } + } + } + + /** + * Returns an array of the underlying scheduling threads. + */ + public Thread[] getThreads() { + return this.threads.clone(); + } + + private void insertFresh(final SchedulableTick task) { + final TickThreadRunner[] runners = this.runners; + + final int firstIdleThread = this.idleThreads.nextSetBit(0); + + if (firstIdleThread != -1) { + // push to idle thread + this.idleThreads.clear(firstIdleThread); + final TickThreadRunner runner = runners[firstIdleThread]; + task.awaitingLink = this.awaiting.addLast(task); + runner.acceptTask(task); + return; + } + + // try to replace the last awaiting task + final SchedulableTick last = this.awaiting.last(); + + if (last != null && TICK_COMPARATOR_BY_TIME.compare(task, last) < 0) { + // need to replace the last task + this.awaiting.pollLast(); + last.awaitingLink = null; + task.awaitingLink = this.awaiting.addLast(task); + // need to add task to queue to be picked up later + this.queued.add(last); + + final TickThreadRunner runner = last.ownedBy; + runner.replaceTask(task); + + return; + } + + // add to queue, will be picked up later + this.queued.add(task); + } + + private void takeTask(final TickThreadRunner runner, final SchedulableTick tick) { + if (!this.awaiting.remove(tick.awaitingLink)) { + throw new IllegalStateException("Task is not in awaiting"); + } + tick.awaitingLink = null; + } + + private SchedulableTick returnTask(final TickThreadRunner runner, final SchedulableTick reschedule) { + if (reschedule != null) { + this.queued.add(reschedule); + } + final SchedulableTick ret = this.queued.poll(); + if (ret == null) { + this.idleThreads.set(runner.id); + } else { + ret.awaitingLink = this.awaiting.addLast(ret); + } + + return ret; + } + + public void schedule(final SchedulableTick task) { + synchronized (this.scheduleLock) { + if (!task.tryMarkScheduled()) { + throw new IllegalStateException("Task " + task + " is already scheduled or cancelled"); + } + + task.schedulerOwnedBy = this; + + this.insertFresh(task); + } + } + + public boolean updateTickStartToMax(final SchedulableTick task, final long newStart) { + synchronized (this.scheduleLock) { + if (TimeUtil.compareTimes(newStart, task.getScheduledStart()) <= 0) { + return false; + } + if (this.queued.remove(task)) { + task.setScheduledStart(newStart); + this.queued.add(task); + return true; + } + if (task.awaitingLink != null) { + this.awaiting.remove(task.awaitingLink); + task.awaitingLink = null; + + // re-queue task + task.setScheduledStart(newStart); + this.queued.add(task); + + // now we need to replace the task the runner was waiting for + final TickThreadRunner runner = task.ownedBy; + final SchedulableTick replace = this.queued.poll(); + + // replace cannot be null, since we have added a task to queued + if (replace != task) { + runner.replaceTask(replace); + } + + return true; + } + + return false; + } + } + + /** + * Returns {@code null} if the task is not scheduled, returns {@code TRUE} if the task was cancelled + * and was queued to execute, returns {@code FALSE} if the task was cancelled but was executing. + */ + public Boolean tryRetire(final SchedulableTick task) { + if (task.schedulerOwnedBy != this) { + return null; + } + + synchronized (this.scheduleLock) { + if (this.queued.remove(task)) { + // cancelled, and no runner owns it - so return + return Boolean.TRUE; + } + if (task.awaitingLink != null) { + this.awaiting.remove(task.awaitingLink); + task.awaitingLink = null; + // here we need to replace the task the runner was waiting for + final TickThreadRunner runner = task.ownedBy; + final SchedulableTick replace = this.queued.poll(); + + if (replace == null) { + // nothing to replace with, set to idle + this.idleThreads.set(runner.id); + runner.forceIdle(); + } else { + runner.replaceTask(replace); + } + + return Boolean.TRUE; + } + + // could not find it in queue + return task.tryMarkCancelled() ? Boolean.FALSE : null; + } + } + + public void notifyTasks(final SchedulableTick task) { + // Not implemented + } + + /** + * Represents a tickable task that can be scheduled into a {@link SchedulerThreadPool}. + *

+ * A tickable task is expected to run on a fixed interval, which is determined by + * the {@link SchedulerThreadPool}. + *

+ *

+ * A tickable task can have intermediate tasks that can be executed before its tick method is ran. Instead of + * the {@link SchedulerThreadPool} parking in-between ticks, the scheduler will instead drain + * intermediate tasks from scheduled tasks. The parsing of intermediate tasks allows the scheduler to take + * advantage of downtime to reduce the intermediate task load from tasks once they begin ticking. + *

+ *

+ * It is guaranteed that {@link #runTick()} and {@link #runTasks(BooleanSupplier)} are never + * invoked in parallel. + * It is required that when intermediate tasks are scheduled, that {@link SchedulerThreadPool#notifyTasks(SchedulableTick)} + * is invoked for any scheduled task - otherwise, {@link #runTasks(BooleanSupplier)} may not be invoked to + * parse intermediate tasks. + *

+ */ + public static abstract class SchedulableTick { + private static final AtomicLong ID_GENERATOR = new AtomicLong(); + public final long id = ID_GENERATOR.getAndIncrement(); + + private static final int SCHEDULE_STATE_NOT_SCHEDULED = 0; + private static final int SCHEDULE_STATE_SCHEDULED = 1; + private static final int SCHEDULE_STATE_CANCELLED = 2; + + private final AtomicInteger scheduled = new AtomicInteger(); + private SchedulerThreadPool schedulerOwnedBy; + private long scheduledStart = DEADLINE_NOT_SET; + private TickThreadRunner ownedBy; + + private LinkedSortedSet.Link awaitingLink; + + private boolean tryMarkScheduled() { + return this.scheduled.compareAndSet(SCHEDULE_STATE_NOT_SCHEDULED, SCHEDULE_STATE_SCHEDULED); + } + + private boolean tryMarkCancelled() { + return this.scheduled.compareAndSet(SCHEDULE_STATE_SCHEDULED, SCHEDULE_STATE_CANCELLED); + } + + private boolean isScheduled() { + return this.scheduled.get() == SCHEDULE_STATE_SCHEDULED; + } + + protected final long getScheduledStart() { + return this.scheduledStart; + } + + /** + * If this task is scheduled, then this may only be invoked during {@link #runTick()}, + * and {@link #runTasks(BooleanSupplier)} + */ + protected final void setScheduledStart(final long value) { + this.scheduledStart = value; + } + + /** + * Executes the tick. + *

+ * It is the callee's responsibility to invoke {@link #setScheduledStart(long)} to adjust the start of + * the next tick. + *

+ * @return {@code true} if the task should continue to be scheduled, {@code false} otherwise. + */ + public abstract boolean runTick(); + + /** + * Returns whether this task has any intermediate tasks that can be executed. + */ + public abstract boolean hasTasks(); + + /** + * Returns {@code null} if this task should not be scheduled, otherwise returns + * {@code Boolean.TRUE} if there are more intermediate tasks to execute and + * {@code Boolean.FALSE} if there are no more intermediate tasks to execute. + */ + public abstract Boolean runTasks(final BooleanSupplier canContinue); + + @Override + public String toString() { + return "SchedulableTick:{" + + "class=" + this.getClass().getName() + "," + + "scheduled_state=" + this.scheduled.get() + "," + + "}"; + } + } + + private static final class TickThreadRunner implements Runnable { + + /** + * There are no tasks in this thread's runqueue, so it is parked. + *

+ * stateTarget = null + *

+ */ + private static final int STATE_IDLE = 0; + + /** + * The runner is waiting to tick a task, as it has no intermediate tasks to execute. + *

+ * stateTarget = the task awaiting tick + *

+ */ + private static final int STATE_AWAITING_TICK = 1; + + /** + * The runner is executing a tick for one of the tasks that was in its runqueue. + *

+ * stateTarget = the task being ticked + *

+ */ + private static final int STATE_EXECUTING_TICK = 2; + + public final int id; + public final SchedulerThreadPool scheduler; + + private volatile Thread thread; + private volatile TickThreadRunnerState state = new TickThreadRunnerState(null, STATE_IDLE); + private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(TickThreadRunner.class, "state", TickThreadRunnerState.class); + + private void setStatePlain(final TickThreadRunnerState state) { + STATE_HANDLE.set(this, state); + } + + private void setStateOpaque(final TickThreadRunnerState state) { + STATE_HANDLE.setOpaque(this, state); + } + + private void setStateVolatile(final TickThreadRunnerState state) { + STATE_HANDLE.setVolatile(this, state); + } + + private static record TickThreadRunnerState(SchedulableTick stateTarget, int state) {} + + public TickThreadRunner(final int id, final SchedulerThreadPool scheduler) { + this.id = id; + this.scheduler = scheduler; + } + + private Thread getRunnerThread() { + return this.thread; + } + + private void acceptTask(final SchedulableTick task) { + if (task.ownedBy != null) { + throw new IllegalStateException("Already owned by another runner"); + } + task.ownedBy = this; + final TickThreadRunnerState state = this.state; + if (state.state != STATE_IDLE) { + throw new IllegalStateException("Cannot accept task in state " + state); + } + this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK)); + LockSupport.unpark(this.getRunnerThread()); + } + + private void replaceTask(final SchedulableTick task) { + final TickThreadRunnerState state = this.state; + if (state.state != STATE_AWAITING_TICK) { + throw new IllegalStateException("Cannot replace task in state " + state); + } + if (task.ownedBy != null) { + throw new IllegalStateException("Already owned by another runner"); + } + task.ownedBy = this; + + state.stateTarget.ownedBy = null; + + this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK)); + LockSupport.unpark(this.getRunnerThread()); + } + + private void forceIdle() { + final TickThreadRunnerState state = this.state; + if (state.state != STATE_AWAITING_TICK) { + throw new IllegalStateException("Cannot replace task in state " + state); + } + state.stateTarget.ownedBy = null; + this.setStateOpaque(new TickThreadRunnerState(null, STATE_IDLE)); + // no need to unpark + } + + private boolean takeTask(final TickThreadRunnerState state, final SchedulableTick task) { + synchronized (this.scheduler.scheduleLock) { + if (this.state != state) { + return false; + } + this.setStatePlain(new TickThreadRunnerState(task, STATE_EXECUTING_TICK)); + this.scheduler.takeTask(this, task); + return true; + } + } + + private void returnTask(final SchedulableTick task, final boolean reschedule) { + synchronized (this.scheduler.scheduleLock) { + task.ownedBy = null; + + final SchedulableTick newWait = this.scheduler.returnTask(this, reschedule && task.isScheduled() ? task : null); + if (newWait == null) { + this.setStatePlain(new TickThreadRunnerState(null, STATE_IDLE)); + } else { + if (newWait.ownedBy != null) { + throw new IllegalStateException("Already owned by another runner"); + } + newWait.ownedBy = this; + this.setStatePlain(new TickThreadRunnerState(newWait, STATE_AWAITING_TICK)); + } + } + } + + @Override + public void run() { + this.thread = Thread.currentThread(); + + main_state_loop: + for (;;) { + final TickThreadRunnerState startState = this.state; + final int startStateType = startState.state; + final SchedulableTick startStateTask = startState.stateTarget; + + if (this.scheduler.halted) { + return; + } + + switch (startStateType) { + case STATE_IDLE: { + while (this.state.state == STATE_IDLE) { + LockSupport.park(); + if (this.scheduler.halted) { + return; + } + } + continue main_state_loop; + } + + case STATE_AWAITING_TICK: { + final long deadline = startStateTask.getScheduledStart(); + for (;;) { + if (this.state != startState) { + continue main_state_loop; + } + final long diff = deadline - System.nanoTime(); + if (diff <= 0L) { + break; + } + LockSupport.parkNanos(startState, diff); + if (this.scheduler.halted) { + return; + } + } + + if (!this.takeTask(startState, startStateTask)) { + continue main_state_loop; + } + + // TODO exception handling + final boolean reschedule = startStateTask.runTick(); + + this.returnTask(startStateTask, reschedule); + + continue main_state_loop; + } + + case STATE_EXECUTING_TICK: { + throw new IllegalStateException("Tick execution must be set by runner thread, not by any other thread"); + } + + default: { + throw new IllegalStateException("Unknown state: " + startState); + } + } + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..63688716244066581d5b505703576e3340e3baf3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java @@ -0,0 +1,60 @@ +package ca.spottedleaf.concurrentutil.util; + +public final class TimeUtil { + + /* + * The comparator is not a valid comparator for every long value. To prove where it is valid, see below. + * + * For reflexivity, we have that x - x = 0. We then have that for any long value x that + * compareTimes(x, x) == 0, as expected. + * + * For symmetry, we have that x - y = -(y - x) except for when y - x = Long.MIN_VALUE. + * So, the difference between any times x and y must not be equal to Long.MIN_VALUE. + * + * As for the transitive relation, consider we have x,y such that x - y = a > 0 and z such that + * y - z = b > 0. Then, we will have that the x - z > 0 is equivalent to a + b > 0. For long values, + * this holds as long as a + b <= Long.MAX_VALUE. + * + * Also consider we have x, y such that x - y = a < 0 and z such that y - z = b < 0. Then, we will have + * that x - z < 0 is equivalent to a + b < 0. For long values, this holds as long as a + b >= -Long.MAX_VALUE. + * + * Thus, the comparator is only valid for timestamps such that abs(c - d) <= Long.MAX_VALUE for all timestamps + * c and d. + */ + + /** + * This function is appropriate to be used as a {@link java.util.Comparator} between two timestamps, which + * indicates whether the timestamps represented by t1, t2 that t1 is before, equal to, or after t2. + */ + public static int compareTimes(final long t1, final long t2) { + final long diff = t1 - t2; + + // HD, Section 2-7 + return (int) ((diff >> 63) | (-diff >>> 63)); + } + + public static long getGreatestTime(final long t1, final long t2) { + final long diff = t1 - t2; + return diff < 0L ? t2 : t1; + } + + public static long getLeastTime(final long t1, final long t2) { + final long diff = t1 - t2; + return diff > 0L ? t2 : t1; + } + + public static long clampTime(final long value, final long min, final long max) { + final long diffMax = value - max; + final long diffMin = value - min; + + if (diffMax > 0L) { + return max; + } + if (diffMin < 0L) { + return min; + } + return value; + } + + private TimeUtil() {} +} diff --git a/src/main/java/ca/spottedleaf/leafprofiler/LProfileGraph.java b/src/main/java/ca/spottedleaf/leafprofiler/LProfileGraph.java new file mode 100644 index 0000000000000000000000000000000000000000..14a4778f7913b849fabbd772f9cb8a0bc5a6ed6c --- /dev/null +++ b/src/main/java/ca/spottedleaf/leafprofiler/LProfileGraph.java @@ -0,0 +1,58 @@ +package ca.spottedleaf.leafprofiler; + +import ca.spottedleaf.concurrentutil.map.SWMRInt2IntHashTable; +import java.util.Arrays; + +public final class LProfileGraph { + + public static final int ROOT_NODE = 0; + + // volatile required for correct publishing after resizing + private volatile SWMRInt2IntHashTable[] nodes = new SWMRInt2IntHashTable[16]; + private int nodeCount; + + public LProfileGraph() { + this.nodes[ROOT_NODE] = new SWMRInt2IntHashTable(); + this.nodeCount = 1; + } + + private int createNode(final int parent, final int type) { + synchronized (this) { + SWMRInt2IntHashTable[] nodes = this.nodes; + + final SWMRInt2IntHashTable node = nodes[parent]; + + final int newNode = this.nodeCount; + final int prev = node.putIfAbsent(type, newNode); + + if (prev != 0) { + // already exists + return prev; + } + + // insert new node + ++this.nodeCount; + + if (newNode >= nodes.length) { + this.nodes = nodes = Arrays.copyOf(nodes, nodes.length * 2); + } + + nodes[newNode] = new SWMRInt2IntHashTable(); + + return newNode; + } + } + + public int getOrCreateNode(final int parent, final int type) { + // note: requires parent node to exist + final SWMRInt2IntHashTable[] nodes = this.nodes; + + final int mapping = nodes[parent].get(type); + + if (mapping != 0) { + return mapping; + } + + return this.createNode(parent, type); + } +} diff --git a/src/main/java/ca/spottedleaf/leafprofiler/LProfilerRegistry.java b/src/main/java/ca/spottedleaf/leafprofiler/LProfilerRegistry.java new file mode 100644 index 0000000000000000000000000000000000000000..ffa32c1eae22bda371dd1d0318cc7c587f8e5a5c --- /dev/null +++ b/src/main/java/ca/spottedleaf/leafprofiler/LProfilerRegistry.java @@ -0,0 +1,59 @@ +package ca.spottedleaf.leafprofiler; + +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; + +public final class LProfilerRegistry { + + // volatile required to ensure correct publishing when resizing + private volatile ProfilerEntry[] typesById = new ProfilerEntry[16]; + private int totalEntries; + private final ConcurrentHashMap nameToEntry = new ConcurrentHashMap<>(); + + public LProfilerRegistry() { + + } + + public ProfilerEntry getById(final int id) { + final ProfilerEntry[] entries = this.typesById; + + return id < 0 || id >= entries.length ? null : entries[id]; + } + + public ProfilerEntry getByName(final String name) { + return this.nameToEntry.get(name); + } + + public int createType(final ProfileType type, final String name) { + synchronized (this) { + final int id = this.totalEntries; + + final ProfilerEntry ret = new ProfilerEntry(type, name, id); + + final ProfilerEntry prev = this.nameToEntry.putIfAbsent(name, ret); + + if (prev != null) { + throw new IllegalStateException("Entry already exists: " + prev); + } + + ++this.totalEntries; + + ProfilerEntry[] entries = this.typesById; + + if (id >= entries.length) { + this.typesById = entries = Arrays.copyOf(entries, entries.length * 2); + } + + // should be opaque, but I don't think that matters here. + entries[id] = ret; + + return id; + } + } + + public static enum ProfileType { + TIMER, COUNTER + } + + public static record ProfilerEntry(ProfileType type, String name, int id) {} +} diff --git a/src/main/java/ca/spottedleaf/leafprofiler/LeafProfiler.java b/src/main/java/ca/spottedleaf/leafprofiler/LeafProfiler.java new file mode 100644 index 0000000000000000000000000000000000000000..ad8c590fe7479fcb3c7ff5dc3ac3a4d6f33c5938 --- /dev/null +++ b/src/main/java/ca/spottedleaf/leafprofiler/LeafProfiler.java @@ -0,0 +1,61 @@ +package ca.spottedleaf.leafprofiler; + +import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; +import java.util.Arrays; + +public final class LeafProfiler { + + public final LProfilerRegistry registry; + public final LProfileGraph graph; + + private long[] data; + private final IntArrayFIFOQueue callStack = new IntArrayFIFOQueue(); + private int topOfStack = LProfileGraph.ROOT_NODE; + private final LongArrayFIFOQueue timerStack = new LongArrayFIFOQueue(); + private long lastTimerStart = 0L; + + public LeafProfiler(final LProfilerRegistry registry, final LProfileGraph graph) { + this.registry = registry; + this.graph = graph; + } + + private long[] resizeData(final long[] old, final int least) { + return this.data = Arrays.copyOf(old, Math.max(old.length * 2, least * 2)); + } + + private void incrementDirect(final int nodeId, final long count) { + final long[] data = this.data; + if (nodeId >= data.length) { + this.resizeData(data, nodeId)[nodeId] += count; + } else { + data[nodeId] += count; + } + } + + public void incrementCounter(final int type, final long count) { + // this is supposed to be an optimised version of startTimer then stopTimer + final int node = this.graph.getOrCreateNode(this.topOfStack, type); + this.incrementDirect(node, count); + } + + public void startTimer(final int type, final long startTime) { + final int parentNode = this.topOfStack; + final int newNode = this.graph.getOrCreateNode(parentNode, type); + this.callStack.enqueue(parentNode); + this.topOfStack = newNode; + + this.timerStack.enqueue(this.lastTimerStart); + this.lastTimerStart = startTime; + } + + public void stopTimer(final int type, final long endTime) { + final int currentNode = this.topOfStack; + this.topOfStack = this.callStack.dequeueLastInt(); + + final long lastStart = this.lastTimerStart; + this.lastTimerStart = this.timerStack.dequeueLastLong(); + + this.incrementDirect(currentNode, endTime - lastStart); + } +} diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java index cab91880a08c6fdc545804911d295e0f24f4d983..8f2bff24256f0946f38e1d8b0ae9c8c921b0a2db 100644 --- a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java +++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java @@ -179,11 +179,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo return; } - if (!Bukkit.isPrimaryThread()) { - // Plugins? - MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo)); - return; - } + // Folia - region threading LevelChunk chunk = chunkPacketInfo.getChunk(); int x = chunk.getPos().x; diff --git a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java index 22a2547810d0c029f29685faddf7ac21cde2df0b..e36b4053eb2676e934b8c9c401bf58cfa7dd969c 100644 --- a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java +++ b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java @@ -832,14 +832,14 @@ public class RedstoneWireTurbo { j = getMaxCurrentStrength(upd, j); int l = 0; - wire.shouldSignal = false; + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading // Unfortunately, World.isBlockIndirectlyGettingPowered is complicated, // and I'm not ready to try to replicate even more functionality from // elsewhere in Minecraft into this accelerator. So sadly, we must // suffer the performance hit of this very expensive call. If there // is consistency to what this call returns, we may be able to cache it. final int k = worldIn.getBestNeighborSignal(upd.self); - wire.shouldSignal = true; + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading // The variable 'k' holds the maximum redstone power value of any adjacent blocks. // If 'k' has the highest level of all neighbors, then the power level of this diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java index 4d9bc4a62ebae0f3707900503576c64733de639f..9ba89a4bf4ae4e42418eca18f9d8a109e6c56a14 100644 --- a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java +++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java @@ -98,7 +98,7 @@ public final class ChatProcessor { final CraftPlayer player = this.player.getBukkitEntity(); final AsyncPlayerChatEvent ae = new AsyncPlayerChatEvent(this.async, player, this.craftbukkit$originalMessage, new LazyPlayerSet(this.server)); this.post(ae); - if (listenersOnSyncEvent) { + if (false && listenersOnSyncEvent) { // Folia - region threading final PlayerChatEvent se = new PlayerChatEvent(player, ae.getMessage(), ae.getFormat(), ae.getRecipients()); se.setCancelled(ae.isCancelled()); // propagate cancelled state this.queueIfAsyncOrRunImmediately(new Waitable() { @@ -178,7 +178,7 @@ public final class ChatProcessor { ae.setCancelled(cancelled); // propagate cancelled state this.post(ae); final boolean listenersOnSyncEvent = canYouHearMe(ChatEvent.getHandlerList()); - if (listenersOnSyncEvent) { + if (false && listenersOnSyncEvent) { // Folia - region threading this.queueIfAsyncOrRunImmediately(new Waitable() { @Override protected Void evaluate() { diff --git a/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java index 3c17001bcd3862a76a22df488bff80a0ff4d1b83..b2fffaa862df045bacb346f3cbe7eb963e104e47 100644 --- a/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java +++ b/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java @@ -23,35 +23,42 @@ public class ClickCallbackProviderImpl implements ClickCallback.Provider { public static final class CallbackManager { - private final Map callbacks = new HashMap<>(); - private final Queue queue = new ConcurrentLinkedQueue<>(); + private final java.util.concurrent.ConcurrentHashMap callbacks = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading + // Folia - region threading private CallbackManager() { } public UUID addCallback(final @NotNull ClickCallback callback, final ClickCallback.@NotNull Options options) { final UUID id = UUID.randomUUID(); - this.queue.add(new StoredCallback(callback, options, id)); + final StoredCallback scb = new StoredCallback(callback, options, id); // Folia - region threading + this.callbacks.put(scb.id(), scb); // Folia - region threading return id; } public void handleQueue(final int currentTick) { // Evict expired entries if (currentTick % 100 == 0) { - this.callbacks.values().removeIf(callback -> !callback.valid()); + this.callbacks.values().removeIf(StoredCallback::expired); // Folia - region threading - don't read uses field } - // Add entries from queue - StoredCallback callback; - while ((callback = this.queue.poll()) != null) { - this.callbacks.put(callback.id(), callback); - } + // Folia - region threading } public void runCallback(final @NotNull Audience audience, final UUID id) { - final StoredCallback callback = this.callbacks.get(id); - if (callback != null && callback.valid()) { //TODO Message if expired/invalid? - callback.takeUse(); + // Folia start - region threading + final StoredCallback[] use = new StoredCallback[1]; + this.callbacks.computeIfPresent(id, (final UUID keyInMap, final StoredCallback value) -> { + if (!value.valid()) { + return null; + } + use[0] = value; + value.takeUse(); + return value.valid() ? value : null; + }); + final StoredCallback callback = use[0]; + if (callback != null) { //TODO Message if expired/invalid? + // Folia end - region threading callback.callback.accept(audience); } } diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java index 61d03808c8d1ab822d9b2f31fab0de14089a3b15..370e649a255a456d7f901b22e26241e135009af7 100644 --- a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java +++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java @@ -91,6 +91,9 @@ public final class ChunkSystem { for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) { chunkMap.regionManagers.get(index).addChunk(holder.pos.x, holder.pos.z); } + // Folia start - threaded regions + level.regioniser.addChunk(holder.pos.x, holder.pos.z); + // Folia end - threaded regions } public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { @@ -98,30 +101,34 @@ public final class ChunkSystem { for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) { chunkMap.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z); } + // Folia start - threaded regions + level.regioniser.removeChunk(holder.pos.x, holder.pos.z); + // Folia end - threaded regions } public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { chunk.playerChunk = holder; + chunk.level.getCurrentWorldData().addChunk(chunk); // Folia - region threading } public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { - + chunk.level.getCurrentWorldData().removeChunk(chunk); // Folia - region threading } public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { - chunk.level.getChunkSource().tickingChunks.add(chunk); + // Folia - region threading } public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { - chunk.level.getChunkSource().tickingChunks.remove(chunk); + // Folia - region threading } public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { - chunk.level.getChunkSource().entityTickingChunks.add(chunk); + chunk.level.getCurrentWorldData().addEntityTickingChunk(chunk); // Folia - region threading } public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { - chunk.level.getChunkSource().entityTickingChunks.remove(chunk); + chunk.level.getCurrentWorldData().removeEntityTickingChunk(chunk); // Folia - region threading } public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { diff --git a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java index 99dc2038aa4fb0f70dc3670e451018ff541d3a85..5b4025178c1e476ed5dd0808cc33bf1ec7c08b66 100644 --- a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java +++ b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java @@ -232,17 +232,19 @@ public class RegionizedPlayerChunkLoader { public void tick() { TickThread.ensureTickThread("Cannot tick player chunk loader async"); long currTime = System.nanoTime(); - for (final ServerPlayer player : this.world.players()) { + for (final ServerPlayer player : this.world.getLocalPlayers()) { // Folia - region threding player.chunkLoader.update(); player.chunkLoader.midTickUpdate(currTime); } } - public void tickMidTick() { + public boolean tickMidTick() { // Folia - region threading - report whether tickets were added final long time = System.nanoTime(); - for (final ServerPlayer player : this.world.players()) { - player.chunkLoader.midTickUpdate(time); + boolean ret = false; // Folia - region threading - report whether tickets were added + for (final ServerPlayer player : this.world.getLocalPlayers()) { // Folia - region threading + ret |= player.chunkLoader.midTickUpdate(time); } + return ret; // Folia - region threading - report whether tickets were added } private static long[] generateBFSOrder(final int radius) { @@ -384,13 +386,13 @@ public class RegionizedPlayerChunkLoader { this.player = player; } - private void flushDelayedTicketOps() { + private boolean flushDelayedTicketOps() { // Folia - region threading - report whether tickets were added if (this.delayedTicketOps.isEmpty()) { - return; + return false; // Folia - region threading - report whether tickets were added } this.world.chunkTaskScheduler.chunkHolderManager.pushDelayedTicketUpdates(this.delayedTicketOps); this.delayedTicketOps.clear(); - this.world.chunkTaskScheduler.chunkHolderManager.tryDrainTicketUpdates(); + return this.world.chunkTaskScheduler.chunkHolderManager.tryDrainTicketUpdates() == Boolean.TRUE; // Folia - region threading - report whether tickets were added } private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation op) { @@ -607,7 +609,7 @@ public class RegionizedPlayerChunkLoader { return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance; } - void midTickUpdate(final long time) { + boolean midTickUpdate(final long time) { // Folia - region threading - report whether tickets were added TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async"); // update rate limits this.chunkSendCounter.update(time); @@ -799,7 +801,7 @@ public class RegionizedPlayerChunkLoader { this.chunkSendCounter.addTime(time, sendSlots); } - this.flushDelayedTicketOps(); + return this.flushDelayedTicketOps(); // Folia - region threading - report whether tickets were added // we assume propagate ticket levels happens after this call } diff --git a/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java index 61c170555c8854b102c640b0b6a615f9f732edbf..515cc130a411f218ed20628eb918be9d770b9939 100644 --- a/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java +++ b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java @@ -187,7 +187,12 @@ public final class EntityLookup implements LevelEntityGetter { @Override public Iterable getAll() { - return new ArrayIterable<>(this.accessibleEntities.getRawData(), 0, this.accessibleEntities.size()); + // Folia start - region threading + synchronized (this.accessibleEntities) { + Entity[] iterate = java.util.Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size()); + return new ArrayIterable<>(iterate, 0, iterate.length); + } + // Folia end - region threading } @Override @@ -261,7 +266,9 @@ public final class EntityLookup implements LevelEntityGetter { if (newVisibility.ordinal() > oldVisibility.ordinal()) { // status upgrade if (!oldVisibility.isAccessible() && newVisibility.isAccessible()) { + synchronized (this.accessibleEntities) { // Folia - region threading this.accessibleEntities.add(entity); + } // Folia - region threading EntityLookup.this.worldCallback.onTrackingStart(entity); } @@ -275,7 +282,9 @@ public final class EntityLookup implements LevelEntityGetter { } if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) { + synchronized (this.accessibleEntities) { // Folia - region threading this.accessibleEntities.remove(entity); + } // Folia - region threading EntityLookup.this.worldCallback.onTrackingEnd(entity); } } @@ -385,11 +394,26 @@ public final class EntityLookup implements LevelEntityGetter { entity.setLevelCallback(new EntityCallback(entity)); + this.world.getCurrentWorldData().addEntity(entity); // Folia - region threading + this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false); return true; } + // Folia start - region threading + // only appropriate to use when in shutdown, as this performs no logic hooks to properly add to world + public boolean addEntityForShutdownTeleportComplete(final Entity entity) { + final BlockPos pos = entity.blockPosition(); + final int sectionX = pos.getX() >> 4; + final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection); + final int sectionZ = pos.getZ() >> 4; + final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); + + return slices.addEntity(entity, sectionY); + } + // Folia end - region threading + private void removeEntity(final Entity entity) { final int sectionX = entity.sectionX; final int sectionY = entity.sectionY; @@ -407,6 +431,7 @@ public final class EntityLookup implements LevelEntityGetter { LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")"); } } + entity.sectionX = entity.sectionY = entity.sectionZ = Integer.MIN_VALUE; this.entityByLock.writeLock(); @@ -823,6 +848,9 @@ public final class EntityLookup implements LevelEntityGetter { EntityLookup.this.entityStatusChange(entity, null, tickingState, Visibility.HIDDEN, false, false, reason.shouldDestroy()); this.entity.setLevelCallback(NoOpCallback.INSTANCE); + + // only AFTER full removal callbacks, so that thread checking will work. // Folia - region threading + EntityLookup.this.world.getCurrentWorldData().removeEntity(entity); // Folia - region threading } } diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java index c6d20bc2f0eab737338db6b88dacb63f0decb66c..9ac75b6c9d9698c6369978c4b004a82aa2b747f4 100644 --- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java @@ -3,7 +3,6 @@ package io.papermc.paper.chunk.system.scheduling; import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; -import co.aikar.timings.Timing; import com.google.common.collect.ImmutableList; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -19,10 +18,12 @@ import it.unimi.dsi.fastutil.longs.Long2IntMap; import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongIterator; import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; import net.minecraft.nbt.CompoundTag; import io.papermc.paper.chunk.system.ChunkSystem; import net.minecraft.server.MinecraftServer; @@ -34,8 +35,6 @@ import net.minecraft.server.level.TicketType; import net.minecraft.util.SortedArraySet; import net.minecraft.util.Unit; import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.ChunkStatus; import org.bukkit.plugin.Plugin; import org.slf4j.Logger; import java.io.IOException; @@ -54,6 +53,13 @@ import java.util.concurrent.locks.LockSupport; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; +// Folia start - region threading +import io.papermc.paper.threadedregions.RegionizedServer; +import io.papermc.paper.threadedregions.ThreadedRegionizer; +import io.papermc.paper.threadedregions.TickRegionScheduler; +import io.papermc.paper.threadedregions.TickRegions; +// Folia end - region threading + public final class ChunkHolderManager { private static final Logger LOGGER = LogUtils.getClassLogger(); @@ -63,40 +69,198 @@ public final class ChunkHolderManager { public static final int ENTITY_TICKING_TICKET_LEVEL = 31; public static final int MAX_TICKET_LEVEL = ChunkMap.MAX_CHUNK_DISTANCE; // inclusive - private static final long NO_TIMEOUT_MARKER = -1L; + // Folia start - region threading + private static final long NO_TIMEOUT_MARKER = Long.MIN_VALUE; + private static final long PROBE_MARKER = Long.MIN_VALUE + 1; + // special region threading fields + // this field contains chunk holders that were created in addTicketAtLevel + // because the chunk holders were created without a reliable unload hook (i.e creation for entity/poi loading, + // which always check for unload after their tasks finish) we need to do that ourselves later + private final ReferenceOpenHashSet specialCaseUnload = new ReferenceOpenHashSet<>(); + // Folia end - region threading - final ReentrantLock ticketLock = new ReentrantLock(); + public final ReentrantLock ticketLock = new ReentrantLock(); // Folia - region threading private final SWMRLong2ObjectHashTable chunkHolders = new SWMRLong2ObjectHashTable<>(16384, 0.25f); - private final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap<>(8192, 0.25f); - // what a disaster of a name - // this is a map of removal tick to a map of chunks and the number of tickets a chunk has that are to expire that tick - private final Long2ObjectOpenHashMap removeTickToChunkExpireTicketCount = new Long2ObjectOpenHashMap<>(); + // Folia - region threading private final ServerLevel world; private final ChunkTaskScheduler taskScheduler; - private long currentTick; - private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); - private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { - if (c1 == c2) { - return 0; + // Folia start - region threading + public static final class HolderManagerRegionData { + /* + * This region data is a bit of a mess, because it is part global state and part region state. + * Typically for region state we do not need to worry about threading concerns because it is only + * accessed by the current region when ticking. But since this contains state ( + * tickets, and removeTickToChunkExpireTicketCount) that can be written to by any thread holding the + * ticket lock, the merge logic is complicated as merging only holds the region lock. So, Folia has modified + * the add and remove ticket functions to acquire the region lock if the current region does not own the target + * position. + */ + private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); + private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { + if (c1 == c2) { + return 0; + } + + final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); + + if (saveTickCompare != 0) { + return saveTickCompare; + } + + final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); + final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); + + if (coord1 == coord2) { + throw new IllegalStateException("Duplicate chunkholder in auto save queue"); + } + + return Long.compare(coord1, coord2); + }); + private long currentTick; + private final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap<>(8192, 0.25f); + // what a disaster of a name + // this is a map of removal tick to a map of chunks and the number of tickets a chunk has that are to expire that tick + private final Long2ObjectOpenHashMap removeTickToChunkExpireTicketCount = new Long2ObjectOpenHashMap<>(); + + public void merge(final HolderManagerRegionData into, final long tickOffset) { + // Order doesn't really matter for the pending full update... + into.pendingFullLoadUpdate.addAll(this.pendingFullLoadUpdate); + + // We need to copy the set to iterate over, because modifying the field used in compareTo while iterating + // will destroy the result from compareTo (However, the set is not destroyed _after_ iteration because a constant + // addition to every entry will not affect compareTo). + for (final NewChunkHolder holder : new ArrayList<>(this.autoSaveQueue)) { + holder.lastAutoSave += tickOffset; + into.autoSaveQueue.add(holder); + } + + final long chunkManagerTickOffset = into.currentTick - this.currentTick; + for (final Iterator>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry>> entry = iterator.next(); + final SortedArraySet> oldTickets = entry.getValue(); + final SortedArraySet> newTickets = SortedArraySet.create(Math.max(4, oldTickets.size() + 1)); + for (final Ticket ticket : oldTickets) { + newTickets.add( + new Ticket(ticket.getType(), ticket.getTicketLevel(), ticket.key, + ticket.removalTick == NO_TIMEOUT_MARKER ? NO_TIMEOUT_MARKER : ticket.removalTick + chunkManagerTickOffset) + ); + } + into.tickets.put(entry.getLongKey(), newTickets); + } + for (final Iterator> iterator = this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry entry = iterator.next(); + into.removeTickToChunkExpireTicketCount.merge( + (long)(entry.getLongKey() + chunkManagerTickOffset), entry.getValue(), + (final Long2IntOpenHashMap t, final Long2IntOpenHashMap f) -> { + for (final Iterator itr = f.long2IntEntrySet().fastIterator(); itr.hasNext();) { + final Long2IntMap.Entry e = itr.next(); + t.addTo(e.getLongKey(), e.getIntValue()); + } + return t; + } + ); + } + } + + public void split(final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData, + final ReferenceOpenHashSet dataSet) { + for (final NewChunkHolder fullLoadUpdate : this.pendingFullLoadUpdate) { + final int regionCoordinateX = fullLoadUpdate.chunkX >> chunkToRegionShift; + final int regionCoordinateZ = fullLoadUpdate.chunkZ >> chunkToRegionShift; + + final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); + if (data != null) { + data.pendingFullLoadUpdate.add(fullLoadUpdate); + } // else: fullLoadUpdate is an unloaded chunk holder + } + + for (final NewChunkHolder autoSave : this.autoSaveQueue) { + final int regionCoordinateX = autoSave.chunkX >> chunkToRegionShift; + final int regionCoordinateZ = autoSave.chunkZ >> chunkToRegionShift; + + final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); + if (data != null) { + data.autoSaveQueue.add(autoSave); + } // else: autoSave is an unloaded chunk holder + } + for (final HolderManagerRegionData data : dataSet) { + data.currentTick = this.currentTick; + } + for (final Iterator>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry>> entry = iterator.next(); + final long chunkKey = entry.getLongKey(); + final int regionCoordinateX = CoordinateUtils.getChunkX(chunkKey) >> chunkToRegionShift; + final int regionCoordinateZ = CoordinateUtils.getChunkZ(chunkKey) >> chunkToRegionShift; + + // can never be null, since a chunk holder exists if the ticket set is not empty + regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)).tickets.put(chunkKey, entry.getValue()); + } + for (final Iterator> iterator = this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry entry = iterator.next(); + final long tick = entry.getLongKey(); + final Long2IntOpenHashMap chunkToCount = entry.getValue(); + + for (final Iterator itr = chunkToCount.long2IntEntrySet().fastIterator(); itr.hasNext();) { + final Long2IntMap.Entry e = itr.next(); + final long chunkKey = e.getLongKey(); + final int regionCoordinateX = CoordinateUtils.getChunkX(chunkKey) >> chunkToRegionShift; + final int regionCoordinateZ = CoordinateUtils.getChunkZ(chunkKey) >> chunkToRegionShift; + final int count = e.getIntValue(); + + // can never be null, since a chunk holder exists if the ticket set is not empty + final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); + + data.removeTickToChunkExpireTicketCount.computeIfAbsent(tick, (final long keyInMap) -> { + return new Long2IntOpenHashMap(); + }).put(chunkKey, count); + } + } + } + } + + private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() { + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + + if (region == null) { + return null; + } + + if (this.world != null && this.world != region.getData().world) { + throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey()); } - final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); + return region.getData().getHolderManagerRegionData(); + } + + // MUST hold ticket lock + private ChunkHolderManager.HolderManagerRegionData getDataFor(final long key) { + return this.getDataFor(CoordinateUtils.getChunkX(key), CoordinateUtils.getChunkZ(key)); + } - if (saveTickCompare != 0) { - return saveTickCompare; + // MUST hold ticket lock + private ChunkHolderManager.HolderManagerRegionData getDataFor(final int chunkX, final int chunkZ) { + if (!this.ticketLock.isHeldByCurrentThread()) { + throw new IllegalStateException("Must hold ticket level lock"); } - final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); - final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); + final ThreadedRegionizer.ThreadedRegion region + = this.world.regioniser.getRegionAtUnsynchronised(chunkX, chunkZ); - if (coord1 == coord2) { - throw new IllegalStateException("Duplicate chunkholder in auto save queue"); + if (region == null) { + return null; } - return Long.compare(coord1, coord2); - }); + return region.getData().getHolderManagerRegionData(); + } + // Folia end - region threading + public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { this.world = world; @@ -129,8 +293,13 @@ public final class ChunkHolderManager { } public void close(final boolean save, final boolean halt) { + // Folia start - region threading + this.close(save, halt, true, true, true); + } + public void close(final boolean save, final boolean halt, final boolean first, final boolean last, final boolean checkRegions) { + // Folia end - region threading TickThread.ensureTickThread("Closing world off-main"); - if (halt) { + if (first && halt) { // Folia - region threading LOGGER.info("Waiting 60s for chunk system to halt for world '" + this.world.getWorld().getName() + "'"); if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) { LOGGER.warn("Failed to halt world generation/loading tasks for world '" + this.world.getWorld().getName() + "'"); @@ -140,9 +309,10 @@ public final class ChunkHolderManager { } if (save) { - this.saveAllChunks(true, true, true); + this.saveAllChunksRegionised(true, true, true, first, last, checkRegions); // Folia - region threading } + if (last) { // Folia - region threading if (this.world.chunkDataControllerNew.hasTasks() || this.world.entityDataControllerNew.hasTasks() || this.world.poiDataControllerNew.hasTasks()) { RegionFileIOThread.flush(); } @@ -163,27 +333,34 @@ public final class ChunkHolderManager { } catch (final IOException ex) { LOGGER.error("Failed to close poi regionfile cache for world '" + this.world.getWorld().getName() + "'", ex); } + } // Folia - region threading } void ensureInAutosave(final NewChunkHolder holder) { - if (!this.autoSaveQueue.contains(holder)) { - holder.lastAutoSave = MinecraftServer.currentTick; - this.autoSaveQueue.add(holder); + // Folia start - region threading + final HolderManagerRegionData regionData = this.getCurrentRegionData(); + if (!regionData.autoSaveQueue.contains(holder)) { + holder.lastAutoSave = RegionizedServer.getCurrentTick(); + // Folia end - region threading + regionData.autoSaveQueue.add(holder); } } public void autoSave() { final List reschedule = new ArrayList<>(); - final long currentTick = MinecraftServer.currentTickLong; + final long currentTick = RegionizedServer.getCurrentTick(); final long maxSaveTime = currentTick - this.world.paperConfig().chunks.autoSaveInterval.value(); - for (int autoSaved = 0; autoSaved < this.world.paperConfig().chunks.maxAutoSaveChunksPerTick && !this.autoSaveQueue.isEmpty();) { - final NewChunkHolder holder = this.autoSaveQueue.first(); + // Folia start - region threading + final HolderManagerRegionData regionData = this.getCurrentRegionData(); + for (int autoSaved = 0; autoSaved < this.world.paperConfig().chunks.maxAutoSaveChunksPerTick && !regionData.autoSaveQueue.isEmpty();) { + // Folia end - region threading + final NewChunkHolder holder = regionData.autoSaveQueue.first(); if (holder.lastAutoSave > maxSaveTime) { break; } - this.autoSaveQueue.remove(holder); + regionData.autoSaveQueue.remove(holder); holder.lastAutoSave = currentTick; if (holder.save(false, false) != null) { @@ -197,15 +374,20 @@ public final class ChunkHolderManager { for (final NewChunkHolder holder : reschedule) { if (holder.getChunkStatus().isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { - this.autoSaveQueue.add(holder); + regionData.autoSaveQueue.add(holder); } } } public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) { + // Folia start - region threading + this.saveAllChunksRegionised(flush, shutdown, logProgress, true, true, true); + } + public void saveAllChunksRegionised(final boolean flush, final boolean shutdown, final boolean logProgress, final boolean first, final boolean last, final boolean checkRegion) { + // Folia end - region threading final List holders = this.getChunkHolders(); - if (logProgress) { + if (first && logProgress) { // Folia - region threading LOGGER.info("Saving all chunkholders for world '" + this.world.getWorld().getName() + "'"); } @@ -213,7 +395,7 @@ public final class ChunkHolderManager { int saved = 0; - long start = System.nanoTime(); + final long start = System.nanoTime(); long lastLog = start; boolean needsFlush = false; final int flushInterval = 50; @@ -224,6 +406,12 @@ public final class ChunkHolderManager { for (int i = 0, len = holders.size(); i < len; ++i) { final NewChunkHolder holder = holders.get(i); + // Folia start - region threading + if (!checkRegion && !TickThread.isTickThreadFor(this.world, holder.chunkX, holder.chunkZ)) { + // skip holders that would fail the thread check + continue; + } + // Folia end - region threading try { final NewChunkHolder.SaveStat saveStat = holder.save(shutdown, false); if (saveStat != null) { @@ -256,7 +444,7 @@ public final class ChunkHolderManager { } } } - if (flush) { + if (last && flush) { // Folia - region threading RegionFileIOThread.flush(); } if (logProgress) { @@ -290,18 +478,16 @@ public final class ChunkHolderManager { } public boolean hasTickets() { - this.ticketLock.lock(); - try { - return !this.tickets.isEmpty(); - } finally { - this.ticketLock.unlock(); - } + return !this.getTicketsCopy().isEmpty(); // Folia - region threading } public String getTicketDebugString(final long coordinate) { this.ticketLock.lock(); try { - final SortedArraySet> tickets = this.tickets.get(coordinate); + // Folia start - region threading + final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = this.getDataFor(coordinate); + final SortedArraySet> tickets = holderManagerRegionData == null ? null : holderManagerRegionData.tickets.get(coordinate); + // Folia end - region threading return tickets != null ? tickets.first().toString() : "no_ticket"; } finally { @@ -312,7 +498,17 @@ public final class ChunkHolderManager { public Long2ObjectOpenHashMap>> getTicketsCopy() { this.ticketLock.lock(); try { - return this.tickets.clone(); + // Folia start - region threading + Long2ObjectOpenHashMap>> ret = new Long2ObjectOpenHashMap<>(); + this.world.regioniser.computeForAllRegions((region) -> { + for (final LongIterator iterator = region.getData().getHolderManagerRegionData().tickets.keySet().longIterator(); iterator.hasNext();) { + final long chunk = iterator.nextLong(); + + ret.put(chunk, region.getData().getHolderManagerRegionData().tickets.get(chunk)); + } + }); + return ret; + // Folia end - region threading } finally { this.ticketLock.unlock(); } @@ -322,7 +518,11 @@ public final class ChunkHolderManager { ImmutableList.Builder ret; this.ticketLock.lock(); try { - SortedArraySet> tickets = this.tickets.get(ChunkPos.asLong(x, z)); + // Folia start - region threading + final long coordinate = CoordinateUtils.getChunkKey(x, z); + final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = this.getDataFor(coordinate); + final SortedArraySet> tickets = holderManagerRegionData == null ? null : holderManagerRegionData.tickets.get(coordinate); + // Folia end - region threading if (tickets == null) { return Collections.emptyList(); @@ -375,12 +575,37 @@ public final class ChunkHolderManager { return false; } + // Folia start - region threading + final ThreadedRegionizer.ThreadedRegion currRegion = TickRegionScheduler.getCurrentRegion(); + final boolean lock = currRegion == null || this.world.regioniser.getRegionAtUnsynchronised( + CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk) + ) != currRegion; + // Folia end - region threading + this.ticketLock.lock(); try { - final long removeTick = removeDelay == 0 ? NO_TIMEOUT_MARKER : this.currentTick + removeDelay; + // Folia start - region threading + NewChunkHolder holder = this.chunkHolders.get(chunk); + if (holder == null) { + // we need to guarantee that a chunk holder exists for each ticket + // this must be executed before retrieving the holder manager data for a target chunk, to ensure the + // region will exist + this.chunkHolders.put(chunk, holder = this.createChunkHolder(chunk)); + this.specialCaseUnload.add(holder); + } + + if (lock) { + // we just need to prevent merging, so we only need the read lock + // additionally, this will prevent deadlock in the remove all tickets function by using the read lock + this.world.regioniser.acquireReadLock(); + } + try { + final ChunkHolderManager.HolderManagerRegionData targetData = lock ? this.getDataFor(chunk) : currRegion.getData().getHolderManagerRegionData(); + // Folia end - region threading + final long removeTick = removeDelay == 0 ? NO_TIMEOUT_MARKER : targetData.currentTick + removeDelay; // Folia - region threading final Ticket ticket = new Ticket<>(type, level, identifier, removeTick); - final SortedArraySet> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> { + final SortedArraySet> ticketsAtChunk = targetData.tickets.computeIfAbsent(chunk, (final long keyInMap) -> { // Folia - region threading return SortedArraySet.create(4); }); @@ -392,25 +617,25 @@ public final class ChunkHolderManager { final long oldRemovalTick = current.removalTick; if (removeTick != oldRemovalTick) { if (oldRemovalTick != NO_TIMEOUT_MARKER) { - final Long2IntOpenHashMap removeCounts = this.removeTickToChunkExpireTicketCount.get(oldRemovalTick); + final Long2IntOpenHashMap removeCounts = targetData.removeTickToChunkExpireTicketCount.get(oldRemovalTick); // Folia - region threading final int prevCount = removeCounts.addTo(chunk, -1); if (prevCount == 1) { removeCounts.remove(chunk); if (removeCounts.isEmpty()) { - this.removeTickToChunkExpireTicketCount.remove(oldRemovalTick); + targetData.removeTickToChunkExpireTicketCount.remove(oldRemovalTick); // Folia - region threading } } } if (removeTick != NO_TIMEOUT_MARKER) { - this.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> { + targetData.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> { // Folia - region threading return new Long2IntOpenHashMap(); }).addTo(chunk, 1); } } } else { if (removeTick != NO_TIMEOUT_MARKER) { - this.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> { + targetData.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> { // Folia - region threading return new Long2IntOpenHashMap(); }).addTo(chunk, 1); } @@ -421,6 +646,11 @@ public final class ChunkHolderManager { } return current == ticket; + } finally { // Folia start - region threading + if (lock) { + this.world.regioniser.releaseReadLock(); + } + } // Folia end - region threading } finally { this.ticketLock.unlock(); } @@ -439,35 +669,70 @@ public final class ChunkHolderManager { return false; } + // Folia start - region threading + final ThreadedRegionizer.ThreadedRegion currRegion = TickRegionScheduler.getCurrentRegion(); + final boolean lock = currRegion == null || this.world.regioniser.getRegionAtUnsynchronised( + CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk) + ) != currRegion; + // Folia end - region threading + this.ticketLock.lock(); try { - final SortedArraySet> ticketsAtChunk = this.tickets.get(chunk); + // Folia start - region threading + if (lock) { + // we just need to prevent merging, so we only need the read lock + // additionally, this will prevent deadlock in the remove all tickets function by using the read lock + this.world.regioniser.acquireReadLock(); + } + try { + final ChunkHolderManager.HolderManagerRegionData targetData = lock ? this.getDataFor(chunk) : currRegion.getData().getHolderManagerRegionData(); + // Folia end - region threading + + final SortedArraySet> ticketsAtChunk = targetData == null ? null : targetData.tickets.get(chunk); + // Folia end - region threading if (ticketsAtChunk == null) { return false; } final int oldLevel = getTicketLevelAt(ticketsAtChunk); - final Ticket ticket = (Ticket)ticketsAtChunk.removeAndGet(new Ticket<>(type, level, identifier, -2L)); + final Ticket ticket = (Ticket)ticketsAtChunk.removeAndGet(new Ticket<>(type, level, identifier, PROBE_MARKER)); // Folia - region threading if (ticket == null) { return false; } + int newLevel = getTicketLevelAt(ticketsAtChunk); // Folia - region threading - moved up from below + // Folia start - region threading + // we should not change the ticket levels while the target region may be ticking + if (newLevel > level) { + final long unknownRemoveTick = targetData.currentTick + Math.max(0, TicketType.UNKNOWN.timeout); + final Ticket unknownTicket = new Ticket<>(TicketType.UNKNOWN, level, new ChunkPos(chunk), unknownRemoveTick); + if (ticketsAtChunk.add(unknownTicket)) { + targetData.removeTickToChunkExpireTicketCount.computeIfAbsent(unknownRemoveTick, (final long keyInMap) -> { + return new Long2IntOpenHashMap(); + }).addTo(chunk, 1); + } else { + throw new IllegalStateException("Should have been able to add " + unknownTicket + " to " + ticketsAtChunk); + } + newLevel = level; + } + // Folia end - region threading + if (ticketsAtChunk.isEmpty()) { - this.tickets.remove(chunk); + targetData.tickets.remove(chunk); // Folia - region threading } - final int newLevel = getTicketLevelAt(ticketsAtChunk); + // Folia - region threading - move up final long removeTick = ticket.removalTick; if (removeTick != NO_TIMEOUT_MARKER) { - final Long2IntOpenHashMap removeCounts = this.removeTickToChunkExpireTicketCount.get(removeTick); + final Long2IntOpenHashMap removeCounts = targetData.removeTickToChunkExpireTicketCount.get(removeTick); // Folia - region threading final int currCount = removeCounts.addTo(chunk, -1); if (currCount == 1) { removeCounts.remove(chunk); if (removeCounts.isEmpty()) { - this.removeTickToChunkExpireTicketCount.remove(removeTick); + targetData.removeTickToChunkExpireTicketCount.remove(removeTick); // Folia - region threading } } } @@ -477,6 +742,11 @@ public final class ChunkHolderManager { } return true; + } finally { // Folia start - region threading + if (lock) { + this.world.regioniser.releaseReadLock(); + } + } // Folia end - region threading } finally { this.ticketLock.unlock(); } @@ -516,24 +786,33 @@ public final class ChunkHolderManager { this.ticketLock.lock(); try { - for (final LongIterator iterator = new LongArrayList(this.tickets.keySet()).longIterator(); iterator.hasNext();) { - final long chunk = iterator.nextLong(); + // Folia start - region threading + this.world.regioniser.computeForAllRegions((region) -> { + for (final LongIterator iterator = new LongArrayList(region.getData().getHolderManagerRegionData().tickets.keySet()).longIterator(); iterator.hasNext();) { + final long chunk = iterator.nextLong(); - this.removeTicketAtLevel(ticketType, chunk, ticketLevel, ticketIdentifier); - } + this.removeTicketAtLevel(ticketType, chunk, ticketLevel, ticketIdentifier); + } + }); + // Folia end - region threading } finally { this.ticketLock.unlock(); } } public void tick() { - TickThread.ensureTickThread("Cannot tick ticket manager off-main"); + // Folia start - region threading + final ChunkHolderManager.HolderManagerRegionData data = this.getCurrentRegionData(); + if (data == null) { + throw new IllegalStateException("Not running tick() while on a region"); + } + // Folia end - region threading this.ticketLock.lock(); try { - final long tick = ++this.currentTick; + final long tick = ++data.currentTick; // Folia - region threading - final Long2IntOpenHashMap toRemove = this.removeTickToChunkExpireTicketCount.remove(tick); + final Long2IntOpenHashMap toRemove = data.removeTickToChunkExpireTicketCount.remove(tick); // Folia - region threading if (toRemove == null) { return; @@ -546,10 +825,10 @@ public final class ChunkHolderManager { for (final LongIterator iterator = toRemove.keySet().longIterator(); iterator.hasNext();) { final long chunk = iterator.nextLong(); - final SortedArraySet> tickets = this.tickets.get(chunk); + final SortedArraySet> tickets = data.tickets.get(chunk); // Folia - region threading tickets.removeIf(expireNow); if (tickets.isEmpty()) { - this.tickets.remove(chunk); + data.tickets.remove(chunk); // Folia - region threading this.ticketLevelPropagator.removeSource(chunk); } else { this.ticketLevelPropagator.setSource(chunk, convertBetweenTicketLevels(tickets.first().getTicketLevel())); @@ -798,30 +1077,62 @@ public final class ChunkHolderManager { if (changedFullStatus.isEmpty()) { return; } - if (!TickThread.isTickThread()) { - this.taskScheduler.scheduleChunkTask(() -> { - final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate; - for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { - pendingFullLoadUpdate.add(changedFullStatus.get(i)); - } - ChunkHolderManager.this.processPendingFullUpdate(); - }, PrioritisedExecutor.Priority.HIGHEST); - } else { - final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; - for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { - pendingFullLoadUpdate.add(changedFullStatus.get(i)); + final Long2ObjectOpenHashMap> sectionToUpdates = new Long2ObjectOpenHashMap<>(); + final List thisRegionHolders = new ArrayList<>(); + + final int regionShift = this.world.regioniser.sectionChunkShift; + final ThreadedRegionizer.ThreadedRegion thisRegion + = TickRegionScheduler.getCurrentRegion(); + + for (final NewChunkHolder holder : changedFullStatus) { + final int regionX = holder.chunkX >> regionShift; + final int regionZ = holder.chunkZ >> regionShift; + final long holderSectionKey = CoordinateUtils.getChunkKey(regionX, regionZ); + + // region may be null + if (thisRegion != null && this.world.regioniser.getRegionAtUnsynchronised(holder.chunkX, holder.chunkZ) == thisRegion) { + thisRegionHolders.add(holder); + } else { + sectionToUpdates.computeIfAbsent(holderSectionKey, (final long keyInMap) -> { + return new ArrayList<>(); + }).add(holder); + } + } + + if (!thisRegionHolders.isEmpty()) { + thisRegion.getData().getHolderManagerRegionData().pendingFullLoadUpdate.addAll(thisRegionHolders); + } + + if (!sectionToUpdates.isEmpty()) { + for (final Iterator>> iterator = sectionToUpdates.long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry> entry = iterator.next(); + final long sectionKey = entry.getLongKey(); + + final int chunkX = CoordinateUtils.getChunkX(sectionKey) << regionShift; + final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift; + + final List regionHolders = entry.getValue(); + this.taskScheduler.scheduleChunkTaskEventually(chunkX, chunkZ, () -> { // Folia - region threading + ChunkHolderManager.this.getCurrentRegionData().pendingFullLoadUpdate.addAll(regionHolders); + ChunkHolderManager.this.processPendingFullUpdate(); + }, PrioritisedExecutor.Priority.HIGHEST); } } } final ReferenceLinkedOpenHashSet unloadQueue = new ReferenceLinkedOpenHashSet<>(); + /* + * Note: Only called on chunk holders that the current ticking region owns + */ private void removeChunkHolder(final NewChunkHolder holder) { holder.killed = true; holder.vanillaChunkHolder.onChunkRemove(); - this.autoSaveQueue.remove(holder); + // Folia - region threading ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder); + this.getCurrentRegionData().autoSaveQueue.remove(holder); // Folia - region threading this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); } @@ -839,6 +1150,8 @@ public final class ChunkHolderManager { throw new IllegalStateException("Cannot hold scheduling lock while calling processUnloads"); } + final ChunkHolderManager.HolderManagerRegionData currentData = this.getCurrentRegionData(); // Folia - region threading + final List unloadQueue; final List scheduleList = new ArrayList<>(); this.ticketLock.lock(); @@ -851,11 +1164,22 @@ public final class ChunkHolderManager { // in order to ensure all chunks in the unload queue do not have a pending ticket level update, // process them now this.processTicketUpdates(false, false, scheduleList); - unloadQueue = new ArrayList<>((int)(this.unloadQueue.size() * 0.05) + 1); - final int unloadCount = Math.max(50, (int)(this.unloadQueue.size() * 0.05)); - for (int i = 0; i < unloadCount && !this.unloadQueue.isEmpty(); ++i) { - final NewChunkHolder chunkHolder = this.unloadQueue.removeFirst(); + // Folia start - region threading + final ArrayDeque toUnload = new ArrayDeque<>(); + // The unload queue is globally maintained, but we can only unload chunks in our region + for (final NewChunkHolder holder : this.unloadQueue) { + if (TickThread.isTickThreadFor(this.world, holder.chunkX, holder.chunkZ)) { + toUnload.add(holder); + } + } + // Folia end - region threading + + final int unloadCount = Math.max(50, (int)(toUnload.size() * 0.05)); // Folia - region threading + unloadQueue = new ArrayList<>(unloadCount + 1); // Folia - region threading + for (int i = 0; i < unloadCount && !toUnload.isEmpty(); ++i) { // Folia - region threading + final NewChunkHolder chunkHolder = toUnload.removeFirst(); // Folia - region threading + this.unloadQueue.remove(chunkHolder); // Folia - region threading if (chunkHolder.isSafeToUnload() != null) { LOGGER.error("Chunkholder " + chunkHolder + " is not safe to unload but is inside the unload queue?"); continue; @@ -1170,6 +1494,19 @@ public final class ChunkHolderManager { this.ticketLevelUpdates.clear(); } } + + // Folia start - region threading + // it is possible that a special case new chunk holder had its ticket removed before it was propagated, + // which means checkUnload was never invoked. By checking unload here, we ensure that either the + // ticket level was propagated (in which case, a later depropagation would check again) or that + // we called checkUnload for it. + if (!this.specialCaseUnload.isEmpty()) { + for (final NewChunkHolder special : this.specialCaseUnload) { + special.checkUnload(); + } + this.specialCaseUnload.clear(); + } + // Folia end - region threading } finally { this.ticketLock.unlock(); } @@ -1193,7 +1530,12 @@ public final class ChunkHolderManager { // only call on tick thread protected final boolean processPendingFullUpdate() { - final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; + final HolderManagerRegionData data = this.getCurrentRegionData(); + if (data == null) { + return false; + } + + final ArrayDeque pendingFullLoadUpdate = data.pendingFullLoadUpdate; boolean ret = false; @@ -1204,9 +1546,7 @@ public final class ChunkHolderManager { ret |= holder.handleFullStatusChange(changedFullStatus); if (!changedFullStatus.isEmpty()) { - for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { - pendingFullLoadUpdate.add(changedFullStatus.get(i)); - } + this.addChangedStatuses(changedFullStatus); changedFullStatus.clear(); } } @@ -1256,7 +1596,7 @@ public final class ChunkHolderManager { private JsonObject getDebugJsonNoLock() { final JsonObject ret = new JsonObject(); - ret.addProperty("current_tick", Long.valueOf(this.currentTick)); + // Folia - region threading - move down final JsonArray unloadQueue = new JsonArray(); ret.add("unload_queue", unloadQueue); @@ -1275,60 +1615,73 @@ public final class ChunkHolderManager { holders.add(holder.getDebugJson()); } - final JsonArray removeTickToChunkExpireTicketCount = new JsonArray(); - ret.add("remove_tick_to_chunk_expire_ticket_count", removeTickToChunkExpireTicketCount); + // Folia start - region threading + final JsonArray regions = new JsonArray(); + ret.add("regions", regions); + this.world.regioniser.computeForAllRegionsUnsynchronised((region) -> { + final JsonObject regionJson = new JsonObject(); + regions.add(regionJson); + + final TickRegions.TickRegionData regionData = region.getData(); - for (final Long2ObjectMap.Entry tickEntry : this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet()) { - final long tick = tickEntry.getLongKey(); - final Long2IntOpenHashMap coordinateToCount = tickEntry.getValue(); + regionJson.addProperty("current_tick", Long.valueOf(regionData.getCurrentTick())); - final JsonObject tickJson = new JsonObject(); - removeTickToChunkExpireTicketCount.add(tickJson); + final JsonArray removeTickToChunkExpireTicketCount = new JsonArray(); + regionJson.add("remove_tick_to_chunk_expire_ticket_count", removeTickToChunkExpireTicketCount); - tickJson.addProperty("tick", Long.valueOf(tick)); + for (final Long2ObjectMap.Entry tickEntry : regionData.getHolderManagerRegionData().removeTickToChunkExpireTicketCount.long2ObjectEntrySet()) { + final long tick = tickEntry.getLongKey(); + final Long2IntOpenHashMap coordinateToCount = tickEntry.getValue(); - final JsonArray tickEntries = new JsonArray(); - tickJson.add("entries", tickEntries); + final JsonObject tickJson = new JsonObject(); + removeTickToChunkExpireTicketCount.add(tickJson); - for (final Long2IntMap.Entry entry : coordinateToCount.long2IntEntrySet()) { - final long coordinate = entry.getLongKey(); - final int count = entry.getIntValue(); + tickJson.addProperty("tick", Long.valueOf(tick)); - final JsonObject entryJson = new JsonObject(); - tickEntries.add(entryJson); + final JsonArray tickEntries = new JsonArray(); + tickJson.add("entries", tickEntries); - entryJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); - entryJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); - entryJson.addProperty("count", Integer.valueOf(count)); + for (final Long2IntMap.Entry entry : coordinateToCount.long2IntEntrySet()) { + final long coordinate = entry.getLongKey(); + final int count = entry.getIntValue(); + + final JsonObject entryJson = new JsonObject(); + tickEntries.add(entryJson); + + entryJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); + entryJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); + entryJson.addProperty("count", Integer.valueOf(count)); + } } - } - final JsonArray allTicketsJson = new JsonArray(); - ret.add("tickets", allTicketsJson); + final JsonArray allTicketsJson = new JsonArray(); + regionJson.add("tickets", allTicketsJson); - for (final Long2ObjectMap.Entry>> coordinateTickets : this.tickets.long2ObjectEntrySet()) { - final long coordinate = coordinateTickets.getLongKey(); - final SortedArraySet> tickets = coordinateTickets.getValue(); + for (final Long2ObjectMap.Entry>> coordinateTickets : regionData.getHolderManagerRegionData().tickets.long2ObjectEntrySet()) { + final long coordinate = coordinateTickets.getLongKey(); + final SortedArraySet> tickets = coordinateTickets.getValue(); - final JsonObject coordinateJson = new JsonObject(); - allTicketsJson.add(coordinateJson); + final JsonObject coordinateJson = new JsonObject(); + allTicketsJson.add(coordinateJson); - coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); - coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); + coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); + coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); - final JsonArray ticketsSerialized = new JsonArray(); - coordinateJson.add("tickets", ticketsSerialized); + final JsonArray ticketsSerialized = new JsonArray(); + coordinateJson.add("tickets", ticketsSerialized); - for (final Ticket ticket : tickets) { - final JsonObject ticketSerialized = new JsonObject(); - ticketsSerialized.add(ticketSerialized); + for (final Ticket ticket : tickets) { + final JsonObject ticketSerialized = new JsonObject(); + ticketsSerialized.add(ticketSerialized); - ticketSerialized.addProperty("type", ticket.getType().toString()); - ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel())); - ticketSerialized.addProperty("identifier", Objects.toString(ticket.key)); - ticketSerialized.addProperty("remove_tick", Long.valueOf(ticket.removalTick)); + ticketSerialized.addProperty("type", ticket.getType().toString()); + ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel())); + ticketSerialized.addProperty("identifier", Objects.toString(ticket.key)); + ticketSerialized.addProperty("remove_tick", Long.valueOf(ticket.removalTick)); + } } - } + }); + // Folia end - region threading return ret; } diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java index 84cc9397237fa0c17aa1012dfb5683c90eb6d3b8..25db30284e3bab9ebad1ca7320db66116057e599 100644 --- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java +++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java @@ -113,7 +113,7 @@ public final class ChunkTaskScheduler { public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor; public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor; - private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue(); + // Folia - regionised ticking final ReentrantLock schedulingLock = new ReentrantLock(); public final ChunkHolderManager chunkHolderManager; @@ -240,14 +240,13 @@ public final class ChunkTaskScheduler { }; // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions - this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING); + this.scheduleChunkTaskEventually(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING); // Folia - region threading // so, make the main thread pick it up MinecraftServer.chunkSystemCrash = new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException); } public boolean executeMainThreadTask() { - TickThread.ensureTickThread("Cannot execute main thread task off-main"); - return this.mainThreadExecutor.executeTask(); + throw new UnsupportedOperationException("Use regionised ticking hooks"); // Folia - regionised ticking } public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { @@ -267,7 +266,7 @@ public final class ChunkTaskScheduler { public void scheduleTickingState(final int chunkX, final int chunkZ, final ChunkHolder.FullChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer onComplete) { - if (!TickThread.isTickThread()) { + if (!TickThread.isTickThreadFor(this.world, chunkX, chunkZ)) { this.scheduleChunkTask(chunkX, chunkZ, () -> { ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); }, priority); @@ -380,9 +379,50 @@ public final class ChunkTaskScheduler { }); } + // Folia start - region threading + // only appropriate to use with ServerLevel#syncLoadNonFull + public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus, + final PrioritisedExecutor.Priority priority) { + final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); + final int minLevel = 33 + ChunkStatus.getDistance(toStatus); + final List tasks = new ArrayList<>(); + this.chunkHolderManager.ticketLock.lock(); + try { + this.schedulingLock.lock(); + try { + final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); + if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { + return false; + } else { + final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); + if (genStatus != null && genStatus.isOrAfter(toStatus)) { + return true; + } else { + chunkHolder.raisePriority(priority); + + if (!chunkHolder.upgradeGenTarget(toStatus)) { + this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); + } + } + } + } finally { + this.schedulingLock.unlock(); + } + } finally { + this.chunkHolderManager.ticketLock.unlock(); + } + + for (int i = 0, len = tasks.size(); i < len; ++i) { + tasks.get(i).schedule(); + } + + return true; + } + // Folia end - region threading + public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer onComplete) { - if (!TickThread.isTickThread()) { + if (!TickThread.isTickThreadFor(this.world, chunkX, chunkZ)) { this.scheduleChunkTask(chunkX, chunkZ, () -> { ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); }, priority); @@ -409,7 +449,7 @@ public final class ChunkTaskScheduler { this.chunkHolderManager.processTicketUpdates(); } - final Consumer loadCallback = (final ChunkAccess chunk) -> { + final Consumer loadCallback = onComplete == null && !addTicket ? null : (final ChunkAccess chunk) -> { try { if (onComplete != null) { onComplete.accept(chunk); @@ -449,7 +489,9 @@ public final class ChunkTaskScheduler { if (!chunkHolder.upgradeGenTarget(toStatus)) { this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); } - chunkHolder.addStatusConsumer(toStatus, loadCallback); + if (loadCallback != null) { + chunkHolder.addStatusConsumer(toStatus, loadCallback); + } } } } finally { @@ -463,7 +505,7 @@ public final class ChunkTaskScheduler { tasks.get(i).schedule(); } - if (!scheduled) { + if (loadCallback != null && !scheduled) { // couldn't schedule try { loadCallback.accept(chunk); @@ -652,7 +694,7 @@ public final class ChunkTaskScheduler { */ @Deprecated public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) { - return this.scheduleChunkTask(run, PrioritisedExecutor.Priority.NORMAL); + throw new UnsupportedOperationException(); // Folia - regionised ticking } /** @@ -660,7 +702,7 @@ public final class ChunkTaskScheduler { */ @Deprecated public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final PrioritisedExecutor.Priority priority) { - return this.mainThreadExecutor.queueRunnable(run, priority); + throw new UnsupportedOperationException(); // Folia - regionised ticking } public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { @@ -669,28 +711,33 @@ public final class ChunkTaskScheduler { public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) { - return this.mainThreadExecutor.createTask(run, priority); + return MinecraftServer.getServer().regionizedServer.taskQueue.createChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking } public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { - return this.mainThreadExecutor.queueRunnable(run); + return this.scheduleChunkTask(chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); // TODO rebase into chunk system patch } public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) { - return this.mainThreadExecutor.queueRunnable(run, priority); + return MinecraftServer.getServer().regionizedServer.taskQueue.queueChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking } - public void executeTasksUntil(final BooleanSupplier exit) { - if (Bukkit.isPrimaryThread()) { - this.mainThreadExecutor.executeConditionally(exit); - } else { - long counter = 1L; - while (!exit.getAsBoolean()) { - counter = ConcurrentUtil.linearLongBackoff(counter, 100_000L, 5_000_000L); // 100us, 5ms - } - } + // Folia start - region threading + // this function is guaranteed to never touch the ticket lock or schedule lock + // yes, this IS a hack so that we can avoid deadlock due to region threading introducing the + // ticket lock in the schedule logic + public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run, + final PrioritisedExecutor.Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(chunkX, chunkZ, run, priority); + this.world.taskQueueRegionData.pushGlobalChunkTask(() -> { + MinecraftServer.getServer().regionizedServer.taskQueue.queueChunkTask(ChunkTaskScheduler.this.world, chunkX, chunkZ, run, priority); + }); + return ret; } + // Folia end - region threading + + // Folia - regionised ticking public boolean halt(final boolean sync, final long maxWaitNS) { this.lightExecutor.halt(); @@ -699,6 +746,7 @@ public final class ChunkTaskScheduler { this.loadExecutor.halt(); final long time = System.nanoTime(); if (sync) { + // start at 10 * 0.5ms -> 5ms for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { if ( !this.lightExecutor.isActive() && diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java index 8013dd333e27aa5fd0beb431fa32491eec9f5246..3b70ccd8e0b1ada943f57faf99c23b2935249cf6 100644 --- a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java +++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java @@ -708,7 +708,7 @@ public final class NewChunkHolder { boolean killed; // must hold scheduling lock - private void checkUnload() { + void checkUnload() { // Folia - region threading if (this.killed) { return; } @@ -1412,7 +1412,7 @@ public final class NewChunkHolder { } // must be scheduled to main, we do not trust the callback to not do anything stupid - this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { + this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading for (final Consumer consumer : consumers) { try { consumer.accept(chunk); @@ -1455,7 +1455,7 @@ public final class NewChunkHolder { } // must be scheduled to main, we do not trust the callback to not do anything stupid - this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { + this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading for (final Consumer consumer : consumers) { try { consumer.accept(chunk); @@ -1715,7 +1715,7 @@ public final class NewChunkHolder { return this.entityChunk; } - public long lastAutoSave; + public long lastAutoSave; // Folia - region threaded - change to relative delay public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {} @@ -1865,7 +1865,7 @@ public final class NewChunkHolder { } catch (final ThreadDeath death) { throw death; } catch (final Throwable thr) { - LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); + LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); // TODO rebase if (unloading && !completing) { this.completeAsyncChunkDataSave(null); } @@ -1913,7 +1913,7 @@ public final class NewChunkHolder { } catch (final ThreadDeath death) { throw death; } catch (final Throwable thr) { - LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); + LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); // TODO rebase } return true; @@ -1939,7 +1939,7 @@ public final class NewChunkHolder { } catch (final ThreadDeath death) { throw death; } catch (final Throwable thr) { - LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); + LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); // TODO rebase } return true; diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java index 7ba60b4b4f29a42c58d98aafc5ea0fa3214f554c..dc1820e1231c15a7244883731a86f04fa8d20aa1 100644 --- a/src/main/java/io/papermc/paper/command/PaperCommand.java +++ b/src/main/java/io/papermc/paper/command/PaperCommand.java @@ -43,7 +43,7 @@ public final class PaperCommand extends Command { commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand()); commands.put(Set.of("syncloadinfo"), new SyncLoadInfoCommand()); commands.put(Set.of("dumpitem"), new DumpItemCommand()); - commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand()); + commands.put(Set.of("mobcaps"), new MobcapsCommand()); // Folia - region threading - revert per player mob caps commands.put(Set.of("dumplisteners"), new DumpListenersCommand()); return commands.entrySet().stream() diff --git a/src/main/java/io/papermc/paper/command/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java index bbb8b1933ef33a3b91f69545f69dd3cfb84b27f5..b23b76a598731da8feef53c370b341233afdbea7 100644 --- a/src/main/java/io/papermc/paper/command/PaperCommands.java +++ b/src/main/java/io/papermc/paper/command/PaperCommands.java @@ -17,7 +17,7 @@ public final class PaperCommands { private static final Map COMMANDS = new HashMap<>(); static { COMMANDS.put("paper", new PaperCommand("paper")); - COMMANDS.put("mspt", new MSPTCommand("mspt")); + COMMANDS.put("tps", new io.papermc.paper.threadedregions.commands.CommandServerHealth()); // Folia - region threading } public static void registerCommands(final MinecraftServer server) { diff --git a/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java index cd2e4d792e972b8bf1e07b8961594a670ae949cf..3ab8dbf2768a4ef8fb53af6f5431f7f6afe6d168 100644 --- a/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java +++ b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java @@ -18,7 +18,9 @@ import static net.kyori.adventure.text.format.NamedTextColor.YELLOW; public final class HeapDumpCommand implements PaperSubcommand { @Override public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading this.dumpHeap(sender); + }); // Folia - region threading return true; } diff --git a/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java b/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java index 99c41a39cdad0271d089c6e03bebfdafba1aaa57..41aaa709dc2e474f23e759ebc51f33021c4f5485 100644 --- a/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java +++ b/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java @@ -46,7 +46,7 @@ public final class MobcapsCommand implements PaperSubcommand { public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { switch (subCommand) { case "mobcaps" -> this.printMobcaps(sender, args); - case "playermobcaps" -> this.printPlayerMobcaps(sender, args); + //case "playermobcaps" -> this.printPlayerMobcaps(sender, args); // Folia - region threading - revert per player mob caps } return true; } @@ -55,7 +55,7 @@ public final class MobcapsCommand implements PaperSubcommand { public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { return switch (subCommand) { case "mobcaps" -> CommandUtil.getListMatchingLast(sender, args, this.suggestMobcaps(args)); - case "playermobcaps" -> CommandUtil.getListMatchingLast(sender, args, this.suggestPlayerMobcaps(sender, args)); + //case "playermobcaps" -> CommandUtil.getListMatchingLast(sender, args, this.suggestPlayerMobcaps(sender, args)); // Folia - region threading - revert per player mob caps default -> throw new IllegalArgumentException(); }; } @@ -140,41 +140,7 @@ public final class MobcapsCommand implements PaperSubcommand { } } - private void printPlayerMobcaps(final CommandSender sender, final String[] args) { - final @Nullable Player player; - if (args.length == 0) { - if (sender instanceof Player pl) { - player = pl; - } else { - sender.sendMessage(Component.text("Must specify a player! ex: '/paper playermobcount playerName'", NamedTextColor.RED)); - return; - } - } else if (args.length == 1) { - final String input = args[0]; - player = Bukkit.getPlayerExact(input); - if (player == null) { - sender.sendMessage(Component.text("Could not find player named '" + input + "'", NamedTextColor.RED)); - return; - } - } else { - sender.sendMessage(Component.text("Too many arguments!", NamedTextColor.RED)); - return; - } - - final ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle(); - final ServerLevel level = serverPlayer.getLevel(); - - if (!level.paperConfig().entities.spawning.perPlayerMobSpawns) { - sender.sendMessage(Component.text("Use '/paper mobcaps' for worlds where per-player mob spawning is disabled.", NamedTextColor.RED)); - return; - } - - sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), Component.text("Mobcaps for player: "), Component.text(player.getName(), NamedTextColor.GREEN))); - sender.sendMessage(createMobcapsComponent( - category -> level.chunkSource.chunkMap.getMobCountNear(serverPlayer, category), - category -> level.getWorld().getSpawnLimitUnsafe(org.bukkit.craftbukkit.util.CraftSpawnCategory.toBukkit(category)) - )); - } + // Folia - region threading - revert per player mob caps private static Component createMobcapsComponent(final ToIntFunction countGetter, final ToIntFunction limitGetter) { return MOB_CATEGORY_COLORS.entrySet().stream() diff --git a/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java index bd68139ae635f2ad7ec8e7a21e0056a139c4c62e..48a43341b17247355a531164019d5cc9c5555f26 100644 --- a/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java +++ b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java @@ -16,7 +16,9 @@ import static net.kyori.adventure.text.format.NamedTextColor.RED; public final class ReloadCommand implements PaperSubcommand { @Override public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading this.doReload(sender); + }); // Folia - region threading return true; } diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java index 9f5f0d8ddc8f480b48079c70e38c9c08eff403f6..3b83f25a24d6f9cdbf131d5a4432fb4ad018be4e 100644 --- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java @@ -288,6 +288,18 @@ public class GlobalConfiguration extends ConfigurationPart { public boolean strictAdvancementDimensionCheck = false; } + // Folia start - threaded regions + public ThreadedRegions threadedRegions; + public class ThreadedRegions extends Post { + + public int threads = -1; + + @Override + public void postProcess() { + io.papermc.paper.threadedregions.TickRegions.init(this); + } + } + // Folia end - threaded regions public ChunkLoadingBasic chunkLoadingBasic; public class ChunkLoadingBasic extends ConfigurationPart { diff --git a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java index 51cf0014c4229fc8671804d885b6381996810130..f13d2e2174b3be0b0d4462406a1e00255867a790 100644 --- a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java +++ b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java @@ -128,7 +128,7 @@ public class WorldConfiguration extends ConfigurationPart { public boolean filterBadTileEntityNbtFromFallingBlocks = true; public List filteredEntityTagNbtPaths = NbtPathSerializer.fromString(List.of("Pos", "Motion", "SleepingX", "SleepingY", "SleepingZ")); public boolean disableMobSpawnerSpawnEggTransformation = false; - public boolean perPlayerMobSpawns = true; + //public boolean perPlayerMobSpawns = true; // Folia - region threading - revert per player mob caps public boolean scanForLegacyEnderDragon = true; @MergeMap public Reference2IntMap spawnLimits = Util.make(new Reference2IntOpenHashMap<>(NaturalSpawner.SPAWNING_CATEGORIES.length), map -> Arrays.stream(NaturalSpawner.SPAWNING_CATEGORIES).forEach(mobCategory -> map.put(mobCategory, -1))); @@ -405,7 +405,14 @@ public class WorldConfiguration extends ConfigurationPart { public Chunks chunks; - public class Chunks extends ConfigurationPart { + // Folia start - region threading - force prevent moving into unloaded chunks + public class Chunks extends Post { + @Override + public void postProcess() { + this.preventMovingIntoUnloadedChunks = true; + } + // Folia end - region threading - force prevent moving into unloaded chunks + public AutosavePeriod autoSaveInterval = AutosavePeriod.def(); public int maxAutoSaveChunksPerTick = 24; public int fixedChunkInhabitedTime = -1; diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java index c0e896343c22badd97c774c4ed1daa4e274f5d44..1bca4a629fe986e5afa47d8884dc44b2be800f32 100644 --- a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java +++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java @@ -243,10 +243,19 @@ class PaperPluginInstanceManager { + pluginName + " (Is it up to date?)", ex, plugin); // Paper } + // Folia start - region threading try { - this.server.getScheduler().cancelTasks(plugin); + this.server.getGlobalRegionScheduler().cancelTasks(plugin); } catch (Throwable ex) { - this.handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for " + this.handlePluginException("Error occurred (in the plugin loader) while cancelling global tasks for " + + pluginName + " (Is it up to date?)", ex, plugin); // Paper + } + // Folia end - region threading + + try { + this.server.getAsyncScheduler().cancelTasks(plugin); // Folia - region threading + } catch (Throwable ex) { + this.handlePluginException("Error occurred (in the plugin loader) while cancelling async tasks for " // Folia - region threading + pluginName + " (Is it up to date?)", ex, plugin); // Paper } diff --git a/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java b/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java new file mode 100644 index 0000000000000000000000000000000000000000..d9687722e02dfd4088c7030abbf5008eb0a092c8 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java @@ -0,0 +1,181 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.util.Validate; +import io.papermc.paper.util.TickThread; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.world.entity.Entity; +import org.bukkit.craftbukkit.entity.CraftEntity; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * An entity can move between worlds with an arbitrary tick delay, be temporarily removed + * for players (i.e end credits), be partially removed from world state (i.e inactive but not removed), + * teleport between ticking regions, teleport between worlds (which will change the underlying Entity object + * for non-players), and even be removed entirely from the server. The uncertainty of an entity's state can make + * it difficult to schedule tasks without worrying about undefined behaviors resulting from any of the states listed + * previously. + * + *

+ * This class is designed to eliminate those states by providing an interface to run tasks only when an entity + * is contained in a world, on the owning thread for the region, and by providing the current Entity object. + * The scheduler also allows a task to provide a callback, the "retired" callback, that will be invoked + * if the entity is removed before a task that was scheduled could be executed. The scheduler is also + * completely thread-safe, allowing tasks to be scheduled from any thread context. The scheduler also indicates + * properly whether a task was scheduled successfully (i.e scheduler not retired), thus the code scheduling any task + * knows whether the given callbacks will be invoked eventually or not - which may be critical for off-thread + * contexts. + *

+ */ +public final class EntityScheduler { + + /** + * The Entity. Note that it is the CraftEntity, since only that class properly tracks world transfers. + */ + public final CraftEntity entity; + + private static final record ScheduledTask(Consumer run, Consumer retired) {} + + private long tickCount = 0L; + private static final long RETIRED_TICK_COUNT = -1L; + private final Object stateLock = new Object(); + private final Long2ObjectOpenHashMap> oneTimeDelayed = new Long2ObjectOpenHashMap<>(); + + private final ArrayDeque currentlyExecuting = new ArrayDeque<>(); + + public EntityScheduler(final CraftEntity entity) { + this.entity = Validate.notNull(entity); + } + + /** + * Retires the scheduler, preventing new tasks from being scheduled and invoking the retired callback + * on all currently scheduled tasks. + * + *

+ * Note: This should only be invoked after synchronously removing the entity from the world. + *

+ * + * @throws IllegalStateException If the scheduler is already retired. + */ + public void retire() { + synchronized (this.stateLock) { + if (this.tickCount == RETIRED_TICK_COUNT) { + throw new IllegalStateException("Already retired"); + } + this.tickCount = RETIRED_TICK_COUNT; + } + + final Entity thisEntity = this.entity.getHandle(); + + // correctly handle and order retiring while running executeTick + for (int i = 0, len = this.currentlyExecuting.size(); i < len; ++i) { + final ScheduledTask task = this.currentlyExecuting.pollFirst(); + final Consumer retireTask = (Consumer)task.retired; + if (retireTask == null) { + continue; + } + + retireTask.accept(thisEntity); + } + + for (final List tasks : this.oneTimeDelayed.values()) { + for (int i = 0, len = tasks.size(); i < len; ++i) { + final ScheduledTask task = tasks.get(i); + final Consumer retireTask = (Consumer)task.retired; + if (retireTask == null) { + continue; + } + + retireTask.accept(thisEntity); + } + } + } + + /** + * Schedules a task with the given delay. If the task failed to schedule because the scheduler is retired (entity + * removed), then returns {@code false}. Otherwise, either the run callback will be invoked after the specified delay, + * or the retired callback will be invoked if the scheduler is retired. + * Note that the retired callback is invoked in critical code, so it should not attempt to remove the entity, remove + * other entities, load chunks, load worlds, modify ticket levels, etc. + * + *

+ * It is guaranteed that the run and retired callback are invoked on the region which owns the entity. + *

+ *

+ * The run and retired callback take an Entity parameter representing the current object entity that the scheduler + * is tied to. Since the scheduler is transferred when an entity changes dimensions, it is possible the entity parameter + * is not the same when the task was first scheduled. Thus, only the parameter provided should be used. + *

+ * @param run The callback to run after the specified delay, may not be null. + * @param retired Retire callback to run if the entity is retired before the run callback can be invoked, may be null. + * @param delay The delay in ticks before the run callback is invoked. Any value less-than 1 is treated as 1. + * @return {@code true} if the task was scheduled, which means that either the run function or the retired function + * will be invoked (but never both), or {@code false} indicating neither the run nor retired function will be invoked + * since the scheduler has been retired. + */ + public boolean schedule(final Consumer run, final Consumer retired, final long delay) { + Validate.notNull(run, "Run task may not be null"); + + final ScheduledTask task = new ScheduledTask(run, retired); + synchronized (this.stateLock) { + if (this.tickCount == RETIRED_TICK_COUNT) { + return false; + } + this.oneTimeDelayed.computeIfAbsent(this.tickCount + Math.max(1L, delay), (final long keyInMap) -> { + return new ArrayList<>(); + }).add(task); + } + + return true; + } + + /** + * Executes a tick for the scheduler. + * + * @throws IllegalStateException If the scheduler is retired. + */ + public void executeTick() { + final Entity thisEntity = this.entity.getHandle(); + + TickThread.ensureTickThread(thisEntity, "May not tick entity scheduler asynchronously"); + final List toRun; + synchronized (this.stateLock) { + if (this.tickCount == RETIRED_TICK_COUNT) { + throw new IllegalStateException("Ticking retired scheduler"); + } + ++this.tickCount; + if (this.oneTimeDelayed.isEmpty()) { + toRun = null; + } else { + toRun = this.oneTimeDelayed.remove(this.tickCount); + } + } + + if (toRun != null) { + for (int i = 0, len = toRun.size(); i < len; ++i) { + this.currentlyExecuting.addLast(toRun.get(i)); + } + } + + // Note: It is allowed for the tasks executed to retire the entity in a given task. + for (int i = 0, len = this.currentlyExecuting.size(); i < len; ++i) { + if (!TickThread.isTickThreadFor(thisEntity)) { + // tp has been queued sync by one of the tasks + // in this case, we need to delay the tasks for next tick + break; + } + final ScheduledTask task = this.currentlyExecuting.pollFirst(); + + if (this.tickCount != RETIRED_TICK_COUNT) { + ((Consumer)task.run).accept(thisEntity); + } else { + // retired synchronously + // note: here task is null + break; + } + } + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java b/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java new file mode 100644 index 0000000000000000000000000000000000000000..1bfda654d7cb79504058b591d460bfef79f0c951 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java @@ -0,0 +1,175 @@ +package io.papermc.paper.threadedregions; + +import com.mojang.logging.LogUtils; +import io.papermc.paper.util.TickThread; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import org.slf4j.Logger; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public final class RegionShutdownThread extends TickThread { + + private static final Logger LOGGER = LogUtils.getClassLogger(); + + ThreadedRegionizer.ThreadedRegion shuttingDown; + + public RegionShutdownThread(final String name) { + super(name); + this.setUncaughtExceptionHandler((thread, thr) -> { + LOGGER.error("Error shutting down server", thr); + }); + } + + static ThreadedRegionizer.ThreadedRegion getRegion() { + final Thread currentThread = Thread.currentThread(); + if (currentThread instanceof RegionShutdownThread shutdownThread) { + return shutdownThread.shuttingDown; + } + return null; + } + + + static RegionizedWorldData getWorldData() { + final Thread currentThread = Thread.currentThread(); + if (currentThread instanceof RegionShutdownThread shutdownThread) { + // no fast path for shutting down + if (shutdownThread.shuttingDown != null) { + return shutdownThread.shuttingDown.getData().world.worldRegionData.get(); + } + } + return null; + } + + // The region shutdown thread bypasses all tick thread checks, which will allow us to execute global saves + // it will not however let us perform arbitrary sync loads, arbitrary world state lookups simply because + // the data required to do that is regionised, and we can only access it when we OWN the region, and we do not. + // Thus, the only operation that the shutdown thread will perform + + private void saveLevelData(final ServerLevel world) { + try { + world.saveLevelData(); + } catch (final Throwable thr) { + LOGGER.error("Failed to save level data for " + world.getWorld().getName(), thr); + } + } + + private void finishTeleportations(final ThreadedRegionizer.ThreadedRegion region, + final ServerLevel world) { + try { + this.shuttingDown = region; + final List pendingTeleports = world.removeAllRegionTeleports(); + if (pendingTeleports.isEmpty()) { + return; + } + final ChunkPos center = region.getCenterChunk(); + LOGGER.info("Completing " + pendingTeleports.size() + " pending teleports in region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'"); + for (final ServerLevel.PendingTeleport pendingTeleport : pendingTeleports) { + LOGGER.info("Completing teleportation to target position " + pendingTeleport.to()); + + // first, add entities to entity chunk so that they will be saved + for (final Entity.EntityTreeNode node : pendingTeleport.rootVehicle().getFullTree()) { + // assume that world and position are set to destination here + node.root.level = world; // in case the pending teleport is from a portal before it finds the exact destination + world.getEntityLookup().addEntityForShutdownTeleportComplete(node.root); + } + + // then, rebuild the passenger tree so that when saving only the root vehicle will be written - and if + // there are any player passengers, that the later player saving will save the tree + pendingTeleport.rootVehicle().restore(); + + // now we are finished + LOGGER.info("Completed teleportation to target position " + pendingTeleport.to()); + } + } catch (final Throwable thr) { + LOGGER.error("Failed to complete pending teleports", thr); + } finally { + this.shuttingDown = null; + } + } + + private void saveRegionChunks(final ThreadedRegionizer.ThreadedRegion region, + final boolean last) { + ChunkPos center = null; + try { + this.shuttingDown = region; + center = region.getCenterChunk(); + LOGGER.info("Saving chunks around region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'"); + region.regioniser.world.chunkTaskScheduler.chunkHolderManager.close(true, true, false, last, false); + } catch (final Throwable thr) { + LOGGER.error("Failed to save chunks for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr); + } finally { + this.shuttingDown = null; + } + } + + private void haltChunkSystem(final ServerLevel world) { + try { + world.chunkTaskScheduler.chunkHolderManager.close(false, true, true, false, false); + } catch (final Throwable thr) { + LOGGER.error("Failed to halt chunk system for world '" + world.getWorld().getName() + "'", thr); + } + } + + private void haltWorldNoRegions(final ServerLevel world) { + try { + world.chunkTaskScheduler.chunkHolderManager.close(true, true, true, true, false); + } catch (final Throwable thr) { + LOGGER.error("Failed to close world '" + world.getWorld().getName() + "' with no regions", thr); + } + } + + @Override + public final void run() { + // await scheduler termination + LOGGER.info("Awaiting scheduler termination for 60s"); + if (TickRegions.getScheduler().halt(true, TimeUnit.SECONDS.toNanos(60L))) { + LOGGER.info("Scheduler halted"); + } else { + LOGGER.warn("Scheduler did not terminate within 60s, proceeding with shutdown anyways"); + TickRegions.getScheduler().dumpAliveThreadTraces("Did not shut down in time"); + } + + MinecraftServer.getServer().stopServer(); // stop part 1: most logic, kicking players, plugins, etc + // halt all chunk systems first so that any in-progress chunk generation stops + LOGGER.info("Halting chunk systems"); + for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { + try { + world.chunkTaskScheduler.halt(false, 0L); + } catch (final Throwable throwable) { + LOGGER.error("Failed to soft halt chunk system for world '" + world.getWorld().getName() + "'", throwable); + } + } + for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { + this.haltChunkSystem(world); + } + LOGGER.info("Halted chunk systems"); + for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { + final List> + regions = new ArrayList<>(); + world.regioniser.computeForAllRegionsUnsynchronised(regions::add); + + for (int i = 0, len = regions.size(); i < len; ++i) { + final ThreadedRegionizer.ThreadedRegion region = regions.get(i); + this.finishTeleportations(region, world); + } + + for (int i = 0, len = regions.size(); i < len; ++i) { + final ThreadedRegionizer.ThreadedRegion region = regions.get(i); + this.saveRegionChunks(region, (i + 1) == len); + } + + this.saveLevelData(world); + } + // moved from stop part 1 + // we need this to be after saving level data, as that will complete any teleportations the player is in + LOGGER.info("Saving players"); + MinecraftServer.getServer().getPlayerList().saveAll(); + + MinecraftServer.getServer().stopPart2(); // stop part 2: close other resources (io thread, etc) + // done, part 2 should call exit() + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java new file mode 100644 index 0000000000000000000000000000000000000000..1f48ada99d6d24880f9bda1cd05d41a4562e42f5 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java @@ -0,0 +1,235 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.util.Validate; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.server.level.ServerLevel; +import javax.annotation.Nullable; +import java.util.function.Supplier; + +/** + * Use to manage data that needs to be regionised. + *

+ * Note: that unlike {@link ThreadLocal}, regionised data is not deleted once the {@code RegionizedData} object is GC'd. + * The data is held in reference to the world it resides in. + *

+ *

+ * Note: Keep in mind that when regionised ticking is disabled, the entire server is considered a single region. + * That is, the data may or may not cross worlds. As such, the {@code RegionizedData} object must be instanced + * per world when appropriate, as it is no longer guaranteed that separate worlds contain separate regions. + * See below for more details on instancing per world. + *

+ *

+ * Regionised data may be world-checked. That is, {@link #get()} may throw an exception if the current + * region's world does not match the {@code RegionizedData}'s world. Consider the usages of {@code RegionizedData} below + * see why the behavior may or may not be desirable: + *

+ *         {@code
+ *         public class EntityTickList {
+ *             private final List entities = new ArrayList<>();
+ *
+ *             public void addEntity(Entity e) {
+ *                 this.entities.add(e);
+ *             }
+ *
+ *             public void removeEntity(Entity e) {
+ *                 this.entities.remove(e);
+ *             }
+ *         }
+ *
+ *         public class World {
+ *
+ *             // callback is left out of this example
+ *             // note: world != null here
+ *             public final RegionizedData entityTickLists =
+ *                 new RegionizedData<>(this, () -> new EntityTickList(), ...);
+ *
+ *             public void addTickingEntity(Entity e) {
+ *                 // What we expect here is that this world is the
+ *                 // current ticking region's world.
+ *                 // If that is true, then calling this.entityTickLists.get()
+ *                 // will retrieve the current region's EntityTickList
+ *                 // for this world, which is fine since the current
+ *                 // region is contained within this world.
+ *
+ *                 // But if the current region's world is not this world,
+ *                 // and if the world check is disabled, then we will actually
+ *                 // retrieve _this_ world's EntityTickList for the region,
+ *                 // and NOT the EntityTickList for the region's world.
+ *                 // This is because the RegionizedData object is instantiated
+ *                 // per world.
+ *                 this.entityTickLists.get().addEntity(e);
+ *             }
+ *         }
+ *
+ *         public class TickTimes {
+ *
+ *             private final List tickTimesNS = new ArrayList<>();
+ *
+ *             public void completeTick(long timeNS) {
+ *                 this.tickTimesNS.add(timeNS);
+ *             }
+ *
+ *             public double getAverageTickLengthMS() {
+ *                 double sum = 0.0;
+ *                 for (long time : tickTimesNS) {
+ *                     sum += (double)time;
+ *                 }
+ *                 return (sum / this.tickTimesNS.size()) / 1.0E6; // 1ms = 1 million ns
+ *             }
+ *         }
+ *
+ *         public class Server {
+ *             public final List worlds = ...;
+ *
+ *             // callback is left out of this example
+ *             // note: world == null here, because this RegionizedData object
+ *             // is not instantiated per world, but rather globally.
+ *             public final RegionizedData tickTimes =
+ *                  new RegionizedData<>(null, () -> new TickTimes(), ...);
+ *         }
+ *         }
+ *     
+ * In general, it is advised that if a RegionizedData object is instantiated per world, that world checking + * is enabled for it by passing the world to the constructor. + *

+ */ +public final class RegionizedData { + + private final ServerLevel world; + private final Supplier initialValueSupplier; + private final RegioniserCallback callback; + + /** + * Creates a regionised data holder. The provided initial value supplier may not be null, and it must + * never produce {@code null} values. + *

+ * Note that the supplier or regioniser callback may be used while the region lock is held, so any blocking + * operations may deadlock the entire server and as such the function should be completely non-blocking + * and must complete in a timely manner. + *

+ *

+ * If the provided world is {@code null}, then the world checks are disabled. The world should only ever + * be {@code null} if the data is specifically not specific to worlds. For example, using {@code null} + * for an entity tick list is invalid since the entities are tied to a world and region, + * however using {@code null} for tasks to run at the end of a tick is valid since the tasks are tied to + * region only. + *

+ * @param world The world in which the region data resides. + * @param supplier Initial value supplier used to lazy initialise region data. + * @param callback Region callback to manage this regionised data. + */ + public RegionizedData(final ServerLevel world, final Supplier supplier, final RegioniserCallback callback) { + this.world = world; + this.initialValueSupplier = Validate.notNull(supplier, "Supplier may not be null."); + this.callback = Validate.notNull(callback, "Regioniser callback may not be null."); + } + + T createNewValue() { + return Validate.notNull(this.initialValueSupplier.get(), "Initial value supplier may not return null"); + } + + RegioniserCallback getCallback() { + return this.callback; + } + + /** + * Returns the current data type for the current ticking region. If there is no region, returns {@code null}. + * @return the current data type for the current ticking region. If there is no region, returns {@code null}. + * @throws IllegalStateException If the following are true: The server is in region ticking mode, + * this {@code RegionizedData}'s world is not {@code null}, + * and the current ticking region's world does not match this {@code RegionizedData}'s world. + */ + public @Nullable T get() { + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + + if (region == null) { + return null; + } + + if (this.world != null && this.world != region.getData().world) { + throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey()); + } + + return region.getData().getOrCreateRegionizedData(this); + } + + /** + * Class responsible for handling merge / split requests from the regioniser. + *

+ * It is critical to note that each function is called while holding the region lock. + *

+ */ + public static interface RegioniserCallback { + + /** + * Completely merges the data in {@code from} to {@code into}. + *

+ * Calculating Tick Offsets: + * Sometimes data stores absolute tick deadlines, and since regions tick independently, absolute deadlines + * are not comparable across regions. Consider absolute deadlines {@code deadlineFrom, deadlineTo} in + * regions {@code from} and {@code into} respectively. We can calculate the relative deadline for the from + * region with {@code relFrom = deadlineFrom - currentTickFrom}. Then, we can use the same equation for + * computing the absolute deadline in region {@code into} that has the same relative deadline as {@code from} + * as {@code deadlineTo = relFrom + currentTickTo}. By substituting {@code relFrom} as {@code deadlineFrom - currentTickFrom}, + * we finally have that {@code deadlineTo = deadlineFrom + (currentTickTo - currentTickFrom)} and + * that we can use an offset {@code fromTickOffset = currentTickTo - currentTickFrom} to calculate + * {@code deadlineTo} as {@code deadlineTo = deadlineFrom + fromTickOffset}. + *

+ *

+ * Critical Notes: + *

  • + *
      + * This function is called while the region lock is held, so any blocking operations may + * deadlock the entire server and as such the function should be completely non-blocking and must complete + * in a timely manner. + *
    + *
      + * This function may not throw any exceptions, or the server will be left in an unrecoverable state. + *
    + *
  • + *

    + * + * @param from The data to merge from. + * @param into The data to merge into. + * @param fromTickOffset The addend to absolute tick deadlines stored in the {@code from} region to adjust to the into region. + */ + public void merge(final T from, final T into, final long fromTickOffset); + + /** + * Splits the data in {@code from} into {@code dataSet}. + *

    + * The chunk coordinate to region section coordinate bit shift amount is provided in {@code chunkToRegionShift}. + * To convert from chunk coordinates to region coordinates and keys, see the code below: + *

    +         *         {@code
    +         *         int chunkX = ...;
    +         *         int chunkZ = ...;
    +         *
    +         *         int regionSectionX = chunkX >> chunkToRegionShift;
    +         *         int regionSectionZ = chunkZ >> chunkToRegionShift;
    +         *         long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(regionSectionX, regionSectionZ);
    +         *         }
    +         *     
    + *

    + *

    + * The {@code regionToData} hashtable provides a lookup from {@code regionSectionKey} (see above) to the + * data that is owned by the region which occupies the region section. + *

    + *

    + * Unlike {@link #merge(Object, Object, long)}, there is no absolute tick offset provided. This is because + * the new regions formed from the split will start at the same tick number, and so no adjustment is required. + *

    + * + * @param from The data to split from. + * @param chunkToRegionShift The signed right-shift value used to convert chunk coordinates into region section coordinates. + * @param regionToData Lookup hash table from region section key to . + * @param dataSet The data set to split into. + */ + public void split( + final T from, final int chunkToRegionShift, + final Long2ReferenceOpenHashMap regionToData, final ReferenceOpenHashSet dataSet + ); + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java new file mode 100644 index 0000000000000000000000000000000000000000..6c1d55144f044f39926ddf998104950b9efe3ee1 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java @@ -0,0 +1,348 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; +import com.mojang.logging.LogUtils; +import io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler; +import io.papermc.paper.util.TickThread; +import net.minecraft.CrashReport; +import net.minecraft.ReportedException; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketListener; +import net.minecraft.network.PacketSendListener; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.protocol.game.ClientboundDisconnectPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.dedicated.DedicatedServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.network.ServerLoginPacketListenerImpl; +import net.minecraft.world.level.GameRules; +import org.bukkit.Bukkit; +import org.slf4j.Logger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; + +public final class RegionizedServer { + + private static final Logger LOGGER = LogUtils.getLogger(); + private static final RegionizedServer INSTANCE = new RegionizedServer(); + + public final RegionizedTaskQueue taskQueue = new RegionizedTaskQueue(); + + private final CopyOnWriteArrayList worlds = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList connections = new CopyOnWriteArrayList<>(); + + private final MultiThreadedQueue globalTickQueue = new MultiThreadedQueue<>(); + + private final GlobalTickTickHandle tickHandle = new GlobalTickTickHandle(this); + + public static RegionizedServer getInstance() { + return INSTANCE; + } + + public void addConnection(final Connection conn) { + this.connections.add(conn); + } + + private boolean removeConnection(final Connection conn) { + return this.connections.remove(conn); + } + + public void addWorld(final ServerLevel world) { + this.worlds.add(world); + } + + public void init() { + // call init event _before_ scheduling anything + new RegionizedServerInitEvent().callEvent(); + + // now we can schedule + this.tickHandle.setInitialStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS); + TickRegions.getScheduler().scheduleRegion(this.tickHandle); + TickRegions.getScheduler().init(); + } + + public void invalidateStatus() { + this.lastServerStatus = 0L; + } + + public void addTaskWithoutNotify(final Runnable run) { + this.globalTickQueue.add(run); + } + + public void addTask(final Runnable run) { + this.addTaskWithoutNotify(run); + TickRegions.getScheduler().setHasTasks(this.tickHandle); + } + + /** + * Returns the current tick of the region ticking. + * @throws IllegalStateException If there is no current region. + */ + public static long getCurrentTick() throws IllegalStateException { + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + if (region == null) { + if (TickThread.isShutdownThread()) { + return 0L; + } + throw new IllegalStateException("No currently ticking region"); + } + return region.getData().getCurrentTick(); + } + + public static boolean isGlobalTickThread() { + return INSTANCE.tickHandle == TickRegionScheduler.getCurrentTickingTask(); + } + + public static void ensureGlobalTickThread(final String reason) { + if (!isGlobalTickThread()) { + throw new IllegalStateException(reason); + } + } + + public static TickRegionScheduler.RegionScheduleHandle getGlobalTickData() { + return INSTANCE.tickHandle; + } + + private static final class GlobalTickTickHandle extends TickRegionScheduler.RegionScheduleHandle { + + private final RegionizedServer server; + + private final AtomicBoolean scheduled = new AtomicBoolean(); + private final AtomicBoolean ticking = new AtomicBoolean(); + + public GlobalTickTickHandle(final RegionizedServer server) { + super(null, SchedulerThreadPool.DEADLINE_NOT_SET); + this.server = server; + } + + /** + * Only valid to call BEFORE scheduled!!!! + */ + final void setInitialStart(final long start) { + if (this.scheduled.getAndSet(true)) { + throw new IllegalStateException("Double scheduling global tick"); + } + this.updateScheduledStart(start); + } + + @Override + protected boolean tryMarkTicking() { + return !this.ticking.getAndSet(true); + } + + @Override + protected boolean markNotTicking() { + return this.ticking.getAndSet(false); + } + + @Override + protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) { + this.drainTasks(); + this.server.globalTick(tickCount); + } + + private void drainTasks() { + while (this.runOneTask()); + } + + private boolean runOneTask() { + final Runnable run = this.server.globalTickQueue.poll(); + if (run == null) { + return false; + } + + // TODO try catch? + run.run(); + + return true; + } + + @Override + protected boolean runRegionTasks(final BooleanSupplier canContinue) { + do { + if (!this.runOneTask()) { + return false; + } + } while (canContinue.getAsBoolean()); + + return true; + } + + @Override + protected boolean hasIntermediateTasks() { + return !this.server.globalTickQueue.isEmpty(); + } + } + + private long lastServerStatus; + private long tickCount; + + private void globalTick(final int tickCount) { + ++this.tickCount; + // expire invalid click command callbacks + io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue((int)this.tickCount); + + // scheduler + ((FoliaGlobalRegionScheduler)Bukkit.getGlobalRegionScheduler()).tick(); + + // commands + ((DedicatedServer)MinecraftServer.getServer()).handleConsoleInputs(); + + // needs + // player ping sample + // world global tick + // connection tick + + // tick player ping sample + this.tickPlayerSample(); + + // tick worlds + for (final ServerLevel world : this.worlds) { + this.globalTick(world, tickCount); + } + + // tick connections + this.tickConnections(); + + // player list + MinecraftServer.getServer().getPlayerList().tick(); + } + + private void tickPlayerSample() { + final MinecraftServer mcServer = MinecraftServer.getServer(); + + final long currtime = System.nanoTime(); + + // player ping sample + // copied from MinecraftServer#tickServer + // note: we need to reorder setPlayers to be the last operation it does, rather than the first to avoid publishing + // an uncomplete status + if (currtime - this.lastServerStatus >= 5000000000L) { + this.lastServerStatus = currtime; + mcServer.rebuildServerStatus(); + } + } + + private boolean hasConnectionMovedToMain(final Connection conn) { + final PacketListener packetListener = conn.getPacketListener(); + + return (packetListener instanceof ServerGamePacketListenerImpl) || + (packetListener instanceof ServerLoginPacketListenerImpl loginListener && loginListener.state.ordinal() >= ServerLoginPacketListenerImpl.State.HANDING_OFF.ordinal()); + } + + private void tickConnections() { + final List connections = new ArrayList<>(this.connections); + Collections.shuffle(connections); // shuffle to prevent people from "gaming" the server by re-logging + for (final Connection conn : connections) { + if (!conn.becomeActive()) { + continue; + } + + if (this.hasConnectionMovedToMain(conn)) { + if (!conn.isConnected()) { + this.removeConnection(conn); + } + continue; + } + + if (!conn.isConnected()) { + this.removeConnection(conn); + conn.handleDisconnection(); + continue; + } + + try { + conn.tick(); + } catch (final Exception exception) { + if (conn.isMemoryConnection()) { + throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection")); + } + + LOGGER.warn("Failed to handle packet for {}", io.papermc.paper.configuration.GlobalConfiguration.get().logging.logPlayerIpAddresses ? String.valueOf(conn.getRemoteAddress()) : "", exception); // Paper + MutableComponent ichatmutablecomponent = Component.literal("Internal server error"); + + conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> { + conn.disconnect(ichatmutablecomponent); + })); + conn.setReadOnly(); + continue; + } + } + } + + // A global tick only updates things like weather / worldborder, basically anything in the world that is + // NOT tied to a specific region, but rather shared amongst all of them. + private void globalTick(final ServerLevel world, final int tickCount) { + // needs + // worldborder tick + // advancing the weather cycle + // sleep status thing + // updating sky brightness + // time ticking (game time + daylight), plus PrimayLevelDat#getScheduledEvents ticking + + // Typically, we expect there to be a running region to drain a world's global chunk tasks. However, + // this may not be the case - and thus, only the global tick thread can do anything. + world.taskQueueRegionData.drainGlobalChunkTasks(); + + // worldborder tick + this.tickWorldBorder(world); + + // weather cycle + this.advanceWeatherCycle(world); + + // sleep status + this.checkNightSkip(world); + + // update raids + this.updateRaids(world); + + // sky brightness + this.updateSkyBrightness(world); + + // time ticking (TODO API synchronisation?) + this.tickTime(world, tickCount); + + world.updateTickData(); + } + + private void updateRaids(final ServerLevel world) { + world.getRaids().globalTick(); + } + + private void checkNightSkip(final ServerLevel world) { + world.tickSleep(); + } + + private void advanceWeatherCycle(final ServerLevel world) { + world.advanceWeatherCycle(); + } + + private void updateSkyBrightness(final ServerLevel world) { + world.updateSkyBrightness(); + } + + private void tickWorldBorder(final ServerLevel world) { + world.getWorldBorder().tick(); + } + + private void tickTime(final ServerLevel world, final int tickCount) { + if (world.tickTime) { + if (world.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { + world.setDayTime(world.levelData.getDayTime() + (long)tickCount); + } + world.serverLevelData.setGameTime(world.serverLevelData.getGameTime() + (long)tickCount); + } + } + + public static final record WorldLevelData(ServerLevel world, long nonRedstoneGameTime, long dayTime) { + + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..ac043fbc74874c205b821c3d2d011b921d5704fa --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java @@ -0,0 +1,752 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager; +import io.papermc.paper.util.CoordinateUtils; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.util.Unit; +import java.lang.invoke.VarHandle; +import java.util.ArrayDeque; +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; + +public final class RegionizedTaskQueue { + + private static final TicketType TASK_QUEUE_TICKET = TicketType.create("task_queue_ticket", (a, b) -> 0); + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run) { + return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, true, run, PrioritisedExecutor.Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run, final PrioritisedExecutor.Priority priority) { + return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, true, run, priority); + } + + public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run) { + return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, false, run, PrioritisedExecutor.Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run, final PrioritisedExecutor.Priority priority) { + return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, false, run, priority); + } + + public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run) { + return this.queueChunkTask(world, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run, final PrioritisedExecutor.Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(world, chunkX, chunkZ, run, priority); + ret.queue(); + return ret; + } + + public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run) { + return this.queueTickTaskQueue(world, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run, final PrioritisedExecutor.Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.createTickTaskQueue(world, chunkX, chunkZ, run, priority); + ret.queue(); + return ret; + } + + public static final class WorldRegionTaskData { + private final ServerLevel world; + private final MultiThreadedQueue globalChunkTask = new MultiThreadedQueue<>(); + private final SWMRLong2ObjectHashTable referenceCounters = new SWMRLong2ObjectHashTable<>(); + + public WorldRegionTaskData(final ServerLevel world) { + this.world = world; + } + + private boolean executeGlobalChunkTask() { + final Runnable run = this.globalChunkTask.poll(); + if (run != null) { + run.run(); + return true; + } + return false; + } + + public void drainGlobalChunkTasks() { + while (this.executeGlobalChunkTask()); + } + + public void pushGlobalChunkTask(final Runnable run) { + this.globalChunkTask.add(run); + } + + private PrioritisedQueue getQueue(final boolean synchronise, final int chunkX, final int chunkZ, final boolean isChunkTask) { + final ThreadedRegionizer regioniser = this.world.regioniser; + final ThreadedRegionizer.ThreadedRegion region + = synchronise ? regioniser.getRegionAtSynchronised(chunkX, chunkZ) : regioniser.getRegionAtUnsynchronised(chunkX, chunkZ); + if (region == null) { + return null; + } + final RegionTaskQueueData taskQueueData = region.getData().getTaskQueueData(); + return (isChunkTask ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue); + } + + private void removeTicket(final long coord) { + this.world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel( + TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE + ); + } + + private void addTicket(final long coord) { + this.world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel( + TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE + ); + } + + private void decrementReference(final AtomicLong reference, final long coord) { + final long val = reference.decrementAndGet(); + if (val == 0L) { + final ReentrantLock ticketLock = this.world.chunkTaskScheduler.chunkHolderManager.ticketLock; + ticketLock.lock(); + try { + if (this.referenceCounters.remove(coord, reference)) { + WorldRegionTaskData.this.removeTicket(coord); + } // else: race condition, something replaced our reference - not our issue anymore + } finally { + ticketLock.unlock(); + } + } else if (val < 0L) { + throw new IllegalStateException("Reference count < 0: " + val); + } + } + + private AtomicLong incrementReference(final long coord) { + final AtomicLong ret = this.referenceCounters.get(coord); + if (ret != null) { + // try to fast acquire counter + int failures = 0; + for (long curr = ret.get();;) { + if (curr == 0L) { + // failed to fast acquire as reference expired + break; + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = ret.compareAndExchange(curr, curr + 1L))) { + return ret; + } + + ++failures; + } + } + + // slow acquire + final ReentrantLock ticketLock = this.world.chunkTaskScheduler.chunkHolderManager.ticketLock; + ticketLock.lock(); + try { + final AtomicLong replace = new AtomicLong(1L); + final AtomicLong valueInMap = this.referenceCounters.putIfAbsent(coord, replace); + if (valueInMap == null) { + // replaced, we should usually be here + this.addTicket(coord); + return replace; + } // else: need to attempt to acquire the reference + + int failures = 0; + for (long curr = valueInMap.get();;) { + if (curr == 0L) { + // don't need to add ticket here, since ticket is only removed during the lock + // we just need to replace the value in the map so that the thread removing fails and doesn't + // remove the ticket (see decrementReference) + this.referenceCounters.put(coord, replace); + return replace; + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = valueInMap.compareAndExchange(curr, curr + 1L))) { + // acquired + return valueInMap; + } + + ++failures; + } + } finally { + ticketLock.unlock(); + } + } + } + + public static final class RegionTaskQueueData { + private final PrioritisedQueue tickTaskQueue = new PrioritisedQueue(); + private final PrioritisedQueue chunkQueue = new PrioritisedQueue(); + private final WorldRegionTaskData worldRegionTaskData; + + public RegionTaskQueueData(final WorldRegionTaskData worldRegionTaskData) { + this.worldRegionTaskData = worldRegionTaskData; + } + + void mergeInto(final RegionTaskQueueData into) { + this.tickTaskQueue.mergeInto(into.tickTaskQueue); + this.chunkQueue.mergeInto(into.chunkQueue); + } + + public boolean executeTickTask() { + return this.tickTaskQueue.executeTask(); + } + + public boolean executeChunkTask() { + return this.worldRegionTaskData.executeGlobalChunkTask() || this.chunkQueue.executeTask(); + } + + void split(final ThreadedRegionizer regioniser, + final Long2ReferenceOpenHashMap> into) { + this.tickTaskQueue.split( + false, regioniser, into + ); + this.chunkQueue.split( + true, regioniser, into + ); + } + + public void drainTasks() { + // first, update chunk loader + final ServerLevel world = this.worldRegionTaskData.world; + if (world.playerChunkLoader.tickMidTick()) { + // only process ticket updates if the player chunk loader did anything + world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); + } + + final PrioritisedQueue tickTaskQueue = this.tickTaskQueue; + final PrioritisedQueue chunkTaskQueue = this.chunkQueue; + + int allowedTickTasks = tickTaskQueue.getScheduledTasks(); + int allowedChunkTasks = chunkTaskQueue.getScheduledTasks(); + + boolean executeTickTasks = allowedTickTasks > 0; + boolean executeChunkTasks = allowedChunkTasks > 0; + boolean executeGlobalTasks = true; + + do { + executeTickTasks = executeTickTasks && allowedTickTasks-- > 0 && tickTaskQueue.executeTask(); + executeChunkTasks = executeChunkTasks && allowedChunkTasks-- > 0 && chunkTaskQueue.executeTask(); + executeGlobalTasks = executeGlobalTasks && this.worldRegionTaskData.executeGlobalChunkTask(); + } while (executeTickTasks | executeChunkTasks | executeGlobalTasks); + + if (allowedChunkTasks > 0) { + // if we executed chunk tasks, we should try to process ticket updates for full status changes + world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); + } + } + + public boolean hasTasks() { + return !this.tickTaskQueue.isEmpty() || !this.chunkQueue.isEmpty(); + } + } + + static final class PrioritisedQueue { + private final ArrayDeque[] queues = new ArrayDeque[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES]; { + for (int i = 0; i < PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) { + this.queues[i] = new ArrayDeque<>(); + } + } + private boolean isDestroyed; + + public int getScheduledTasks() { + synchronized (this) { + int ret = 0; + + for (final ArrayDeque queue : this.queues) { + ret += queue.size(); + } + + return ret; + } + } + + public boolean isEmpty() { + final ArrayDeque[] queues = this.queues; + final int max = PrioritisedExecutor.Priority.IDLE.priority; + synchronized (this) { + for (int i = 0; i <= max; ++i) { + if (!queues[i].isEmpty()) { + return false; + } + } + return true; + } + } + + public void mergeInto(final PrioritisedQueue target) { + synchronized (this) { + this.isDestroyed = true; + synchronized (target) { + mergeInto(target, this.queues); + } + } + } + + private static void mergeInto(final PrioritisedQueue target, final ArrayDeque[] thisQueues) { + final ArrayDeque[] otherQueues = target.queues; + for (int i = 0; i < thisQueues.length; ++i) { + final ArrayDeque fromQ = thisQueues[i]; + final ArrayDeque intoQ = otherQueues[i]; + + // it is possible for another thread to queue tasks into the target queue before we do + // since only the ticking region can poll, we don't have to worry about it when they are being queued - + // but when we are merging, we need to ensure order is maintained (notwithstanding priority changes) + // we can ensure order is maintained by adding all of the tasks from the fromQ into the intoQ at the + // front of the queue, but we need to use descending iterator to ensure we do not reverse + // the order of elements from fromQ + for (final Iterator iterator = fromQ.descendingIterator(); iterator.hasNext();) { + intoQ.addFirst(iterator.next()); + } + } + } + + // into is a map of section coordinate to region + public void split(final boolean isChunkData, + final ThreadedRegionizer regioniser, + final Long2ReferenceOpenHashMap> into) { + final Reference2ReferenceOpenHashMap, ArrayDeque[]> + split = new Reference2ReferenceOpenHashMap<>(); + final int shift = regioniser.sectionChunkShift; + synchronized (this) { + this.isDestroyed = true; + // like mergeTarget, we need to be careful about insertion order so we can maintain order when splitting + + // first, build the targets + final ArrayDeque[] thisQueues = this.queues; + for (int i = 0; i < thisQueues.length; ++i) { + final ArrayDeque fromQ = thisQueues[i]; + + for (final ChunkBasedPriorityTask task : fromQ) { + final int sectionX = task.chunkX >> shift; + final int sectionZ = task.chunkZ >> shift; + final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); + final ThreadedRegionizer.ThreadedRegion + region = into.get(sectionKey); + if (region == null) { + throw new IllegalStateException(); + } + + split.computeIfAbsent(region, (keyInMap) -> { + final ArrayDeque[] ret = new ArrayDeque[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES]; + + for (int k = 0; k < ret.length; ++k) { + ret[k] = new ArrayDeque<>(); + } + + return ret; + })[i].add(task); + } + } + + // merge the targets into their queues + for (final Iterator, ArrayDeque[]>> + iterator = split.reference2ReferenceEntrySet().fastIterator(); + iterator.hasNext();) { + final Reference2ReferenceMap.Entry, ArrayDeque[]> + entry = iterator.next(); + final RegionTaskQueueData taskQueueData = entry.getKey().getData().getTaskQueueData(); + mergeInto(isChunkData ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue, entry.getValue()); + } + } + } + + /** + * returns null if the task cannot be scheduled, returns false if this task queue is dead, and returns true + * if the task was added + */ + private Boolean tryPush(final ChunkBasedPriorityTask task) { + final ArrayDeque[] queues = this.queues; + synchronized (this) { + final PrioritisedExecutor.Priority priority = task.getPriority(); + if (priority == PrioritisedExecutor.Priority.COMPLETING) { + return null; + } + if (this.isDestroyed) { + return Boolean.FALSE; + } + queues[priority.priority].addLast(task); + return Boolean.TRUE; + } + } + + private boolean executeTask() { + final ArrayDeque[] queues = this.queues; + final int max = PrioritisedExecutor.Priority.IDLE.priority; + ChunkBasedPriorityTask task = null; + AtomicLong referenceCounter = null; + synchronized (this) { + if (this.isDestroyed) { + throw new IllegalStateException("Attempting to poll from dead queue"); + } + + search_loop: + for (int i = 0; i <= max; ++i) { + final ArrayDeque queue = queues[i]; + while ((task = queue.pollFirst()) != null) { + if ((referenceCounter = task.trySetCompleting(i)) != null) { + break search_loop; + } + } + } + } + + if (task == null) { + return false; + } + + try { + task.executeInternal(); + } finally { + task.world.decrementReference(referenceCounter, task.sectionLowerLeftCoord); + } + + return true; + } + + private static final class ChunkBasedPriorityTask implements PrioritisedExecutor.PrioritisedTask { + + private static final AtomicLong REFERENCE_COUNTER_NOT_SET = new AtomicLong(-1L); + + private final WorldRegionTaskData world; + private final int chunkX; + private final int chunkZ; + private final long sectionLowerLeftCoord; // chunk coordinate + private final boolean isChunkTask; + + private volatile AtomicLong referenceCounter; + private static final VarHandle REFERENCE_COUNTER_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "referenceCounter", AtomicLong.class); + private Runnable run; + private volatile PrioritisedExecutor.Priority priority; + private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "priority", PrioritisedExecutor.Priority.class); + + ChunkBasedPriorityTask(final WorldRegionTaskData world, final int chunkX, final int chunkZ, final boolean isChunkTask, + final Runnable run, final PrioritisedExecutor.Priority priority) { + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.isChunkTask = isChunkTask; + this.run = run; + this.setReferenceCounterPlain(REFERENCE_COUNTER_NOT_SET); + this.setPriorityPlain(priority); + + final int regionShift = world.world.regioniser.sectionChunkShift; + final int regionMask = (1 << regionShift) - 1; + + this.sectionLowerLeftCoord = CoordinateUtils.getChunkKey(chunkX & ~regionMask, chunkZ & ~regionMask); + } + + private PrioritisedExecutor.Priority getPriorityVolatile() { + return (PrioritisedExecutor.Priority)PRIORITY_HANDLE.getVolatile(this); + } + + private void setPriorityPlain(final PrioritisedExecutor.Priority priority) { + PRIORITY_HANDLE.set(this, priority); + } + + private void setPriorityVolatile(final PrioritisedExecutor.Priority priority) { + PRIORITY_HANDLE.setVolatile(this, priority); + } + + private PrioritisedExecutor.Priority compareAndExchangePriority(final PrioritisedExecutor.Priority expect, final PrioritisedExecutor.Priority update) { + return (PrioritisedExecutor.Priority)PRIORITY_HANDLE.compareAndExchange(this, expect, update); + } + + private void setReferenceCounterPlain(final AtomicLong value) { + REFERENCE_COUNTER_HANDLE.set(this, value); + } + + private AtomicLong getReferenceCounterVolatile() { + return (AtomicLong)REFERENCE_COUNTER_HANDLE.get(this); + } + + private AtomicLong compareAndExchangeReferenceCounter(final AtomicLong expect, final AtomicLong update) { + return (AtomicLong)REFERENCE_COUNTER_HANDLE.compareAndExchange(this, expect, update); + } + + private void executeInternal() { + try { + this.run.run(); + } finally { + this.run = null; + } + } + + private void cancelInternal() { + this.run = null; + } + + private boolean tryComplete(final boolean cancel) { + int failures = 0; + for (AtomicLong curr = this.getReferenceCounterVolatile();;) { + if (curr == null) { + return false; + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) { + ++failures; + continue; + } + + // we have the reference count, we win no matter what. + this.setPriorityVolatile(PrioritisedExecutor.Priority.COMPLETING); + + try { + if (cancel) { + this.cancelInternal(); + } else { + this.executeInternal(); + } + } finally { + if (curr != REFERENCE_COUNTER_NOT_SET) { + this.world.decrementReference(curr, this.sectionLowerLeftCoord); + } + } + + return true; + } + } + + @Override + public boolean queue() { + if (this.getReferenceCounterVolatile() != REFERENCE_COUNTER_NOT_SET) { + return false; + } + + final AtomicLong referenceCounter = this.world.incrementReference(this.sectionLowerLeftCoord); + if (this.compareAndExchangeReferenceCounter(REFERENCE_COUNTER_NOT_SET, referenceCounter) != REFERENCE_COUNTER_NOT_SET) { + // we don't expect race conditions here, so it is OK if we have to needlessly reference count + this.world.decrementReference(referenceCounter, this.sectionLowerLeftCoord); + return false; + } + + boolean synchronise = false; + for (;;) { + // we need to synchronise for repeated operations so that we guarantee that we do not retrieve + // the same queue again, as the region lock will be given to us only when the merge/split operation + // is done + final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask); + + if (queue == null) { + if (!synchronise) { + // may be incorrectly null when unsynchronised + continue; + } + // may have been cancelled before we got to the queue + if (this.getReferenceCounterVolatile() != null) { + throw new IllegalStateException("Expected null ref count when queue does not exist"); + } + // the task never could be polled from the queue, so we return false + // don't decrement reference count, as we were certainly cancelled by another thread, which + // will decrement the reference count + return false; + } + + synchronise = true; + + final Boolean res = queue.tryPush(this); + if (res == null) { + // we were cancelled + // don't decrement reference count, as we were certainly cancelled by another thread, which + // will decrement the reference count + return false; + } + + if (!res.booleanValue()) { + // failed, try again + continue; + } + + // successfully queued + return true; + } + } + + private AtomicLong trySetCompleting(final int minPriority) { + // first, try to set priority to EXECUTING + for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) { + if (curr.isLowerPriority(minPriority)) { + return null; + } + + if (curr == (curr = this.compareAndExchangePriority(curr, PrioritisedExecutor.Priority.COMPLETING))) { + break; + } // else: continue + } + + for (AtomicLong curr = this.getReferenceCounterVolatile();;) { + if (curr == null) { + // something acquired before us + return null; + } + + if (curr == REFERENCE_COUNTER_NOT_SET) { + throw new IllegalStateException(); + } + + if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) { + continue; + } + return curr; + } + } + + private void updatePriorityInQueue() { + boolean synchronise = false; + for (;;) { + final AtomicLong referenceCount = this.getReferenceCounterVolatile(); + if (referenceCount == REFERENCE_COUNTER_NOT_SET || referenceCount == null) { + // cancelled or not queued + return; + } + + if (this.getPriorityVolatile() == PrioritisedExecutor.Priority.COMPLETING) { + // cancelled + return; + } + + // we need to synchronise for repeated operations so that we guarantee that we do not retrieve + // the same queue again, as the region lock will be given to us only when the merge/split operation + // is done + final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask); + + if (queue == null) { + if (!synchronise) { + // may be incorrectly null when unsynchronised + continue; + } + // must have been removed + return; + } + + synchronise = true; + + final Boolean res = queue.tryPush(this); + if (res == null) { + // we were cancelled + return; + } + + if (!res.booleanValue()) { + // failed, try again + continue; + } + + // successfully queued + return; + } + } + + @Override + public PrioritisedExecutor.Priority getPriority() { + return this.getPriorityVolatile(); + } + + @Override + public boolean lowerPriority(final PrioritisedExecutor.Priority priority) { + int failures = 0; + for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) { + if (curr == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (curr.isLowerOrEqualPriority(priority)) { + return false; + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { + this.updatePriorityInQueue(); + return true; + } + ++failures; + } + } + + @Override + public boolean setPriority(final PrioritisedExecutor.Priority priority) { + int failures = 0; + for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) { + if (curr == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (curr == priority) { + return false; + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { + this.updatePriorityInQueue(); + return true; + } + ++failures; + } + } + + @Override + public boolean raisePriority(final PrioritisedExecutor.Priority priority) { + int failures = 0; + for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) { + if (curr == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (curr.isHigherOrEqualPriority(priority)) { + return false; + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { + this.updatePriorityInQueue(); + return true; + } + ++failures; + } + } + + @Override + public boolean execute() { + return this.tryComplete(false); + } + + @Override + public boolean cancel() { + return this.tryComplete(true); + } + } + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java new file mode 100644 index 0000000000000000000000000000000000000000..89e6dc92bfbb28d20f252eca5257db1d3d042327 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java @@ -0,0 +1,685 @@ +package io.papermc.paper.threadedregions; + +import com.destroystokyo.paper.util.maplist.ReferenceList; +import com.destroystokyo.paper.util.misc.PlayerAreaMap; +import com.destroystokyo.paper.util.misc.PooledLinkedHashSets; +import com.mojang.logging.LogUtils; +import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager; +import io.papermc.paper.util.CoordinateUtils; +import io.papermc.paper.util.TickThread; +import io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.CrashReport; +import net.minecraft.ReportedException; +import net.minecraft.core.BlockPos; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketSendListener; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.protocol.game.ClientboundDisconnectPacket; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.util.VisibleForDebug; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.village.VillageSiege; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.BlockEventData; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.TickingBlockEntity; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.redstone.CollectingNeighborUpdater; +import net.minecraft.world.level.redstone.NeighborUpdater; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.ticks.LevelTicks; +import org.bukkit.craftbukkit.block.CraftBlockState; +import org.bukkit.craftbukkit.util.UnsafeList; +import org.slf4j.Logger; +import javax.annotation.Nullable; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public final class RegionizedWorldData { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public static final RegionizedData.RegioniserCallback REGION_CALLBACK = new RegionizedData.RegioniserCallback<>() { + @Override + public void merge(final RegionizedWorldData from, final RegionizedWorldData into, final long fromTickOffset) { + // connections + for (final Connection conn : from.connections) { + into.connections.add(conn); + } + // time + final long fromRedstoneTimeOffset = into.redstoneTime - from.redstoneTime; + // entities + for (final ServerPlayer player : from.localPlayers) { + into.localPlayers.add(player); + } + for (final Entity entity : from.allEntities) { + into.allEntities.add(entity); + entity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); + } + for (final Iterator iterator = from.entityTickList.unsafeIterator(); iterator.hasNext();) { + into.entityTickList.add(iterator.next()); + } + for (final Iterator iterator = from.navigatingMobs.unsafeIterator(); iterator.hasNext();) { + into.navigatingMobs.add(iterator.next()); + } + // block ticking + into.blockEvents.addAll(from.blockEvents); + // ticklists use game time + from.blockLevelTicks.merge(into.blockLevelTicks, fromRedstoneTimeOffset); + from.fluidLevelTicks.merge(into.fluidLevelTicks, fromRedstoneTimeOffset); + + // tile entity ticking + for (final TickingBlockEntity tileEntityWrapped : from.pendingBlockEntityTickers) { + into.pendingBlockEntityTickers.add(tileEntityWrapped); + final BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); + if (tileEntity != null) { + tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); + } + } + for (final TickingBlockEntity tileEntityWrapped : from.blockEntityTickers) { + into.blockEntityTickers.add(tileEntityWrapped); + final BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); + if (tileEntity != null) { + tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); + } + } + + // ticking chunks + for (final Iterator iterator = from.entityTickingChunks.unsafeIterator(); iterator.hasNext();) { + into.entityTickingChunks.add(iterator.next()); + } + // redstone torches + if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) { + if (into.redstoneUpdateInfos == null) { + into.redstoneUpdateInfos = new ArrayDeque<>(); + } + for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) { + info.offsetTime(fromRedstoneTimeOffset); + into.redstoneUpdateInfos.add(info); + } + } + // light chunks being worked on + into.chunksBeingWorkedOn.putAll(from.chunksBeingWorkedOn); + // mob spawning + into.catSpawnerNextTick = Math.max(from.catSpawnerNextTick, into.catSpawnerNextTick); + into.patrolSpawnerNextTick = Math.max(from.patrolSpawnerNextTick, into.patrolSpawnerNextTick); + into.phantomSpawnerNextTick = Math.max(from.phantomSpawnerNextTick, into.phantomSpawnerNextTick); + if (from.wanderingTraderTickDelay != Integer.MIN_VALUE && into.wanderingTraderTickDelay != Integer.MIN_VALUE) { + into.wanderingTraderTickDelay = Math.max(from.wanderingTraderTickDelay, into.wanderingTraderTickDelay); + into.wanderingTraderSpawnDelay = Math.max(from.wanderingTraderSpawnDelay, into.wanderingTraderSpawnDelay); + into.wanderingTraderSpawnChance = Math.max(from.wanderingTraderSpawnChance, into.wanderingTraderSpawnChance); + } + } + + @Override + public void split(final RegionizedWorldData from, final int chunkToRegionShift, + final Long2ReferenceOpenHashMap regionToData, + final ReferenceOpenHashSet dataSet) { + // connections + for (final Connection conn : from.connections) { + final ServerPlayer player = conn.getPlayer(); + final ChunkPos pos = player.chunkPosition(); + // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means + // the chunk holder must _exist_, and so the region section exists. + regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) + .connections.add(conn); + } + // entities + for (final ServerPlayer player : from.localPlayers) { + final ChunkPos pos = player.chunkPosition(); + // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means + // the chunk holder must _exist_, and so the region section exists. + regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) + .localPlayers.add(player); + } + for (final Entity entity : from.allEntities) { + final ChunkPos pos = entity.chunkPosition(); + // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means + // the chunk holder must _exist_, and so the region section exists. + final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)); + into.allEntities.add(entity); + // Note: entityTickList is a subset of allEntities + if (from.entityTickList.contains(entity)) { + into.entityTickList.add(entity); + } + // Note: navigatingMobs is a subset of allEntities + if (entity instanceof Mob mob && from.navigatingMobs.contains(mob)) { + into.navigatingMobs.add(mob); + } + } + // block ticking + for (final BlockEventData blockEventData : from.blockEvents) { + final BlockPos pos = blockEventData.pos(); + final int chunkX = pos.getX() >> 4; + final int chunkZ = pos.getZ() >> 4; + + final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); + // Unlike entities, the chunk holder is not guaranteed to exist for block events, because the block events + // is just some list. So if it unloads, I guess it's just lost. + if (into != null) { + into.blockEvents.add(blockEventData); + } + } + + final Long2ReferenceOpenHashMap> levelTicksBlockRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f); + final Long2ReferenceOpenHashMap> levelTicksFluidRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f); + + for (final Iterator> iterator = regionToData.long2ReferenceEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ReferenceMap.Entry entry = iterator.next(); + final long key = entry.getLongKey(); + final RegionizedWorldData worldData = entry.getValue(); + + levelTicksBlockRegionData.put(key, worldData.blockLevelTicks); + levelTicksFluidRegionData.put(key, worldData.fluidLevelTicks); + } + + from.blockLevelTicks.split(chunkToRegionShift, levelTicksBlockRegionData); + from.fluidLevelTicks.split(chunkToRegionShift, levelTicksFluidRegionData); + + // tile entity ticking + for (final TickingBlockEntity tileEntity : from.pendingBlockEntityTickers) { + final BlockPos pos = tileEntity.getPos(); + final int chunkX = pos.getX() >> 4; + final int chunkZ = pos.getZ() >> 4; + + final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); + if (into != null) { + into.pendingBlockEntityTickers.add(tileEntity); + } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets + // marked as removed. So if there is no section, it's probably removed! + } + for (final TickingBlockEntity tileEntity : from.blockEntityTickers) { + final BlockPos pos = tileEntity.getPos(); + final int chunkX = pos.getX() >> 4; + final int chunkZ = pos.getZ() >> 4; + + final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); + if (into != null) { + into.blockEntityTickers.add(tileEntity); + } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets + // marked as removed. So if there is no section, it's probably removed! + } + // time + for (final RegionizedWorldData regionizedWorldData : dataSet) { + regionizedWorldData.redstoneTime = from.redstoneTime; + } + // ticking chunks + for (final Iterator iterator = from.entityTickingChunks.unsafeIterator(); iterator.hasNext();) { + final LevelChunk levelChunk = iterator.next(); + final ChunkPos pos = levelChunk.getPos(); + + // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded + regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) + .entityTickingChunks.add(levelChunk); + } + // redstone torches + if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) { + for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) { + final BlockPos pos = info.pos; + + final RegionizedWorldData worldData = regionToData.get(CoordinateUtils.getChunkKey((pos.getX() >> 4) >> chunkToRegionShift, (pos.getZ() >> 4) >> chunkToRegionShift)); + if (worldData != null) { + if (worldData.redstoneUpdateInfos == null) { + worldData.redstoneUpdateInfos = new ArrayDeque<>(); + } + worldData.redstoneUpdateInfos.add(info); + } // else: chunk unloaded + } + } + // light chunks being worked on + for (final Iterator iterator = from.chunksBeingWorkedOn.long2IntEntrySet().fastIterator(); iterator.hasNext();) { + final Long2IntOpenHashMap.Entry entry = iterator.next(); + final long pos = entry.getLongKey(); + final int chunkX = CoordinateUtils.getChunkX(pos); + final int chunkZ = CoordinateUtils.getChunkZ(pos); + final int value = entry.getIntValue(); + + // should never be null, as it is a reference counter for ticket + regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)).chunksBeingWorkedOn.put(pos, value); + } + // mob spawning + for (final RegionizedWorldData regionizedWorldData : dataSet) { + regionizedWorldData.catSpawnerNextTick = from.catSpawnerNextTick; + regionizedWorldData.patrolSpawnerNextTick = from.patrolSpawnerNextTick; + regionizedWorldData.phantomSpawnerNextTick = from.phantomSpawnerNextTick; + regionizedWorldData.wanderingTraderTickDelay = from.wanderingTraderTickDelay; + regionizedWorldData.wanderingTraderSpawnChance = from.wanderingTraderSpawnChance; + regionizedWorldData.wanderingTraderSpawnDelay = from.wanderingTraderSpawnDelay; + regionizedWorldData.villageSiegeState = new VillageSiegeState(); // just re set it, as the spawn pos will be invalid + } + } + }; + + public final ServerLevel world; + + private RegionizedServer.WorldLevelData tickData; + + // connections + public final List connections = new ArrayList<>(); + + // misc. fields + private boolean isHandlingTick; + + public void setHandlingTick(final boolean to) { + this.isHandlingTick = to; + } + + public boolean isHandlingTick() { + return this.isHandlingTick; + } + + // entities + private final List localPlayers = new ArrayList<>(); + private final ReferenceList allEntities = new ReferenceList<>(); + private final IteratorSafeOrderedReferenceSet entityTickList = new IteratorSafeOrderedReferenceSet<>(); + private final IteratorSafeOrderedReferenceSet navigatingMobs = new IteratorSafeOrderedReferenceSet<>(); + + // block ticking + private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>(); + private final LevelTicks blockLevelTicks; + private final LevelTicks fluidLevelTicks; + + // tile entity ticking + private final List pendingBlockEntityTickers = new ArrayList<>(); + private final List blockEntityTickers = new ArrayList<>(); + private boolean tickingBlockEntities; + + // time + private long redstoneTime = 1L; + + public long getRedstoneGameTime() { + return this.redstoneTime; + } + + public void setRedstoneGameTime(final long to) { + this.redstoneTime = to; + } + + // ticking chunks + private final IteratorSafeOrderedReferenceSet entityTickingChunks = new IteratorSafeOrderedReferenceSet<>(); + private final IteratorSafeOrderedReferenceSet chunks = new IteratorSafeOrderedReferenceSet<>(); + + // Paper/CB api hook misc + // don't bother to merge/split these, no point + // From ServerLevel + public boolean hasPhysicsEvent = true; // Paper + public boolean hasEntityMoveEvent = false; // Paper + // Paper start - Optimize Hoppers + public boolean skipPullModeEventFire = false; + public boolean skipPushModeEventFire = false; + public boolean skipHopperEvents = false; + // Paper end - Optimize Hoppers + public long lastMidTickExecuteFailure; + public long lastMidTickExecute; + // From Level + public boolean populating; + public final NeighborUpdater neighborUpdater; + public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710 + public boolean captureBlockStates = false; + public boolean captureTreeGeneration = false; + public final Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper + public final Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper + public List captureDrops; + // Paper start + public int wakeupInactiveRemainingAnimals; + public int wakeupInactiveRemainingFlying; + public int wakeupInactiveRemainingMonsters; + public int wakeupInactiveRemainingVillagers; + // Paper end + public final TempCollisionList tempCollisionList = new TempCollisionList<>(); + public final TempCollisionList tempEntitiesList = new TempCollisionList<>(); + public int currentPrimedTnt = 0; // Spigot + @Nullable + @VisibleForDebug + public NaturalSpawner.SpawnState lastSpawnState; + public boolean shouldSignal = true; + + // not transient + public java.util.ArrayDeque redstoneUpdateInfos; + public final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); + + public static final class TempCollisionList { + final UnsafeList list = new UnsafeList<>(64); + boolean inUse; + + public UnsafeList get() { + if (this.inUse) { + return new UnsafeList<>(16); + } + this.inUse = true; + return this.list; + } + + public void ret(List list) { + if (list != this.list) { + return; + } + + ((UnsafeList)list).setSize(0); + this.inUse = false; + } + + public void reset() { + this.list.completeReset(); + } + } + public void resetCollisionLists() { + this.tempCollisionList.reset(); + this.tempEntitiesList.reset(); + } + + // Mob spawning + private final PooledLinkedHashSets pooledHashSets = new PooledLinkedHashSets<>(); + public final PlayerAreaMap mobSpawnMap = new PlayerAreaMap(this.pooledHashSets); + public int catSpawnerNextTick = 0; + public int patrolSpawnerNextTick = 0; + public int phantomSpawnerNextTick = 0; + public int wanderingTraderTickDelay = Integer.MIN_VALUE; + public int wanderingTraderSpawnDelay; + public int wanderingTraderSpawnChance; + public VillageSiegeState villageSiegeState = new VillageSiegeState(); + + public static final class VillageSiegeState { + public boolean hasSetupSiege; + public VillageSiege.State siegeState = VillageSiege.State.SIEGE_DONE; + public int zombiesToSpawn; + public int nextSpawnTime; + public int spawnX; + public int spawnY; + public int spawnZ; + } + + public RegionizedWorldData(final ServerLevel world) { + this.world = world; + this.blockLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world.getProfilerSupplier(), world, true); + this.fluidLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world.getProfilerSupplier(), world, false); + this.neighborUpdater = new CollectingNeighborUpdater(world, world.neighbourUpdateMax); + + // tasks may be drained before the region ticks, so we must set up the tick data early just in case + this.updateTickData(); + } + + public void checkWorld(final Level against) { + if (this.world != against) { + throw new IllegalStateException("World mismatch: expected " + this.world.getWorld().getName() + " but got " + (against == null ? "null" : against.getWorld().getName())); + } + } + + public RegionizedServer.WorldLevelData getTickData() { + return this.tickData; + } + + public void updateTickData() { + this.tickData = this.world.tickData; + this.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper + this.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper + this.skipHopperEvents = this.world.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper + } + + // connections + public void tickConnections() { + final List connections = new ArrayList<>(this.connections); + Collections.shuffle(connections); + for (final Connection conn : connections) { + if (!conn.isConnected()) { + conn.handleDisconnection(); + this.connections.remove(conn); + // note: ALL connections HERE have a player + final ServerPlayer player = conn.getPlayer(); + // now that the connection is removed, we can allow this region to die + player.getLevel().chunkSource.removeTicketAtLevel( + ServerGamePacketListenerImpl.DISCONNECT_TICKET, player.connection.disconnectPos, + ChunkHolderManager.MAX_TICKET_LEVEL, + player.connection.disconnectTicketId + ); + continue; + } + if (!this.connections.contains(conn)) { + // removed by connection tick? + continue; + } + + try { + conn.tick(); + } catch (final Exception exception) { + if (conn.isMemoryConnection()) { + throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection")); + } + + LOGGER.warn("Failed to handle packet for {}", io.papermc.paper.configuration.GlobalConfiguration.get().logging.logPlayerIpAddresses ? String.valueOf(conn.getRemoteAddress()) : "", exception); // Paper + MutableComponent ichatmutablecomponent = Component.literal("Internal server error"); + + conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> { + conn.disconnect(ichatmutablecomponent); + })); + conn.setReadOnly(); + continue; + } + } + } + + // entities hooks + public int getEntityCount() { + return this.allEntities.size(); + } + + public int getPlayerCount() { + return this.localPlayers.size(); + } + + public Iterable getLocalEntities() { + return this.allEntities; + } + + public Entity[] getLocalEntitiesCopy() { + return Arrays.copyOf(this.allEntities.getRawData(), this.allEntities.size(), Entity[].class); + } + + public List getLocalPlayers() { + return this.localPlayers; + } + + public void addEntityTickingEntity(final Entity entity) { + if (!TickThread.isTickThreadFor(entity)) { + throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); + } + this.entityTickList.add(entity); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public boolean hasEntityTickingEntity(final Entity entity) { + return this.entityTickList.contains(entity); + } + + public void removeEntityTickingEntity(final Entity entity) { + if (!TickThread.isTickThreadFor(entity)) { + throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); + } + this.entityTickList.remove(entity); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public void forEachTickingEntity(final Consumer action) { + final IteratorSafeOrderedReferenceSet.Iterator iterator = this.entityTickList.iterator(); + try { + while (iterator.hasNext()) { + action.accept(iterator.next()); + } + } finally { + iterator.finishedIterating(); + } + } + + public void addEntity(final Entity entity) { + if (!TickThread.isTickThreadFor(this.world, entity.chunkPosition())) { + throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); + } + if (this.allEntities.add(entity)) { + if (entity instanceof ServerPlayer player) { + this.localPlayers.add(player); + } + TickRegions.RegionStats.updateCurrentRegion(); + } + } + + public boolean hasEntity(final Entity entity) { + return this.allEntities.contains(entity); + } + + public void removeEntity(final Entity entity) { + if (!TickThread.isTickThreadFor(entity)) { + throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); + } + if (this.allEntities.remove(entity)) { + if (entity instanceof ServerPlayer player) { + this.localPlayers.remove(player); + } + TickRegions.RegionStats.updateCurrentRegion(); + } + } + + public void addNavigatingMob(final Mob mob) { + if (!TickThread.isTickThreadFor(mob)) { + throw new IllegalArgumentException("Entity " + mob + " is not under this region's control"); + } + this.navigatingMobs.add(mob); + } + + public void removeNavigatingMob(final Mob mob) { + if (!TickThread.isTickThreadFor(mob)) { + throw new IllegalArgumentException("Entity " + mob + " is not under this region's control"); + } + this.navigatingMobs.remove(mob); + } + + public Iterator getNavigatingMobs() { + return this.navigatingMobs.unsafeIterator(); + } + + // block ticking hooks + // Since block event data does not require chunk holders to be created for the chunk they reside in, + // it's not actually guaranteed that when merging / splitting data that we actually own the data... + // Note that we can only ever not own the event data when the chunk unloads, and so I've decided to + // make the code easier by simply discarding it in such an event + public void pushBlockEvent(final BlockEventData blockEventData) { + TickThread.ensureTickThread(this.world, blockEventData.pos(), "Cannot queue block even data async"); + this.blockEvents.add(blockEventData); + } + + public void pushBlockEvents(final Collection blockEvents) { + for (final BlockEventData blockEventData : blockEvents) { + this.pushBlockEvent(blockEventData); + } + } + + public void removeIfBlockEvents(final Predicate predicate) { + for (final Iterator iterator = this.blockEvents.iterator(); iterator.hasNext();) { + final BlockEventData blockEventData = iterator.next(); + if (predicate.test(blockEventData)) { + iterator.remove(); + } + } + } + + public BlockEventData removeFirstBlockEvent() { + BlockEventData ret; + while (!this.blockEvents.isEmpty()) { + ret = this.blockEvents.removeFirst(); + if (TickThread.isTickThreadFor(this.world, ret.pos())) { + return ret; + } // else: chunk must have been unloaded + } + + return null; + } + + public LevelTicks getBlockLevelTicks() { + return this.blockLevelTicks; + } + + public LevelTicks getFluidLevelTicks() { + return this.fluidLevelTicks; + } + + // tile entity ticking + public void addBlockEntityTicker(final TickingBlockEntity ticker) { + TickThread.ensureTickThread(this.world, ticker.getPos(), "Tile entity must be owned by current region"); + + (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker); + } + + public void seTtickingBlockEntities(final boolean to) { + this.tickingBlockEntities = true; + } + + public List getBlockEntityTickers() { + return this.blockEntityTickers; + } + + public void pushPendingTickingBlockEntities() { + if (!this.pendingBlockEntityTickers.isEmpty()) { + this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); + this.pendingBlockEntityTickers.clear(); + } + } + + // ticking chunks + public void addEntityTickingChunk(final LevelChunk levelChunk) { + this.entityTickingChunks.add(levelChunk); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public void removeEntityTickingChunk(final LevelChunk levelChunk) { + this.entityTickingChunks.remove(levelChunk); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public IteratorSafeOrderedReferenceSet getEntityTickingChunks() { + return this.entityTickingChunks; + } + + public void addChunk(final LevelChunk levelChunk) { + this.chunks.add(levelChunk); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public void removeChunk(final LevelChunk levelChunk) { + this.chunks.remove(levelChunk); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public IteratorSafeOrderedReferenceSet getChunks() { + return this.chunks; + } + + public int getEntityTickingChunkCount() { + return this.entityTickingChunks.size(); + } + + public int getChunkCount() { + return this.chunks.size(); + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/Schedule.java b/src/main/java/io/papermc/paper/threadedregions/Schedule.java new file mode 100644 index 0000000000000000000000000000000000000000..112d24a93bddf3d81c9176c05340c94ecd1a40a3 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/Schedule.java @@ -0,0 +1,91 @@ +package io.papermc.paper.threadedregions; + +/** + * A Schedule is an object that can be used to maintain a periodic schedule for an event of interest. + */ +public final class Schedule { + + private long lastPeriod; + + /** + * Initialises a schedule with the provided period. + * @param firstPeriod The last time an event of interest occurred. + * @see #setLastPeriod(long) + */ + public Schedule(final long firstPeriod) { + this.lastPeriod = firstPeriod; + } + + /** + * Updates the last period to the specified value. This call sets the last "time" the event + * of interest took place at. Thus, the value returned by {@link #getDeadline(long)} is + * the provided time plus the period length provided to {@code getDeadline}. + * @param value The value to set the last period to. + */ + public void setLastPeriod(final long value) { + this.lastPeriod = value; + } + + /** + * Returns the last time the event of interest should have taken place. + */ + public long getLastPeriod() { + return this.lastPeriod; + } + + /** + * Returns the number of times the event of interest should have taken place between the last + * period and the provided time given the period between each event. + * @param periodLength The length of the period between events in ns. + * @param time The provided time. + */ + public int getPeriodsAhead(final long periodLength, final long time) { + final long difference = time - this.lastPeriod; + final int ret = (int)(Math.abs(difference) / periodLength); + return difference >= 0 ? ret : -ret; + } + + /** + * Returns the next starting deadline for the event of interest to take place, + * given the provided period length. + * @param periodLength The provided period length. + */ + public long getDeadline(final long periodLength) { + return this.lastPeriod + periodLength; + } + + /** + * Adjusts the last period so that the next starting deadline returned is the next period specified, + * given the provided period length. + * @param nextPeriod The specified next starting deadline. + * @param periodLength The specified period length. + */ + public void setNextPeriod(final long nextPeriod, final long periodLength) { + this.lastPeriod = nextPeriod - periodLength; + } + + /** + * Increases the last period by the specified number of periods and period length. + * The specified number of periods may be < 0, in which case the last period + * will decrease. + * @param periods The specified number of periods. + * @param periodLength The specified period length. + */ + public void advanceBy(final int periods, final long periodLength) { + this.lastPeriod += (long)periods * periodLength; + } + + /** + * Sets the last period so that it is the specified number of periods ahead + * given the specified time and period length. + * @param periodsToBeAhead Specified number of periods to be ahead by. + * @param periodLength The specified period length. + * @param time The specified time. + */ + public void setPeriodsAhead(final int periodsToBeAhead, final long periodLength, final long time) { + final int periodsAhead = this.getPeriodsAhead(periodLength, time); + final int periodsToAdd = periodsToBeAhead - periodsAhead; + + this.lastPeriod -= (long)periodsToAdd * periodLength; + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java b/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..84b4ff07735fb84e28ee8966ffdedb1bb3d07dff --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java @@ -0,0 +1,60 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.completable.Completable; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.bukkit.Location; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.event.player.PlayerTeleportEvent; +import java.util.function.Consumer; + +public final class TeleportUtils { + + public static void teleport(final Entity from, final boolean useFromRootVehicle, final Entity to, final Float yaw, final Float pitch, + final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer onComplete) { + // retrieve coordinates + final Completable positionCompletable = new Completable<>(); + + positionCompletable.addWaiter( + (final Location loc, final Throwable thr) -> { + if (loc == null) { + onComplete.accept(null); + return; + } + final boolean scheduled = from.getBukkitEntity().taskScheduler.schedule( + (final Entity realFrom) -> { + final Vec3 pos = new Vec3( + loc.getX(), loc.getY(), loc.getZ() + ); + (useFromRootVehicle ? realFrom.getRootVehicle() : realFrom).teleportAsync( + ((CraftWorld)loc.getWorld()).getHandle(), pos, null, null, null, + cause, teleportFlags, onComplete + ); + }, + (final Entity retired) -> { + onComplete.accept(null); + }, + 1L + ); + if (!scheduled) { + onComplete.accept(null); + } + } + ); + + final boolean scheduled = to.getBukkitEntity().taskScheduler.schedule( + (final Entity target) -> { + positionCompletable.complete(target.getBukkitEntity().getLocation()); + }, + (final Entity retired) -> { + onComplete.accept(null); + }, + 1L + ); + if (!scheduled) { + onComplete.accept(null); + } + } + + private TeleportUtils() {} +} diff --git a/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java new file mode 100644 index 0000000000000000000000000000000000000000..5170b43743ea27a5c2aaee37d76f4e7e730fd808 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java @@ -0,0 +1,1309 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import com.destroystokyo.paper.util.SneakyThrow; +import com.mojang.logging.LogUtils; +import io.papermc.paper.util.CoordinateUtils; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongComparator; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import org.slf4j.Logger; +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.StampedLock; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; + +public final class ThreadedRegionizer, S extends ThreadedRegionizer.ThreadedRegionSectionData> { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public final int regionSectionChunkSize; + public final int sectionChunkShift; + public final int minSectionRecalcCount; + public final int emptySectionCreateRadius; + public final int regionSectionMergeRadius; + public final double maxDeadRegionPercent; + public final ServerLevel world; + + private final SWMRLong2ObjectHashTable> sections = new SWMRLong2ObjectHashTable<>(); + private final SWMRLong2ObjectHashTable> regionsById = new SWMRLong2ObjectHashTable<>(); + private final RegionCallbacks callbacks; + private final StampedLock regionLock = new StampedLock(); + private Thread writeLockOwner; + + /* + static final record Operation(String type, int chunkX, int chunkZ) {} + private final MultiThreadedQueue ops = new MultiThreadedQueue<>(); + */ + + /* + * See REGION_LOGIC.md for complete details on what this class is doing + */ + + public ThreadedRegionizer(final int minSectionRecalcCount, final double maxDeadRegionPercent, + final int emptySectionCreateRadius, final int regionSectionMergeRadius, + final int regionSectionChunkShift, final ServerLevel world, + final RegionCallbacks callbacks) { + if (emptySectionCreateRadius <= 0) { + throw new IllegalStateException("Region section create radius must be > 0"); + } + if (regionSectionMergeRadius <= 0) { + throw new IllegalStateException("Region section merge radius must be > 0"); + } + this.regionSectionChunkSize = 1 << regionSectionChunkShift; + this.sectionChunkShift = regionSectionChunkShift; + this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount); + this.maxDeadRegionPercent = maxDeadRegionPercent; + this.emptySectionCreateRadius = emptySectionCreateRadius; + this.regionSectionMergeRadius = regionSectionMergeRadius; + this.world = world; + this.callbacks = callbacks; + //this.loadTestData(); + } + + /* + private static String substr(String val, String prefix, int from) { + int idx = val.indexOf(prefix, from) + prefix.length(); + int idx2 = val.indexOf(',', idx); + if (idx2 == -1) { + idx2 = val.indexOf(']', idx); + } + return val.substring(idx, idx2); + } + + private void loadTestData() { + if (true) { + return; + } + try { + final JsonArray arr = JsonParser.parseReader(new FileReader("test.json")).getAsJsonArray(); + + List ops = new ArrayList<>(); + + for (JsonElement elem : arr) { + JsonObject obj = elem.getAsJsonObject(); + String val = obj.get("value").getAsString(); + + String type = substr(val, "type=", 0); + String x = substr(val, "chunkX=", 0); + String z = substr(val, "chunkZ=", 0); + + ops.add(new Operation(type, Integer.parseInt(x), Integer.parseInt(z))); + } + + for (Operation op : ops) { + switch (op.type) { + case "add": { + this.addChunk(op.chunkX, op.chunkZ); + break; + } + case "remove": { + this.removeChunk(op.chunkX, op.chunkZ); + break; + } + case "mark_ticking": { + this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.tryMarkTicking(); + break; + } + case "rel_region": { + if (this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.state == ThreadedRegion.STATE_TICKING) { + this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.markNotTicking(); + } + break; + } + } + } + + } catch (final Exception ex) { + throw new IllegalStateException(ex); + } + } + */ + + public void acquireReadLock() { + this.regionLock.readLock(); + } + + public void releaseReadLock() { + this.regionLock.tryUnlockRead(); + } + + private void acquireWriteLock() { + final Thread currentThread = Thread.currentThread(); + if (this.writeLockOwner == currentThread) { + throw new IllegalStateException("Cannot recursively operate in the regioniser"); + } + this.regionLock.writeLock(); + this.writeLockOwner = currentThread; + } + + private void releaseWriteLock() { + this.writeLockOwner = null; + this.regionLock.tryUnlockWrite(); + } + + private void onRegionCreate(final ThreadedRegion region) { + final ThreadedRegion conflict; + if ((conflict = this.regionsById.putIfAbsent(region.id, region)) != null) { + throw new IllegalStateException("Region " + region + " is already mapped to " + conflict); + } + } + + private void onRegionDestroy(final ThreadedRegion region) { + final ThreadedRegion removed = this.regionsById.remove(region.id); + if (removed != region) { + throw new IllegalStateException("Expected to remove " + region + ", but removed " + removed); + } + } + + public int getSectionCoordinate(final int chunkCoordinate) { + return chunkCoordinate >> this.sectionChunkShift; + } + + public long getSectionKey(final BlockPos pos) { + return CoordinateUtils.getChunkKey((pos.getX() >> 4) >> this.sectionChunkShift, (pos.getZ() >> 4) >> this.sectionChunkShift); + } + + public long getSectionKey(final ChunkPos pos) { + return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift); + } + + public long getSectionKey(final Entity entity) { + final ChunkPos pos = entity.chunkPosition(); + return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift); + } + + public void computeForAllRegions(final Consumer> consumer) { + this.regionLock.readLock(); + try { + this.regionsById.forEachValue(consumer); + } finally { + this.regionLock.tryUnlockRead(); + } + } + + public void computeForAllRegionsUnsynchronised(final Consumer> consumer) { + this.regionsById.forEachValue(consumer); + } + + public ThreadedRegion getRegionAtUnsynchronised(final int chunkX, final int chunkZ) { + final int sectionX = chunkX >> this.sectionChunkShift; + final int sectionZ = chunkZ >> this.sectionChunkShift; + final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); + + final ThreadedRegionSection section = this.sections.get(sectionKey); + + return section == null ? null : section.getRegion(); + } + + public ThreadedRegion getRegionAtSynchronised(final int chunkX, final int chunkZ) { + final int sectionX = chunkX >> this.sectionChunkShift; + final int sectionZ = chunkZ >> this.sectionChunkShift; + final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); + + // try an optimistic read + { + final long readAttempt = this.regionLock.tryOptimisticRead(); + final ThreadedRegionSection optimisticSection = this.sections.get(sectionKey); + final ThreadedRegion optimisticRet = + optimisticSection == null ? null : optimisticSection.getRegionPlain(); + if (this.regionLock.validate(readAttempt)) { + return optimisticRet; + } + } + + // failed, fall back to acquiring the lock + this.regionLock.readLock(); + try { + final ThreadedRegionSection section = this.sections.get(sectionKey); + + return section == null ? null : section.getRegionPlain(); + } finally { + this.regionLock.tryUnlockRead(); + } + } + + /** + * Adds a chunk to the regioniser. Note that it is illegal to add a chunk unless + * addChunk has not been called for it or removeChunk has been previously called. + * + *

    + * Note that it is illegal to additionally call addChunk or removeChunk for the same + * region section in parallel. + *

    + */ + public void addChunk(final int chunkX, final int chunkZ) { + final int sectionX = chunkX >> this.sectionChunkShift; + final int sectionZ = chunkZ >> this.sectionChunkShift; + final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); + + // Given that for each section, no addChunk/removeChunk can occur in parallel, + // we can avoid the lock IF the section exists AND it has a non-zero chunk count. + { + final ThreadedRegionSection existing = this.sections.get(sectionKey); + if (existing != null && !existing.isEmpty()) { + existing.addChunk(chunkX, chunkZ); + return; + } // else: just acquire the write lock + } + + this.acquireWriteLock(); + try { + ThreadedRegionSection section = this.sections.get(sectionKey); + + List> newSections = new ArrayList<>(); + + if (section == null) { + // no section at all + section = new ThreadedRegionSection<>(sectionX, sectionZ, this, chunkX, chunkZ); + this.sections.put(sectionKey, section); + newSections.add(section); + } else { + section.addChunk(chunkX, chunkZ); + } + // due to the fast check from above, we know the section is empty whether we needed to create it or not + + // enforce the adjacency invariant by creating / updating neighbour sections + final int createRadius = this.emptySectionCreateRadius; + final int searchRadius = createRadius + this.regionSectionMergeRadius; + ReferenceOpenHashSet> nearbyRegions = null; + for (int dx = -searchRadius; dx <= searchRadius; ++dx) { + for (int dz = -searchRadius; dz <= searchRadius; ++dz) { + if ((dx | dz) == 0) { + continue; + } + final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); + final boolean inCreateRange = squareDistance <= createRadius; + + final int neighbourX = dx + sectionX; + final int neighbourZ = dz + sectionZ; + final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); + + ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey); + + if (neighbourSection != null) { + if (nearbyRegions == null) { + nearbyRegions = new ReferenceOpenHashSet<>(((searchRadius * 2 + 1) * (searchRadius * 2 + 1)) >> 1); + } + nearbyRegions.add(neighbourSection.getRegionPlain()); + } + + if (!inCreateRange) { + continue; + } + + // we need to ensure the section exists + if (neighbourSection != null) { + // nothing else to do + neighbourSection.incrementNonEmptyNeighbours(); + continue; + } + neighbourSection = new ThreadedRegionSection<>(neighbourX, neighbourZ, this, 1); + if (null != this.sections.put(neighbourKey, neighbourSection)) { + throw new IllegalStateException("Failed to insert new section"); + } + newSections.add(neighbourSection); + } + } + + final ThreadedRegion regionOfInterest; + final boolean regionOfInterestAlive; + if (nearbyRegions == null) { + // we can simply create a new region, don't have neighbours to worry about merging into + regionOfInterest = new ThreadedRegion<>(this); + regionOfInterestAlive = true; + + for (int i = 0, len = newSections.size(); i < len; ++i) { + regionOfInterest.addSection(newSections.get(i)); + } + + // only call create callback after adding sections + regionOfInterest.onCreate(); + } else { + // need to merge the regions + + ThreadedRegion firstUnlockedRegion = null; + + for (final ThreadedRegion region : nearbyRegions) { + if (region.isTicking()) { + continue; + } + firstUnlockedRegion = region; + break; + } + + if (firstUnlockedRegion != null) { + regionOfInterest = firstUnlockedRegion; + } else { + regionOfInterest = new ThreadedRegion<>(this); + } + + for (int i = 0, len = newSections.size(); i < len; ++i) { + regionOfInterest.addSection(newSections.get(i)); + } + + // only call create callback after adding sections + if (firstUnlockedRegion == null) { + regionOfInterest.onCreate(); + } + + if (firstUnlockedRegion != null && nearbyRegions.size() == 1) { + // nothing to do further, no need to merge anything + return; + } + + // we need to now tell all the other regions to merge into the region we just created, + // and to merge all the ones we can immediately + + boolean delayedTrueMerge = false; + + for (final ThreadedRegion region : nearbyRegions) { + if (region == regionOfInterest) { + continue; + } + // need the relaxed check, as the region may already be + // a merge target + if (!region.tryKill()) { + regionOfInterest.mergeIntoLater(region); + delayedTrueMerge = true; + } else { + region.mergeInto(regionOfInterest); + } + } + + if (delayedTrueMerge && firstUnlockedRegion != null) { + // we need to retire this region, as it can no longer tick + if (regionOfInterest.state == ThreadedRegion.STATE_READY) { + regionOfInterest.state = ThreadedRegion.STATE_TRANSIENT; + this.callbacks.onRegionInactive(regionOfInterest); + } + } + + // need to set alive if we created it and we didn't delay a merge + regionOfInterestAlive = firstUnlockedRegion == null && !delayedTrueMerge && regionOfInterest.mergeIntoLater.isEmpty() && regionOfInterest.expectingMergeFrom.isEmpty(); + } + + if (regionOfInterestAlive) { + regionOfInterest.state = ThreadedRegion.STATE_READY; + if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) { + throw new IllegalStateException("Should not happen on region " + this); + } + this.callbacks.onRegionActive(regionOfInterest); + } + } catch (final Throwable throwable) { + LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable); + SneakyThrow.sneaky(throwable); + return; // unreachable + } finally { + this.releaseWriteLock(); + } + } + + public void removeChunk(final int chunkX, final int chunkZ) { + final int sectionX = chunkX >> this.sectionChunkShift; + final int sectionZ = chunkZ >> this.sectionChunkShift; + final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); + + // Given that for each section, no addChunk/removeChunk can occur in parallel, + // we can avoid the lock IF the section exists AND it has a chunk count > 1 + final ThreadedRegionSection section = this.sections.get(sectionKey); + if (section == null) { + throw new IllegalStateException("Chunk (" + chunkX + "," + chunkZ + ") has no section"); + } + if (!section.hasOnlyOneChunk()) { + // chunk will not go empty, so we don't need to acquire the lock + section.removeChunk(chunkX, chunkZ); + return; + } + + this.acquireWriteLock(); + try { + section.removeChunk(chunkX, chunkZ); + + final int searchRadius = this.emptySectionCreateRadius; + for (int dx = -searchRadius; dx <= searchRadius; ++dx) { + for (int dz = -searchRadius; dz <= searchRadius; ++dz) { + if ((dx | dz) == 0) { + continue; + } + + final int neighbourX = dx + sectionX; + final int neighbourZ = dz + sectionZ; + final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); + + final ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey); + + // should be non-null here always + neighbourSection.decrementNonEmptyNeighbours(); + } + } + } catch (final Throwable throwable) { + LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable); + SneakyThrow.sneaky(throwable); + return; // unreachable + } finally { + this.releaseWriteLock(); + } + } + + // must hold regionLock + private void onRegionRelease(final ThreadedRegion region) { + if (!region.mergeIntoLater.isEmpty()) { + throw new IllegalStateException("Region " + region + " should not have any regions to merge into!"); + } + + final boolean hasExpectingMerges = !region.expectingMergeFrom.isEmpty(); + + // is this region supposed to merge into any other region? + if (hasExpectingMerges) { + // merge the regions into this one + final ReferenceOpenHashSet> expectingMergeFrom = region.expectingMergeFrom.clone(); + for (final ThreadedRegion mergeFrom : expectingMergeFrom) { + if (!mergeFrom.tryKill()) { + throw new IllegalStateException("Merge from region " + mergeFrom + " should be killable! Trying to merge into " + region); + } + mergeFrom.mergeInto(region); + } + + if (!region.expectingMergeFrom.isEmpty()) { + throw new IllegalStateException("Region " + region + " should no longer have merge requests after mering from " + expectingMergeFrom); + } + + if (!region.mergeIntoLater.isEmpty()) { + // There is another nearby ticking region that we need to merge into + region.state = ThreadedRegion.STATE_TRANSIENT; + this.callbacks.onRegionInactive(region); + // return to avoid removing dead sections or splitting, these actions will be performed + // by the region we merge into + return; + } + } + + // now check whether we need to recalculate regions + final boolean removeDeadSections = hasExpectingMerges || region.hasNoAliveSections() + || (region.sectionByKey.size() >= this.minSectionRecalcCount && region.getDeadSectionPercent() >= this.maxDeadRegionPercent); + final boolean removedDeadSections = removeDeadSections && !region.deadSections.isEmpty(); + if (removeDeadSections) { + // kill dead sections + for (final ThreadedRegionSection deadSection : region.deadSections) { + final long key = CoordinateUtils.getChunkKey(deadSection.sectionX, deadSection.sectionZ); + + if (!deadSection.isEmpty()) { + throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!"); + } + if (deadSection.hasNonEmptyNeighbours()) { + throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has non-empty neighbours!"); + } + if (!region.sectionByKey.remove(key, deadSection)) { + throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection); + } + if (this.sections.remove(key) != deadSection) { + throw new IllegalStateException("Cannot remove dead section '" + + deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " + this.sections.get(key)); + } + } + region.deadSections.clear(); + } + + // if we removed dead sections, we should check if the region can be split into smaller ones + // otherwise, the region remains alive + if (!removedDeadSections) { + // didn't remove dead sections, don't check for split + region.state = ThreadedRegion.STATE_READY; + if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) { + throw new IllegalStateException("Illegal state " + region); + } + return; + } + + // first, we need to build copy of coordinate->section map of all sections in recalculate + final Long2ReferenceOpenHashMap> recalculateSections = region.sectionByKey.clone(); + + if (recalculateSections.isEmpty()) { + // looks like the region's sections were all dead, and now there is no region at all + region.state = ThreadedRegion.STATE_DEAD; + region.onRemove(true); + return; + } + + // merge radius is max, since recalculateSections includes the dead or empty sections + final int mergeRadius = Math.max(this.regionSectionMergeRadius, this.emptySectionCreateRadius); + + final List>> newRegions = new ArrayList<>(); + while (!recalculateSections.isEmpty()) { + // select any section, then BFS around it to find all of its neighbours to form a region + // once no more neighbours are found, the region is complete + final List> currRegion = new ArrayList<>(); + final Iterator> firstIterator = recalculateSections.values().iterator(); + + currRegion.add(firstIterator.next()); + firstIterator.remove(); + search_loop: + for (int idx = 0; idx < currRegion.size(); ++idx) { + final ThreadedRegionSection curr = currRegion.get(idx); + final int centerX = curr.sectionX; + final int centerZ = curr.sectionZ; + + // find neighbours in radius + for (int dz = -mergeRadius; dz <= mergeRadius; ++dz) { + for (int dx = -mergeRadius; dx <= mergeRadius; ++dx) { + if ((dx | dz) == 0) { + continue; + } + + final ThreadedRegionSection section = recalculateSections.remove(CoordinateUtils.getChunkKey(dx + centerX, dz + centerZ)); + if (section == null) { + continue; + } + + currRegion.add(section); + + if (recalculateSections.isEmpty()) { + // no point in searching further + break search_loop; + } + } + } + } + + newRegions.add(currRegion); + } + + // now we have split the regions into separate parts, we can split recalculate + + if (newRegions.size() == 1) { + // no need to split anything, we're done here + region.state = ThreadedRegion.STATE_READY; + if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) { + throw new IllegalStateException("Illegal state " + region); + } + return; + } + + // need to split the region, so we need to kill the old one first + region.state = ThreadedRegion.STATE_DEAD; + region.onRemove(true); + + // create new regions + final Long2ReferenceOpenHashMap> newRegionsMap = new Long2ReferenceOpenHashMap<>(); + final ReferenceOpenHashSet> newRegionsSet = new ReferenceOpenHashSet<>(); + + for (final List> sections : newRegions) { + final ThreadedRegion newRegion = new ThreadedRegion<>(this); + newRegionsSet.add(newRegion); + + for (final ThreadedRegionSection section : sections) { + section.setRegionRelease(null); + newRegion.addSection(section); + final ThreadedRegion curr = newRegionsMap.putIfAbsent(section.sectionKey, newRegion); + if (curr != null) { + throw new IllegalStateException("Expected no region at " + section + ", but got " + curr + ", should have put " + newRegion); + } + } + } + + region.split(newRegionsMap, newRegionsSet); + + // only after invoking data callbacks + + for (final ThreadedRegion newRegion : newRegionsSet) { + newRegion.state = ThreadedRegion.STATE_READY; + if (!newRegion.expectingMergeFrom.isEmpty() || !newRegion.mergeIntoLater.isEmpty()) { + throw new IllegalStateException("Illegal state " + newRegion); + } + newRegion.onCreate(); + this.callbacks.onRegionActive(newRegion); + } + } + + public static final class ThreadedRegion, S extends ThreadedRegionSectionData> { + + private static final AtomicLong REGION_ID_GENERATOR = new AtomicLong(); + + private static final int STATE_TRANSIENT = 0; + private static final int STATE_READY = 1; + private static final int STATE_TICKING = 2; + private static final int STATE_DEAD = 3; + + public final long id; + + private int state; + + private final Long2ReferenceOpenHashMap> sectionByKey = new Long2ReferenceOpenHashMap<>(); + private final ReferenceOpenHashSet> deadSections = new ReferenceOpenHashSet<>(); + + public final ThreadedRegionizer regioniser; + + private final R data; + + private final ReferenceOpenHashSet> mergeIntoLater = new ReferenceOpenHashSet<>(); + private final ReferenceOpenHashSet> expectingMergeFrom = new ReferenceOpenHashSet<>(); + + public ThreadedRegion(final ThreadedRegionizer regioniser) { + this.regioniser = regioniser; + this.id = REGION_ID_GENERATOR.getAndIncrement(); + this.state = STATE_TRANSIENT; + this.data = regioniser.callbacks.createNewData(this); + } + + public LongArrayList getOwnedSections() { + final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread(); + if (lock) { + this.regioniser.regionLock.readLock(); + } + try { + final LongArrayList ret = new LongArrayList(this.sectionByKey.size()); + ret.addAll(this.sectionByKey.keySet()); + + return ret; + } finally { + if (lock) { + this.regioniser.regionLock.tryUnlockRead(); + } + } + } + + public LongArrayList getOwnedChunks() { + final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread(); + if (lock) { + this.regioniser.regionLock.readLock(); + } + try { + final LongArrayList ret = new LongArrayList(); + for (final ThreadedRegionSection section : this.sectionByKey.values()) { + ret.addAll(section.getChunks()); + } + + return ret; + } finally { + if (lock) { + this.regioniser.regionLock.tryUnlockRead(); + } + } + } + + public Long getCenterSection() { + final LongArrayList sections = this.getOwnedSections(); + + final LongComparator comparator = (final long k1, final long k2) -> { + final int x1 = CoordinateUtils.getChunkX(k1); + final int x2 = CoordinateUtils.getChunkX(k2); + + final int z1 = CoordinateUtils.getChunkZ(x1); + final int z2 = CoordinateUtils.getChunkZ(x2); + + final int zCompare = Integer.compare(z1, z2); + if (zCompare != 0) { + return zCompare; + } + + return Integer.compare(x1, x2); + }; + + // note: regions don't always have a chunk section at this point, because the region may have been killed + if (sections.isEmpty()) { + return null; + } + + sections.sort(comparator); + + return Long.valueOf(sections.getLong(sections.size() >> 1)); + } + + public ChunkPos getCenterChunk() { + final LongArrayList chunks = this.getOwnedChunks(); + + final LongComparator comparator = (final long k1, final long k2) -> { + final int x1 = CoordinateUtils.getChunkX(k1); + final int x2 = CoordinateUtils.getChunkX(k2); + + final int z1 = CoordinateUtils.getChunkZ(k1); + final int z2 = CoordinateUtils.getChunkZ(k2); + + final int zCompare = Integer.compare(z1, z2); + if (zCompare != 0) { + return zCompare; + } + + return Integer.compare(x1, x2); + }; + chunks.sort(comparator); + + // note: regions don't always have a chunk at this point, because the region may have been killed + if (chunks.isEmpty()) { + return null; + } + + final long middle = chunks.getLong(chunks.size() >> 1); + + return new ChunkPos(CoordinateUtils.getChunkX(middle), CoordinateUtils.getChunkZ(middle)); + } + + private void onCreate() { + this.regioniser.onRegionCreate(this); + this.regioniser.callbacks.onRegionCreate(this); + } + + private void onRemove(final boolean wasActive) { + if (wasActive) { + this.regioniser.callbacks.onRegionInactive(this); + } + this.regioniser.callbacks.onRegionDestroy(this); + this.regioniser.onRegionDestroy(this); + } + + private final boolean hasNoAliveSections() { + return this.deadSections.size() == this.sectionByKey.size(); + } + + private final double getDeadSectionPercent() { + return (double)this.deadSections.size() / (double)this.sectionByKey.size(); + } + + private void split(final Long2ReferenceOpenHashMap> into, final ReferenceOpenHashSet> regions) { + if (this.data != null) { + this.data.split(this.regioniser, into, regions); + } + } + + private void mergeInto(final ThreadedRegion mergeTarget) { + if (this == mergeTarget) { + throw new IllegalStateException("Cannot merge a region onto itself"); + } + if (!this.isDead()) { + throw new IllegalStateException("Source region is not dead! Source " + this + ", target " + mergeTarget); + } else if (mergeTarget.isDead()) { + throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget); + } + + for (final ThreadedRegionSection section : this.sectionByKey.values()) { + section.setRegionRelease(null); + mergeTarget.addSection(section); + } + for (final ThreadedRegionSection deadSection : this.deadSections) { + if (this.sectionByKey.get(deadSection.sectionKey) != deadSection) { + throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this); + } + if (!mergeTarget.deadSections.add(deadSection)) { + throw new IllegalStateException("Merge target contains dead section from source! Has " + deadSection + " from region " + this); + } + } + + // forward merge expectations + for (final ThreadedRegion region : this.expectingMergeFrom) { + if (!region.mergeIntoLater.remove(this)) { + throw new IllegalStateException("Region " + region + " was not supposed to merge into " + this + "?"); + } + if (region != mergeTarget) { + region.mergeIntoLater(mergeTarget); + } + } + + // forward merge into + for (final ThreadedRegion region : this.mergeIntoLater) { + if (!region.expectingMergeFrom.remove(this)) { + throw new IllegalStateException("Region " + this + " was not supposed to merge into " + region + "?"); + } + if (region != mergeTarget) { + mergeTarget.mergeIntoLater(region); + } + } + + // finally, merge data + if (this.data != null) { + this.data.mergeInto(mergeTarget); + } + } + + private void mergeIntoLater(final ThreadedRegion region) { + if (region.isDead()) { + throw new IllegalStateException("Trying to merge later into a dead region: " + region); + } + final boolean add1, add2; + if ((add1 = this.mergeIntoLater.add(region)) != (add2 = region.expectingMergeFrom.add(this))) { + throw new IllegalStateException("Inconsistent state between target merge " + region + " and this " + this + ": add1,add2:" + add1 + "," + add2); + } + } + + private boolean tryKill() { + switch (this.state) { + case STATE_TRANSIENT: { + this.state = STATE_DEAD; + this.onRemove(false); + return true; + } + case STATE_READY: { + this.state = STATE_DEAD; + this.onRemove(true); + return true; + } + case STATE_TICKING: { + return false; + } + case STATE_DEAD: { + throw new IllegalStateException("Already dead"); + } + default: { + throw new IllegalStateException("Unknown state: " + this.state); + } + } + } + + private boolean isDead() { + return this.state == STATE_DEAD; + } + + private boolean isTicking() { + return this.state == STATE_TICKING; + } + + private void removeDeadSection(final ThreadedRegionSection section) { + this.deadSections.remove(section); + } + + private void addDeadSection(final ThreadedRegionSection section) { + this.deadSections.add(section); + } + + private void addSection(final ThreadedRegionSection section) { + if (section.getRegionPlain() != null) { + throw new IllegalStateException("Section already has region"); + } + if (this.sectionByKey.putIfAbsent(section.sectionKey, section) != null) { + throw new IllegalStateException("Already have section " + section + ", mapped to " + this.sectionByKey.get(section.sectionKey)); + } + section.setRegionRelease(this); + } + + public R getData() { + return this.data; + } + + public boolean tryMarkTicking(final BooleanSupplier abort) { + this.regioniser.acquireWriteLock(); + try { + if (this.state != STATE_READY || abort.getAsBoolean()) { + return false; + } + + if (!this.mergeIntoLater.isEmpty() || !this.expectingMergeFrom.isEmpty()) { + throw new IllegalStateException("Region " + this + " should not be ready"); + } + + this.state = STATE_TICKING; + return true; + } finally { + this.regioniser.releaseWriteLock(); + } + } + + public boolean markNotTicking() { + this.regioniser.acquireWriteLock(); + try { + if (this.state != STATE_TICKING) { + throw new IllegalStateException("Attempting to release non-locked state"); + } + + this.regioniser.onRegionRelease(this); + + return this.state == STATE_READY; + } catch (final Throwable throwable) { + LOGGER.error("Failed to acquire region " + this, throwable); + SneakyThrow.sneaky(throwable); + return false; // unreachable + } finally { + this.regioniser.releaseWriteLock(); + } + } + + @Override + public String toString() { + final StringBuilder ret = new StringBuilder(128); + + ret.append("ThreadedRegion{"); + ret.append("state=").append(this.state).append(','); + // To avoid recursion in toString, maybe fix later? + //ret.append("mergeIntoLater=").append(this.mergeIntoLater).append(','); + //ret.append("expectingMergeFrom=").append(this.expectingMergeFrom).append(','); + + ret.append("sectionCount=").append(this.sectionByKey.size()).append(','); + ret.append("sections=["); + for (final Iterator> iterator = this.sectionByKey.values().iterator(); iterator.hasNext();) { + final ThreadedRegionSection section = iterator.next(); + + ret.append(section.toString()); + if (iterator.hasNext()) { + ret.append(','); + } + } + ret.append(']'); + + ret.append('}'); + return ret.toString(); + } + } + + public static final class ThreadedRegionSection, S extends ThreadedRegionSectionData> { + + public final int sectionX; + public final int sectionZ; + public final long sectionKey; + private final long[] chunksBitset; + private int chunkCount; + private int nonEmptyNeighbours; + + private ThreadedRegion region; + private static final VarHandle REGION_HANDLE = ConcurrentUtil.getVarHandle(ThreadedRegionSection.class, "region", ThreadedRegion.class); + + public final ThreadedRegionizer regioniser; + + private final int regionChunkShift; + private final int regionChunkMask; + + private final S data; + + private ThreadedRegion getRegionPlain() { + return (ThreadedRegion)REGION_HANDLE.get(this); + } + + private ThreadedRegion getRegionAcquire() { + return (ThreadedRegion)REGION_HANDLE.getAcquire(this); + } + + private void setRegionRelease(final ThreadedRegion value) { + REGION_HANDLE.setRelease(this, value); + } + + // creates an empty section with zero non-empty neighbours + private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser) { + this.sectionX = sectionX; + this.sectionZ = sectionZ; + this.sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); + this.chunksBitset = new long[Math.max(1, regioniser.regionSectionChunkSize * regioniser.regionSectionChunkSize / Long.SIZE)]; + this.regioniser = regioniser; + this.regionChunkShift = regioniser.sectionChunkShift; + this.regionChunkMask = regioniser.regionSectionChunkSize - 1; + this.data = regioniser.callbacks + .createNewSectionData(sectionX, sectionZ, this.regionChunkShift); + } + + // creates a section with an initial chunk with zero non-empty neighbours + private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser, + final int chunkXInit, final int chunkZInit) { + this(sectionX, sectionZ, regioniser); + + final int initIndex = this.getChunkIndex(chunkXInit, chunkZInit); + this.chunkCount = 1; + this.chunksBitset[initIndex >>> 6] = 1L << (initIndex & (Long.SIZE - 1)); // index / Long.SIZE + } + + // creates an empty section with the specified number of non-empty neighbours + private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser, + final int nonEmptyNeighbours) { + this(sectionX, sectionZ, regioniser); + + this.nonEmptyNeighbours = nonEmptyNeighbours; + } + + public LongArrayList getChunks() { + final LongArrayList ret = new LongArrayList(); + + if (this.chunkCount == 0) { + return ret; + } + + final int shift = this.regionChunkShift; + final int mask = this.regionChunkMask; + final int offsetX = this.sectionX << shift; + final int offsetZ = this.sectionZ << shift; + + final long[] bitset = this.chunksBitset; + for (int arrIdx = 0, arrLen = bitset.length; arrIdx < arrLen; ++arrIdx) { + long value = bitset[arrIdx]; + + for (int i = 0, bits = Long.bitCount(value); i < bits; ++i) { + final int valueIdx = Long.numberOfTrailingZeros(value); + value ^= io.papermc.paper.util.IntegerUtil.getTrailingBit(value); + + final int idx = valueIdx | (arrIdx << 6); + + final int localX = idx & mask; + final int localZ = (idx >>> shift) & mask; + + ret.add(CoordinateUtils.getChunkKey(localX | offsetX, localZ | offsetZ)); + } + } + + return ret; + } + + private boolean isEmpty() { + return this.chunkCount == 0; + } + + private boolean hasOnlyOneChunk() { + return this.chunkCount == 1; + } + + public boolean hasNonEmptyNeighbours() { + return this.nonEmptyNeighbours != 0; + } + + /** + * Returns the section data associated with this region section. May be {@code null}. + */ + public S getData() { + return this.data; + } + + /** + * Returns the region that owns this section. Unsynchronised access may produce outdateed or transient results. + */ + public ThreadedRegion getRegion() { + return this.getRegionAcquire(); + } + + private int getChunkIndex(final int chunkX, final int chunkZ) { + return (chunkX & this.regionChunkMask) | ((chunkZ & this.regionChunkMask) << this.regionChunkShift); + } + + private void markAlive() { + this.getRegionPlain().removeDeadSection(this); + } + + private void markDead() { + this.getRegionPlain().addDeadSection(this); + } + + private void incrementNonEmptyNeighbours() { + if (++this.nonEmptyNeighbours == 1 && this.chunkCount == 0) { + this.markAlive(); + } + final int createRadius = this.regioniser.emptySectionCreateRadius; + if (this.nonEmptyNeighbours >= ((createRadius * 2 + 1) * (createRadius * 2 + 1))) { + throw new IllegalStateException("Non empty neighbours exceeded max value for radius " + createRadius); + } + } + + private void decrementNonEmptyNeighbours() { + if (--this.nonEmptyNeighbours == 0 && this.chunkCount == 0) { + this.markDead(); + } + if (this.nonEmptyNeighbours < 0) { + throw new IllegalStateException("Non empty neighbours reached zero"); + } + } + + /** + * Returns whether the chunk was zero. Effectively returns whether the caller needs to create + * dead sections / increase non-empty neighbour count for neighbouring sections. + */ + private boolean 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()); + } + final boolean notEmpty = ++this.chunkCount == 1; + if (notEmpty && this.nonEmptyNeighbours == 0) { + this.markAlive(); + } + return notEmpty; + } + + /** + * Returns whether the chunk count is now zero. Effectively returns whether + * the caller needs to decrement the neighbour count for neighbouring sections. + */ + private boolean 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()); + } + final boolean empty = --this.chunkCount == 0; + if (empty && this.nonEmptyNeighbours == 0) { + this.markDead(); + } + return empty; + } + + @Override + public String toString() { + return "RegionSection{" + + "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," + + "chunkCount=" + this.chunkCount + "," + + "chunksBitset=" + toString(this.chunksBitset) + "," + + "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," + + "hash=" + this.hashCode() + + "}"; + } + + public String toStringWithRegion() { + return "RegionSection{" + + "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," + + "chunkCount=" + this.chunkCount + "," + + "chunksBitset=" + toString(this.chunksBitset) + "," + + "hash=" + this.hashCode() + "," + + "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," + + "region=" + this.getRegionAcquire() + + "}"; + } + + private static String toString(final long[] array) { + final StringBuilder ret = new StringBuilder(); + final char[] zeros = new char[Long.SIZE / 4]; + for (final long value : array) { + // zero pad the hex string + 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 ThreadedRegionData, S extends ThreadedRegionSectionData> { + + /** + * Splits this region data into the specified regions set. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param regioniser Regioniser for which the regions reside in. + * @param into A map of region section coordinate key to the region that owns the section. + * @param regions The set of regions to split into. + */ + public void split(final ThreadedRegionizer regioniser, final Long2ReferenceOpenHashMap> into, + final ReferenceOpenHashSet> regions); + + /** + * Callback to merge {@code this} region data into the specified region. The state of the region is undefined + * except that its region data is already created. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param into Specified region. + */ + public void mergeInto(final ThreadedRegion into); + } + + public static interface ThreadedRegionSectionData {} + + public static interface RegionCallbacks, S extends ThreadedRegionSectionData> { + + /** + * Creates new section data for the specified section x and section z. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param sectionX x coordinate of the section. + * @param sectionZ z coordinate of the section. + * @param sectionShift The signed right shift value that can be applied to any chunk coordinate that + * produces a section coordinate. + * @return New section data, may be {@code null}. + */ + public S createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift); + + /** + * Creates new region data for the specified region. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param forRegion The region to create the data for. + * @return New region data, may be {@code null}. + */ + public R createNewData(final ThreadedRegion forRegion); + + /** + * Callback for when a region is created. This is invoked after the region is completely set up, + * so its data and owned sections are reliable to inspect. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param region The region that was created. + */ + public void onRegionCreate(final ThreadedRegion region); + + /** + * Callback for when a region is destroyed. This is invoked before the region is actually destroyed; so + * its data and owned sections are reliable to inspect. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param region The region that is about to be destroyed. + */ + public void onRegionDestroy(final ThreadedRegion region); + + /** + * Callback for when a region is considered "active." An active region x is a non-destroyed region which + * is not scheduled to merge into another region y and there are no non-destroyed regions z which are + * scheduled to merge into the region x. Equivalently, an active region is not directly adjacent to any + * other region considering the regioniser's empty section radius. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param region The region that is now active. + */ + public void onRegionActive(final ThreadedRegion region); + + /** + * Callback for when a region transistions becomes inactive. An inactive region is non-destroyed, but + * has neighbouring adjacent regions considering the regioniser's empty section radius. Effectively, + * an inactive region may not tick and needs to be merged into its neighbouring regions. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param region The region that is now inactive. + */ + public void onRegionInactive(final ThreadedRegion region); + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/TickData.java b/src/main/java/io/papermc/paper/threadedregions/TickData.java new file mode 100644 index 0000000000000000000000000000000000000000..29f9fed5f02530b3256e6b993e607d4647daa7b6 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/TickData.java @@ -0,0 +1,333 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.util.TimeUtil; +import io.papermc.paper.util.IntervalledCounter; +import it.unimi.dsi.fastutil.longs.LongArrayList; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class TickData { + + private final long interval; // ns + + private final ArrayDeque timeData = new ArrayDeque<>(); + + public TickData(final long intervalNS) { + this.interval = intervalNS; + } + + public void addDataFrom(final TickRegionScheduler.TickTime time) { + final long start = time.tickStart(); + + TickRegionScheduler.TickTime first; + while ((first = this.timeData.peekFirst()) != null) { + // only remove data completely out of window + if ((start - first.tickEnd()) <= this.interval) { + break; + } + this.timeData.pollFirst(); + } + + this.timeData.add(time); + } + + // fromIndex inclusive, toIndex exclusive + // will throw if arr.length == 0 + private static double median(final long[] arr, final int fromIndex, final int toIndex) { + final int len = toIndex - fromIndex; + final int middle = fromIndex + (len >>> 1); + if ((len & 1) == 0) { + // even, average the two middle points + return (double)(arr[middle - 1] + arr[middle]) / 2.0; + } else { + // odd, just grab the middle + return (double)arr[middle]; + } + } + + // will throw if arr.length == 0 + private static SegmentData computeSegmentData(final long[] arr, final int fromIndex, final int toIndex, + final boolean inverse) { + final int len = toIndex - fromIndex; + long sum = 0L; + final double median = median(arr, fromIndex, toIndex); + long min = arr[0]; + long max = arr[0]; + + for (int i = fromIndex; i < toIndex; ++i) { + final long val = arr[i]; + sum += val; + if (val < min) { + min = val; + } + if (val > max) { + max = val; + } + } + + if (inverse) { + // for positive a,b we have that a >= b if and only if 1/a <= 1/b + return new SegmentData( + len, + (double)len / ((double)sum / 1.0E9), + 1.0E9 / median, + 1.0E9 / (double)max, + 1.0E9 / (double)min + ); + } else { + return new SegmentData( + len, + (double)sum / (double)len, + median, + (double)min, + (double)max + ); + } + } + + private static SegmentedAverage computeSegmentedAverage(final long[] data, final int allStart, final int allEnd, + final int percent99BestStart, final int percent99BestEnd, + final int percent95BestStart, final int percent95BestEnd, + final int percent1WorstStart, final int percent1WorstEnd, + final int percent5WorstStart, final int percent5WorstEnd, + final boolean inverse) { + return new SegmentedAverage( + computeSegmentData(data, allStart, allEnd, inverse), + computeSegmentData(data, percent99BestStart, percent99BestEnd, inverse), + computeSegmentData(data, percent95BestStart, percent95BestEnd, inverse), + computeSegmentData(data, percent1WorstStart, percent1WorstEnd, inverse), + computeSegmentData(data, percent5WorstStart, percent5WorstEnd, inverse) + ); + } + + private static record TickInformation( + long differenceFromLastTick, + long tickTime, + long tickTimeCPU + ) {} + + // rets null if there is no data + public TickReportData generateTickReport(final TickRegionScheduler.TickTime inProgress, final long endTime) { + if (this.timeData.isEmpty() && inProgress == null) { + return null; + } + + final List allData = new ArrayList<>(this.timeData); + if (inProgress != null) { + allData.add(inProgress); + } + + final long intervalStart = allData.get(0).tickStart(); + final long intervalEnd = allData.get(allData.size() - 1).tickEnd(); + + // to make utilisation accurate, we need to take the total time used over the last interval period - + // this means if a tick start before the measurement interval, but ends within the interval, then we + // only consider the time it spent ticking inside the interval + long totalTimeOverInterval = 0L; + long measureStart = endTime - this.interval; + + for (int i = 0, len = allData.size(); i < len; ++i) { + final TickRegionScheduler.TickTime time = allData.get(i); + if (TimeUtil.compareTimes(time.tickStart(), measureStart) < 0) { + final long diff = time.tickEnd() - measureStart; + if (diff > 0L) { + totalTimeOverInterval += diff; + } // else: the time is entirely out of interval + } else { + totalTimeOverInterval += time.tickLength(); + } + } + + // we only care about ticks, but because of inbetween tick task execution + // there will be data in allData that isn't ticks. But, that data cannot + // be ignored since it contributes to utilisation. + // So, we will "compact" the data by merging any inbetween tick times + // the next tick. + // If there is no "next tick", then we will create one. + final List collapsedData = new ArrayList<>(); + for (int i = 0, len = allData.size(); i < len; ++i) { + final List toCollapse = new ArrayList<>(); + TickRegionScheduler.TickTime lastTick = null; + for (;i < len; ++i) { + final TickRegionScheduler.TickTime time = allData.get(i); + if (!time.isTickExecution()) { + toCollapse.add(time); + continue; + } + lastTick = time; + break; + } + + if (toCollapse.isEmpty()) { + // nothing to collapse + final TickRegionScheduler.TickTime last = allData.get(i); + collapsedData.add( + new TickInformation( + last.differenceFromLastTick(), + last.tickLength(), + last.supportCPUTime() ? last.tickCpuTime() : 0L + ) + ); + } else { + long totalTickTime = 0L; + long totalCpuTime = 0L; + for (int k = 0, len2 = collapsedData.size(); k < len2; ++k) { + final TickRegionScheduler.TickTime time = toCollapse.get(k); + totalTickTime += time.tickLength(); + totalCpuTime += time.supportCPUTime() ? time.tickCpuTime() : 0L; + } + if (i < len) { + // we know there is a tick to collapse into + final TickRegionScheduler.TickTime last = allData.get(i); + collapsedData.add( + new TickInformation( + last.differenceFromLastTick(), + last.tickLength() + totalTickTime, + (last.supportCPUTime() ? last.tickCpuTime() : 0L) + totalCpuTime + ) + ); + } else { + // we do not have a tick to collapse into, so we must make one up + // we will assume that the tick is "starting now" and ongoing + + // compute difference between imaginary tick and last tick + final long differenceBetweenTicks; + if (lastTick != null) { + // we have a last tick, use it + differenceBetweenTicks = lastTick.tickStart(); + } else { + // we don't have a last tick, so we must make one up that makes sense + // if the current interval exceeds the max tick time, then use it + + // Otherwise use the interval length. + // This is how differenceFromLastTick() works on TickTime when there is no previous interval. + differenceBetweenTicks = Math.max( + TickRegionScheduler.TIME_BETWEEN_TICKS, totalTickTime + ); + } + + collapsedData.add( + new TickInformation( + differenceBetweenTicks, + totalTickTime, + totalCpuTime + ) + ); + } + } + } + + + final int collectedTicks = collapsedData.size(); + final long[] tickStartToStartDifferences = new long[collectedTicks]; + final long[] timePerTickDataRaw = new long[collectedTicks]; + final long[] missingCPUTimeDataRaw = new long[collectedTicks]; + + long totalTimeTicking = 0L; + + int i = 0; + for (final TickInformation time : collapsedData) { + tickStartToStartDifferences[i] = time.differenceFromLastTick(); + final long timePerTick = timePerTickDataRaw[i] = time.tickTime(); + missingCPUTimeDataRaw[i] = Math.max(0L, timePerTick - time.tickTimeCPU()); + + ++i; + + totalTimeTicking += timePerTick; + } + + Arrays.sort(tickStartToStartDifferences); + Arrays.sort(timePerTickDataRaw); + Arrays.sort(missingCPUTimeDataRaw); + + // Note: computeSegmentData cannot take start == end + final int allStart = 0; + final int allEnd = collectedTicks; + final int percent95BestStart = 0; + final int percent95BestEnd = collectedTicks == 1 ? 1 : (int)(0.95 * collectedTicks); + final int percent99BestStart = 0; + // (int)(0.99 * collectedTicks) == 0 if collectedTicks = 1, so we need to use 1 to avoid start == end + final int percent99BestEnd = collectedTicks == 1 ? 1 : (int)(0.99 * collectedTicks); + final int percent1WorstStart = (int)(0.99 * collectedTicks); + final int percent1WorstEnd = collectedTicks; + final int percent5WorstStart = (int)(0.95 * collectedTicks); + final int percent5WorstEnd = collectedTicks; + + final SegmentedAverage tpsData = computeSegmentedAverage( + tickStartToStartDifferences, + allStart, allEnd, + percent99BestStart, percent99BestEnd, + percent95BestStart, percent95BestEnd, + percent1WorstStart, percent1WorstEnd, + percent5WorstStart, percent5WorstEnd, + true + ); + + final SegmentedAverage timePerTickData = computeSegmentedAverage( + timePerTickDataRaw, + allStart, allEnd, + percent99BestStart, percent99BestEnd, + percent95BestStart, percent95BestEnd, + percent1WorstStart, percent1WorstEnd, + percent5WorstStart, percent5WorstEnd, + false + ); + + final SegmentedAverage missingCPUTimeData = computeSegmentedAverage( + missingCPUTimeDataRaw, + allStart, allEnd, + percent99BestStart, percent99BestEnd, + percent95BestStart, percent95BestEnd, + percent1WorstStart, percent1WorstEnd, + percent5WorstStart, percent5WorstEnd, + false + ); + + final double utilisation = (double)totalTimeOverInterval / (double)this.interval; + + return new TickReportData( + collectedTicks, + intervalStart, + intervalEnd, + totalTimeTicking, + utilisation, + + tpsData, + timePerTickData, + missingCPUTimeData + ); + } + + public static final record TickReportData( + int collectedTicks, + long collectedTickIntervalStart, + long collectedTickIntervalEnd, + long totalTimeTicking, + double utilisation, + + SegmentedAverage tpsData, + // in ns + SegmentedAverage timePerTickData, + // in ns + SegmentedAverage missingCPUTimeData + ) {} + + public static final record SegmentedAverage( + SegmentData segmentAll, + SegmentData segment99PercentBest, + SegmentData segment95PercentBest, + SegmentData segment5PercentWorst, + SegmentData segment1PercentWorst + ) {} + + public static final record SegmentData( + int count, + double average, + double median, + double least, + double greatest + ) {} +} diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java b/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java new file mode 100644 index 0000000000000000000000000000000000000000..ee9f5e1f3387998cddbeb1dc6dc6e2b1ea7cd670 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java @@ -0,0 +1,565 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; +import ca.spottedleaf.concurrentutil.util.TimeUtil; +import com.mojang.logging.LogUtils; +import io.papermc.paper.util.TickThread; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import org.slf4j.Logger; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BooleanSupplier; + +public final class TickRegionScheduler { + + private static final Logger LOGGER = LogUtils.getLogger(); + private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean(); + private static final boolean MEASURE_CPU_TIME; + static { + MEASURE_CPU_TIME = THREAD_MX_BEAN.isThreadCpuTimeSupported(); + if (MEASURE_CPU_TIME) { + THREAD_MX_BEAN.setThreadCpuTimeEnabled(true); + } else { + LOGGER.warn("TickRegionScheduler CPU time measurement is not available"); + } + } + + public static final int TICK_RATE = 20; + public static final long TIME_BETWEEN_TICKS = 1_000_000_000L / TICK_RATE; // ns + + private final SchedulerThreadPool scheduler; + + public TickRegionScheduler(final int threads) { + this.scheduler = new SchedulerThreadPool(threads, new ThreadFactory() { + private final AtomicInteger idGenerator = new AtomicInteger(); + + @Override + public Thread newThread(final Runnable run) { + final Thread ret = new TickThreadRunner(run, "Region Scheduler Thread #" + this.idGenerator.getAndIncrement()); + ret.setUncaughtExceptionHandler(TickRegionScheduler.this::uncaughtException); + return ret; + } + }); + } + + public int getTotalThreadCount() { + return this.scheduler.getThreads().length; + } + + private static void setTickingRegion(final ThreadedRegionizer.ThreadedRegion region) { + final Thread currThread = Thread.currentThread(); + if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { + throw new IllegalStateException("Must be tick thread runner"); + } + if (region != null && tickThreadRunner.currentTickingRegion != null) { + throw new IllegalStateException("Trying to double set ticking region!"); + } + if (region == null && tickThreadRunner.currentTickingRegion == null) { + throw new IllegalStateException("Trying to double unset ticking region!"); + } + tickThreadRunner.currentTickingRegion = region; + if (region != null) { + tickThreadRunner.currentTickingWorldRegionizedData = region.regioniser.world.worldRegionData.get(); + } else { + tickThreadRunner.currentTickingWorldRegionizedData = null; + } + } + + private static void setTickTask(final SchedulerThreadPool.SchedulableTick task) { + final Thread currThread = Thread.currentThread(); + if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { + throw new IllegalStateException("Must be tick thread runner"); + } + if (task != null && tickThreadRunner.currentTickingTask != null) { + throw new IllegalStateException("Trying to double set ticking task!"); + } + if (task == null && tickThreadRunner.currentTickingTask == null) { + throw new IllegalStateException("Trying to double unset ticking task!"); + } + tickThreadRunner.currentTickingTask = task; + } + + /** + * Returns the current ticking region, or {@code null} if there is no ticking region. + * If this thread is not a TickThread, then returns {@code null}. + */ + public static ThreadedRegionizer.ThreadedRegion getCurrentRegion() { + final Thread currThread = Thread.currentThread(); + if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { + return RegionShutdownThread.getRegion(); + } + return tickThreadRunner.currentTickingRegion; + } + + /** + * Returns the current ticking region's world regionised data, or {@code null} if there is no ticking region. + * This is a faster alternative to calling the {@link RegionizedData#get()} method. + * If this thread is not a TickThread, then returns {@code null}. + */ + public static RegionizedWorldData getCurrentRegionizedWorldData() { + final Thread currThread = Thread.currentThread(); + if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { + return RegionShutdownThread.getWorldData(); + } + return tickThreadRunner.currentTickingWorldRegionizedData; + } + + /** + * Returns the current ticking task, or {@code null} if there is no ticking region. + * If this thread is not a TickThread, then returns {@code null}. + */ + public static SchedulerThreadPool.SchedulableTick getCurrentTickingTask() { + final Thread currThread = Thread.currentThread(); + if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { + return null; + } + return tickThreadRunner.currentTickingTask; + } + + /** + * Schedules the given region + * @throws IllegalStateException If the region is already scheduled or is ticking + */ + public void scheduleRegion(final RegionScheduleHandle region) { + region.scheduler = this; + this.scheduler.schedule(region); + } + + /** + * Attempts to de-schedule the provided region. If the current region cannot be cancelled for its next tick or task + * execution, then it will be cancelled after. + */ + public void descheduleRegion(final RegionScheduleHandle region) { + // To avoid acquiring any of the locks the scheduler may be using, we + // simply cancel the next action. + region.markNonSchedulable(); + } + + /** + * Updates the tick start to the farthest into the future of its current scheduled time and the + * provided time. + * @return {@code false} if the region was not scheduled or is currently ticking or the specified time is less-than its + * current start time, {@code true} if the next tick start was adjusted. + */ + public boolean updateTickStartToMax(final RegionScheduleHandle region, final long newStart) { + return this.scheduler.updateTickStartToMax(region, newStart); + } + + public boolean halt(final boolean sync, final long maxWaitNS) { + return this.scheduler.halt(sync, maxWaitNS); + } + + void dumpAliveThreadTraces(final String reason) { + this.scheduler.dumpAliveThreadTraces(reason); + } + + public void setHasTasks(final RegionScheduleHandle region) { + this.scheduler.notifyTasks(region); + } + + public void init() { + this.scheduler.start(); + } + + private void uncaughtException(final Thread thread, final Throwable thr) { + LOGGER.error("Uncaught exception in tick thread \"" + thread.getName() + "\"", thr); + + // prevent further ticks from occurring + // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD + this.scheduler.halt(false, 0L); + + MinecraftServer.getServer().stopServer(); + } + + private void regionFailed(final RegionScheduleHandle handle, final boolean executingTasks, final Throwable thr) { + // when a region fails, we need to shut down the server gracefully + + // prevent further ticks from occurring + // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD + this.scheduler.halt(false, 0L); + + final ChunkPos center = handle.region == null ? null : handle.region.region.getCenterChunk(); + final ServerLevel world = handle.region == null ? null : handle.region.world; + + LOGGER.error("Region #" + (handle.region == null ? -1L : handle.region.id) + " centered at chunk " + center + " in world '" + (world == null ? "null" : world.getWorld().getName()) + "' failed to " + (executingTasks ? "execute tasks" : "tick") + ":", thr); + + MinecraftServer.getServer().stopServer(); + } + + // By using our own thread object, we can use a field for the current region rather than a ThreadLocal. + // This is much faster than a thread local, since the thread local has to use a map lookup. + private static final class TickThreadRunner extends TickThread { + + private ThreadedRegionizer.ThreadedRegion currentTickingRegion; + private RegionizedWorldData currentTickingWorldRegionizedData; + private SchedulerThreadPool.SchedulableTick currentTickingTask; + + public TickThreadRunner(final Runnable run, final String name) { + super(run, name); + } + } + + public static abstract class RegionScheduleHandle extends SchedulerThreadPool.SchedulableTick { + + protected long currentTick; + protected long lastTickStart; + + protected final TickData tickTimes5s; + protected final TickData tickTimes15s; + protected final TickData tickTimes1m; + protected final TickData tickTimes5m; + protected final TickData tickTimes15m; + protected TickTime currentTickData; + protected Thread currentTickingThread; + + public final TickRegions.TickRegionData region; + private final AtomicBoolean cancelled = new AtomicBoolean(); + + protected final Schedule tickSchedule; + + private TickRegionScheduler scheduler; + + public RegionScheduleHandle(final TickRegions.TickRegionData region, final long firstStart) { + this.currentTick = 0L; + this.lastTickStart = SchedulerThreadPool.DEADLINE_NOT_SET; + this.tickTimes5s = new TickData(TimeUnit.SECONDS.toNanos(5L)); + this.tickTimes15s = new TickData(TimeUnit.SECONDS.toNanos(15L)); + this.tickTimes1m = new TickData(TimeUnit.MINUTES.toNanos(1L)); + this.tickTimes5m = new TickData(TimeUnit.MINUTES.toNanos(5L)); + this.tickTimes15m = new TickData(TimeUnit.MINUTES.toNanos(15L)); + this.region = region; + + this.setScheduledStart(firstStart); + this.tickSchedule = new Schedule(firstStart == SchedulerThreadPool.DEADLINE_NOT_SET ? firstStart : firstStart - TIME_BETWEEN_TICKS); + } + + /** + * Subclasses should call this instead of {@link ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool.SchedulableTick#setScheduledStart(long)} + * so that the tick schedule and scheduled start remain synchronised + */ + protected final void updateScheduledStart(final long to) { + this.setScheduledStart(to); + this.tickSchedule.setLastPeriod(to == SchedulerThreadPool.DEADLINE_NOT_SET ? to : to - TIME_BETWEEN_TICKS); + } + + public final void markNonSchedulable() { + this.cancelled.set(true); + } + + public final boolean isMarkedAsNonSchedulable() { + return this.cancelled.get(); + } + + protected abstract boolean tryMarkTicking(); + + protected abstract boolean markNotTicking(); + + protected abstract void tickRegion(final int tickCount, final long startTime, final long scheduledEnd); + + protected abstract boolean runRegionTasks(final BooleanSupplier canContinue); + + protected abstract boolean hasIntermediateTasks(); + + @Override + public final boolean hasTasks() { + return this.hasIntermediateTasks(); + } + + @Override + public final Boolean runTasks(final BooleanSupplier canContinue) { + if (this.cancelled.get()) { + return null; + } + + final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; + final long tickStart = System.nanoTime(); + + if (!this.tryMarkTicking()) { + if (!this.cancelled.get()) { + throw new IllegalStateException("Scheduled region should be acquirable"); + } + // region was killed + return null; + } + + TickRegionScheduler.setTickTask(this); + if (this.region != null) { + TickRegionScheduler.setTickingRegion(this.region.region); + } + + synchronized (this) { + this.currentTickData = new TickTime( + SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, tickStart, cpuStart, + SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME, + false + ); + this.currentTickingThread = Thread.currentThread(); + } + + final boolean ret; + try { + ret = this.runRegionTasks(() -> { + return !RegionScheduleHandle.this.cancelled.get() && canContinue.getAsBoolean(); + }); + } catch (final Throwable thr) { + this.scheduler.regionFailed(this, true, thr); + if (thr instanceof ThreadDeath) { + throw (ThreadDeath)thr; + } + // don't release region for another tick + return null; + } finally { + TickRegionScheduler.setTickTask(null); + if (this.region != null) { + TickRegionScheduler.setTickingRegion(null); + } + final long tickEnd = System.nanoTime(); + final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; + + final TickTime time = new TickTime( + SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, + tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, false + ); + + this.addTickTime(time); + } + + return !this.markNotTicking() || this.cancelled.get() ? null : Boolean.valueOf(ret); + } + + @Override + public final boolean runTick() { + // Remember, we are supposed use setScheduledStart if we return true here, otherwise + // the scheduler will try to schedule for the same time. + if (this.cancelled.get()) { + return false; + } + + final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; + final long tickStart = System.nanoTime(); + + // use max(), don't assume that tickStart >= scheduledStart + final int tickCount = Math.max(1, this.tickSchedule.getPeriodsAhead(TIME_BETWEEN_TICKS, tickStart)); + + if (!this.tryMarkTicking()) { + if (!this.cancelled.get()) { + throw new IllegalStateException("Scheduled region should be acquirable"); + } + // region was killed + return false; + } + if (this.cancelled.get()) { + this.markNotTicking(); + // region should be killed + return false; + } + + TickRegionScheduler.setTickTask(this); + if (this.region != null) { + TickRegionScheduler.setTickingRegion(this.region.region); + } + this.incrementTickCount(); + final long lastTickStart = this.lastTickStart; + this.lastTickStart = tickStart; + + final long scheduledStart = this.getScheduledStart(); + final long scheduledEnd = scheduledStart + TIME_BETWEEN_TICKS; + + synchronized (this) { + this.currentTickData = new TickTime( + lastTickStart, scheduledStart, tickStart, cpuStart, + SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME, + true + ); + this.currentTickingThread = Thread.currentThread(); + } + + try { + // next start isn't updated until the end of this tick + this.tickRegion(tickCount, tickStart, scheduledEnd); + } catch (final Throwable thr) { + this.scheduler.regionFailed(this, false, thr); + if (thr instanceof ThreadDeath) { + throw (ThreadDeath)thr; + } + // regionFailed will schedule a shutdown, so we should avoid letting this region tick further + return false; + } finally { + TickRegionScheduler.setTickTask(null); + if (this.region != null) { + TickRegionScheduler.setTickingRegion(null); + } + final long tickEnd = System.nanoTime(); + final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; + + // in order to ensure all regions get their chance at scheduling, we have to ensure that regions + // that exceed the max tick time are not always prioritised over everything else. Thus, we use the greatest + // of the current time and "ideal" next tick start. + this.tickSchedule.advanceBy(tickCount, TIME_BETWEEN_TICKS); + this.setScheduledStart(TimeUtil.getGreatestTime(tickEnd, this.tickSchedule.getDeadline(TIME_BETWEEN_TICKS))); + + final TickTime time = new TickTime( + lastTickStart, scheduledStart, tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, true + ); + + this.addTickTime(time); + } + + // Only AFTER updating the tickStart + return this.markNotTicking() && !this.cancelled.get(); + } + + /** + * Only safe to call if this tick data matches the current ticking region. + */ + private void addTickTime(final TickTime time) { + synchronized (this) { + this.currentTickData = null; + this.currentTickingThread = null; + this.tickTimes5s.addDataFrom(time); + this.tickTimes15s.addDataFrom(time); + this.tickTimes1m.addDataFrom(time); + this.tickTimes5m.addDataFrom(time); + this.tickTimes15m.addDataFrom(time); + } + } + + private TickTime adjustCurrentTickData(final long tickEnd) { + final TickTime currentTickData = this.currentTickData; + if (currentTickData == null) { + return null; + } + + final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getThreadCpuTime(this.currentTickingThread.getId()) : 0L; + + return new TickTime( + currentTickData.previousTickStart(), currentTickData.scheduledTickStart(), + currentTickData.tickStart(), currentTickData.tickStartCPU(), + tickEnd, cpuEnd, + MEASURE_CPU_TIME, currentTickData.isTickExecution() + ); + } + + public final TickData.TickReportData getTickReport5s(final long currTime) { + synchronized (this) { + return this.tickTimes5s.generateTickReport(this.adjustCurrentTickData(currTime), currTime); + } + } + + public final TickData.TickReportData getTickReport15s(final long currTime) { + synchronized (this) { + return this.tickTimes15s.generateTickReport(this.adjustCurrentTickData(currTime), currTime); + } + } + + public final TickData.TickReportData getTickReport1m(final long currTime) { + synchronized (this) { + return this.tickTimes1m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); + } + } + + public final TickData.TickReportData getTickReport5m(final long currTime) { + synchronized (this) { + return this.tickTimes5m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); + } + } + + public final TickData.TickReportData getTickReport15m(final long currTime) { + synchronized (this) { + return this.tickTimes15m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); + } + } + + /** + * Only safe to call if this tick data matches the current ticking region. + */ + private void incrementTickCount() { + ++this.currentTick; + } + + /** + * Only safe to call if this tick data matches the current ticking region. + */ + public final long getCurrentTick() { + return this.currentTick; + } + + protected final void setCurrentTick(final long value) { + this.currentTick = value; + } + } + + // All time units are in nanoseconds. + public static final record TickTime( + long previousTickStart, + long scheduledTickStart, + long tickStart, + long tickStartCPU, + long tickEnd, + long tickEndCPU, + boolean supportCPUTime, + boolean isTickExecution + ) { + /** + * The difference between the start tick time and the scheduled start tick time. This value is + * < 0 if the tick started before the scheduled tick time. + * Only valid when {@link #isTickExecution()} is {@code true}. + */ + public final long startOvershoot() { + return this.tickStart - this.scheduledTickStart; + } + + /** + * The difference from the end tick time and the start tick time. Always >= 0 (unless nanoTime is just wrong). + */ + public final long tickLength() { + return this.tickEnd - this.tickStart; + } + + /** + * The total CPU time from the start tick time to the end tick time. Generally should be equal to the tickLength, + * unless there is CPU starvation or the tick thread was blocked by I/O or other tasks. Returns Long.MIN_VALUE + * if CPU time measurement is not supported. + */ + public final long tickCpuTime() { + if (!this.supportCPUTime()) { + return Long.MIN_VALUE; + } + return this.tickEndCPU - this.tickStartCPU; + } + + /** + * The difference in time from the start of the last tick to the start of the current tick. If there is no + * last tick, then this value is max(TIME_BETWEEN_TICKS, tickLength). + * Only valid when {@link #isTickExecution()} is {@code true}. + */ + public final long differenceFromLastTick() { + if (this.hasLastTick()) { + return this.tickStart - this.previousTickStart; + } + return Math.max(TIME_BETWEEN_TICKS, this.tickLength()); + } + + /** + * Returns whether there was a tick that occurred before this one. + * Only valid when {@link #isTickExecution()} is {@code true}. + */ + public boolean hasLastTick() { + return this.previousTickStart != SchedulerThreadPool.DEADLINE_NOT_SET; + } + + /* + * Remember, this is the expected behavior of the following: + * + * MSPT: Time per tick. This does not include overshoot time, just the tickLength(). + * + * TPS: The number of ticks per second. It should be ticks / (sum of differenceFromLastTick). + */ + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java new file mode 100644 index 0000000000000000000000000000000000000000..c6bc546b67c701f932e69630db1a5f83efc255fa --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java @@ -0,0 +1,399 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; +import ca.spottedleaf.concurrentutil.util.TimeUtil; +import com.mojang.logging.LogUtils; +import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager; +import io.papermc.paper.configuration.GlobalConfiguration; +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.slf4j.Logger; +import java.util.Iterator; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BooleanSupplier; + +public final class TickRegions implements ThreadedRegionizer.RegionCallbacks { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public static int getRegionChunkShift() { + return 4; + } + + private static boolean initialised; + private static TickRegionScheduler scheduler; + + public static TickRegionScheduler getScheduler() { + return scheduler; + } + + public static void init(final GlobalConfiguration.ThreadedRegions config) { + if (initialised) { + return; + } + initialised = true; + + int tickThreads; + if (config.threads <= 0) { + tickThreads = Runtime.getRuntime().availableProcessors() / 2; + if (tickThreads <= 4) { + tickThreads = 1; + } else { + tickThreads = tickThreads / 4; + } + } else { + tickThreads = config.threads; + } + + scheduler = new TickRegionScheduler(tickThreads); + LOGGER.info("Regionised ticking is enabled with " + tickThreads + " tick threads"); + } + + @Override + public TickRegionData createNewData(final ThreadedRegionizer.ThreadedRegion region) { + return new TickRegionData(region); + } + + @Override + public TickRegionSectionData createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift) { + return null; + } + + @Override + public void onRegionCreate(final ThreadedRegionizer.ThreadedRegion region) { + final TickRegionData data = region.getData(); + // post-region merge/split regioninfo update + data.getRegionStats().updateFrom(data.getOrCreateRegionizedData(data.world.worldRegionData)); + } + + @Override + public void onRegionDestroy(final ThreadedRegionizer.ThreadedRegion region) { + // nothing for now + } + + @Override + public void onRegionActive(final ThreadedRegionizer.ThreadedRegion region) { + final TickRegionData data = region.getData(); + + data.tickHandle.checkInitialSchedule(); + scheduler.scheduleRegion(data.tickHandle); + } + + @Override + public void onRegionInactive(final ThreadedRegionizer.ThreadedRegion region) { + final TickRegionData data = region.getData(); + + scheduler.descheduleRegion(data.tickHandle); + // old handle cannot be scheduled anymore, copy to a new handle + data.tickHandle = data.tickHandle.copy(); + } + + public static final class TickRegionSectionData implements ThreadedRegionizer.ThreadedRegionSectionData {} + + public static final class RegionStats { + + private final AtomicInteger entityCount = new AtomicInteger(); + private final AtomicInteger playerCount = new AtomicInteger(); + private final AtomicInteger chunkCount = new AtomicInteger(); + + public int getEntityCount() { + return this.entityCount.get(); + } + + public int getPlayerCount() { + return this.playerCount.get(); + } + + public int getChunkCount() { + return this.chunkCount.get(); + } + + void updateFrom(final RegionizedWorldData data) { + this.entityCount.setRelease(data == null ? 0 : data.getEntityCount()); + this.playerCount.setRelease(data == null ? 0 : data.getPlayerCount()); + this.chunkCount.setRelease(data == null ? 0 : data.getChunkCount()); + } + + static void updateCurrentRegion() { + TickRegionScheduler.getCurrentRegion().getData().getRegionStats().updateFrom(TickRegionScheduler.getCurrentRegionizedWorldData()); + } + } + + public static final class TickRegionData implements ThreadedRegionizer.ThreadedRegionData { + + private static final AtomicLong ID_GENERATOR = new AtomicLong(); + /** Never 0L, since 0L is reserved for global region. */ + public final long id = ID_GENERATOR.incrementAndGet(); + + public final ThreadedRegionizer.ThreadedRegion region; + public final ServerLevel world; + + // generic regionised data + private final Reference2ReferenceOpenHashMap, Object> regionizedData = new Reference2ReferenceOpenHashMap<>(); + + // tick data + private ConcreteRegionTickHandle tickHandle = new ConcreteRegionTickHandle(this, SchedulerThreadPool.DEADLINE_NOT_SET); + + // queue data + private final RegionizedTaskQueue.RegionTaskQueueData taskQueueData; + + // chunk holder manager data + private final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = new ChunkHolderManager.HolderManagerRegionData(); + + // async-safe read-only region data + private final RegionStats regionStats; + + private TickRegionData(final ThreadedRegionizer.ThreadedRegion region) { + this.region = region; + this.world = region.regioniser.world; + this.taskQueueData = new RegionizedTaskQueue.RegionTaskQueueData(this.world.taskQueueRegionData); + this.regionStats = new RegionStats(); + } + + public RegionStats getRegionStats() { + return this.regionStats; + } + + public RegionizedTaskQueue.RegionTaskQueueData getTaskQueueData() { + return this.taskQueueData; + } + + // the value returned can be invalidated at any time, except when the caller + // is ticking this region + public TickRegionScheduler.RegionScheduleHandle getRegionSchedulingHandle() { + return this.tickHandle; + } + + public long getCurrentTick() { + return this.tickHandle.getCurrentTick(); + } + + public ChunkHolderManager.HolderManagerRegionData getHolderManagerRegionData() { + return this.holderManagerRegionData; + } + + T getRegionizedData(final RegionizedData regionizedData) { + return (T)this.regionizedData.get(regionizedData); + } + + T getOrCreateRegionizedData(final RegionizedData regionizedData) { + T ret = (T)this.regionizedData.get(regionizedData); + + if (ret != null) { + return ret; + } + + ret = regionizedData.createNewValue(); + this.regionizedData.put(regionizedData, ret); + + return ret; + } + + @Override + public void split(final ThreadedRegionizer regioniser, + final Long2ReferenceOpenHashMap> into, + final ReferenceOpenHashSet> regions) { + final int shift = regioniser.sectionChunkShift; + + // tick data + // note: here it is OK force us to access tick handle, as this region is owned (and thus not scheduled), + // and the other regions to split into are not scheduled yet. + for (final ThreadedRegionizer.ThreadedRegion region : regions) { + final TickRegionData data = region.getData(); + data.tickHandle.copyDeadlineAndTickCount(this.tickHandle); + } + + // generic regionised data + for (final Iterator, Object>> dataIterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator(); + dataIterator.hasNext();) { + final Reference2ReferenceMap.Entry, Object> regionDataEntry = dataIterator.next(); + final RegionizedData data = regionDataEntry.getKey(); + final Object from = regionDataEntry.getValue(); + + final ReferenceOpenHashSet dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f); + + for (final ThreadedRegionizer.ThreadedRegion region : regions) { + dataSet.add(region.getData().getOrCreateRegionizedData(data)); + } + + final Long2ReferenceOpenHashMap regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f); + + for (final Iterator>> regionIterator = into.long2ReferenceEntrySet().fastIterator(); + regionIterator.hasNext();) { + final Long2ReferenceMap.Entry> entry = regionIterator.next(); + final ThreadedRegionizer.ThreadedRegion region = entry.getValue(); + final Object to = region.getData().getOrCreateRegionizedData(data); + + regionToData.put(entry.getLongKey(), to); + } + + ((RegionizedData)data).getCallback().split(from, shift, regionToData, dataSet); + } + + // chunk holder manager data + { + final ReferenceOpenHashSet dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f); + + for (final ThreadedRegionizer.ThreadedRegion region : regions) { + dataSet.add(region.getData().holderManagerRegionData); + } + + final Long2ReferenceOpenHashMap regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f); + + for (final Iterator>> regionIterator = into.long2ReferenceEntrySet().fastIterator(); + regionIterator.hasNext();) { + final Long2ReferenceMap.Entry> entry = regionIterator.next(); + final ThreadedRegionizer.ThreadedRegion region = entry.getValue(); + final ChunkHolderManager.HolderManagerRegionData to = region.getData().holderManagerRegionData; + + regionToData.put(entry.getLongKey(), to); + } + + this.holderManagerRegionData.split(shift, regionToData, dataSet); + } + + // task queue + this.taskQueueData.split(regioniser, into); + } + + @Override + public void mergeInto(final ThreadedRegionizer.ThreadedRegion into) { + // Note: merge target is always a region being released from ticking + final TickRegionData data = into.getData(); + final long currentTickTo = data.getCurrentTick(); + final long currentTickFrom = this.getCurrentTick(); + + // here we can access tickHandle because the target (into) is the region being released, so it is + // not actually scheduled + // there's not really a great solution to the tick problem, no matter what it'll be messed up + // we will pick the greatest time delay so that tps will not exceed TICK_RATE + data.tickHandle.updateSchedulingToMax(this.tickHandle); + + // generic regionised data + final long fromTickOffset = currentTickTo - currentTickFrom; // see merge jd + for (final Iterator, Object>> iterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator(); + iterator.hasNext();) { + final Reference2ReferenceMap.Entry, Object> entry = iterator.next(); + final RegionizedData regionizedData = entry.getKey(); + final Object from = entry.getValue(); + final Object to = into.getData().getOrCreateRegionizedData(regionizedData); + + ((RegionizedData)regionizedData).getCallback().merge(from, to, fromTickOffset); + } + + // chunk holder manager data + this.holderManagerRegionData.merge(into.getData().holderManagerRegionData, fromTickOffset); + + // task queue + this.taskQueueData.mergeInto(data.taskQueueData); + } + } + + private static final class ConcreteRegionTickHandle extends TickRegionScheduler.RegionScheduleHandle { + + private final TickRegionData region; + + private ConcreteRegionTickHandle(final TickRegionData region, final long start) { + super(region, start); + this.region = region; + } + + private ConcreteRegionTickHandle copy() { + final ConcreteRegionTickHandle ret = new ConcreteRegionTickHandle(this.region, this.getScheduledStart()); + + ret.currentTick = this.currentTick; + ret.lastTickStart = this.lastTickStart; + ret.tickSchedule.setLastPeriod(this.tickSchedule.getLastPeriod()); + + return ret; + } + + private void updateSchedulingToMax(final ConcreteRegionTickHandle from) { + if (from.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { + return; + } + + if (this.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { + this.updateScheduledStart(from.getScheduledStart()); + return; + } + + this.updateScheduledStart(TimeUtil.getGreatestTime(from.getScheduledStart(), this.getScheduledStart())); + } + + private void copyDeadlineAndTickCount(final ConcreteRegionTickHandle from) { + this.currentTick = from.currentTick; + + if (from.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { + return; + } + + this.tickSchedule.setLastPeriod(from.tickSchedule.getLastPeriod()); + this.setScheduledStart(from.getScheduledStart()); + } + + private void checkInitialSchedule() { + if (this.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { + this.updateScheduledStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS); + } + } + + @Override + protected boolean tryMarkTicking() { + return this.region.region.tryMarkTicking(ConcreteRegionTickHandle.this::isMarkedAsNonSchedulable); + } + + @Override + protected boolean markNotTicking() { + return this.region.region.markNotTicking(); + } + + @Override + protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) { + MinecraftServer.getServer().tickServer(startTime, scheduledEnd, TimeUnit.MILLISECONDS.toMillis(10L), this.region); + } + + @Override + protected boolean runRegionTasks(final BooleanSupplier canContinue) { + final RegionizedTaskQueue.RegionTaskQueueData queue = this.region.taskQueueData; + + // first, update chunk loader + final ServerLevel world = this.region.world; + if (world.playerChunkLoader.tickMidTick()) { + // only process ticket updates if the player chunk loader did anything + world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); + } + + boolean processedChunkTask = false; + + boolean executeChunkTask = true; + boolean executeTickTask = true; + do { + if (executeTickTask) { + executeTickTask = queue.executeTickTask(); + } + if (executeChunkTask) { + processedChunkTask |= (executeChunkTask = queue.executeChunkTask()); + } + } while ((executeChunkTask | executeTickTask) && canContinue.getAsBoolean()); + + if (processedChunkTask) { + // if we processed any chunk tasks, try to process ticket level updates for full status changes + world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); + } + return true; + } + + @Override + protected boolean hasIntermediateTasks() { + return this.region.taskQueueData.hasTasks(); + } + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java new file mode 100644 index 0000000000000000000000000000000000000000..3bcb1dc98c61e025874cc9e008faa722581a530c --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java @@ -0,0 +1,355 @@ +package io.papermc.paper.threadedregions.commands; + +import io.papermc.paper.threadedregions.RegionizedServer; +import io.papermc.paper.threadedregions.RegionizedWorldData; +import io.papermc.paper.threadedregions.ThreadedRegionizer; +import io.papermc.paper.threadedregions.TickData; +import io.papermc.paper.threadedregions.TickRegionScheduler; +import io.papermc.paper.threadedregions.TickRegions; +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +public final class CommandServerHealth extends Command { + + private static final ThreadLocal TWO_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { + return new DecimalFormat("#,##0.00"); + }); + private static final ThreadLocal ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { + return new DecimalFormat("#,##0.0"); + }); + private static final ThreadLocal NO_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { + return new DecimalFormat("#,##0"); + }); + + private static final TextColor HEADER = TextColor.color(79, 164, 240); + private static final TextColor PRIMARY = TextColor.color(48, 145, 237); + private static final TextColor SECONDARY = TextColor.color(104, 177, 240); + private static final TextColor INFORMATION = TextColor.color(145, 198, 243); + private static final TextColor LIST = TextColor.color(33, 97, 188); + + public CommandServerHealth() { + super("tps"); + this.setUsage("/ [server/region] [lowest regions to display]"); + this.setDescription("Reports information about server health."); + this.setPermission("bukkit.command.tps"); + } + + private static Component formatRegionInfo(final String prefix, final double util, final double mspt, final double tps, + final boolean newline) { + return Component.text() + .append(Component.text(prefix, PRIMARY, TextDecoration.BOLD)) + .append(Component.text(ONE_DECIMAL_PLACES.get().format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) + .append(Component.text("% util at ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(mspt), CommandUtil.getColourForMSPT(mspt))) + .append(Component.text(" MSPT at ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(tps), CommandUtil.getColourForTPS(tps))) + .append(Component.text(" TPS" + (newline ? "\n" : ""), PRIMARY)) + .build(); + } + + private static Component formatRegionStats(final TickRegions.RegionStats stats, final boolean newline) { + return Component.text() + .append(Component.text("Chunks: ", PRIMARY)) + .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getChunkCount()), INFORMATION)) + .append(Component.text(" Players: ", PRIMARY)) + .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getPlayerCount()), INFORMATION)) + .append(Component.text(" Entities: ", PRIMARY)) + .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getEntityCount()) + (newline ? "\n" : ""), INFORMATION)) + .build(); + } + + private static boolean executeRegion(final CommandSender sender, final String commandLabel, final String[] args) { + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + if (region == null) { + sender.sendMessage(Component.text("You are not in a region currently", NamedTextColor.RED)); + return true; + } + + final long currTime = System.nanoTime(); + + final TickData.TickReportData report15s = region.getData().getRegionSchedulingHandle().getTickReport15s(currTime); + final TickData.TickReportData report1m = region.getData().getRegionSchedulingHandle().getTickReport1m(currTime); + + final ServerLevel world = region.regioniser.world; + final ChunkPos chunkCenter = region.getCenterChunk(); + final int centerBlockX = ((chunkCenter.x << 4) | 7); + final int centerBlockZ = ((chunkCenter.z << 4) | 7); + + final double util15s = report15s.utilisation(); + final double tps15s = report15s.tpsData().segmentAll().average(); + final double mspt15s = report15s.timePerTickData().segmentAll().average() / 1.0E6; + + final double util1m = report1m.utilisation(); + final double tps1m = report1m.tpsData().segmentAll().average(); + final double mspt1m = report1m.timePerTickData().segmentAll().average() / 1.0E6; + + final int yLoc = 80; + final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]"; + + final Component line = Component.text() + .append(Component.text("Region around block ", PRIMARY)) + .append(Component.text(location, INFORMATION)) + .append(Component.text(":\n", PRIMARY)) + + .append( + formatRegionInfo("15s: ", util15s, mspt15s, tps15s, true) + ) + .append( + formatRegionInfo("1m: ", util1m, mspt1m, tps1m, true) + ) + .append( + formatRegionStats(region.getData().getRegionStats(), false) + ) + + .build(); + + sender.sendMessage(line); + + return true; + } + + private static boolean executeServer(final CommandSender sender, final String commandLabel, final String[] args) { + final int lowestRegionsCount; + if (args.length < 2) { + lowestRegionsCount = 3; + } else { + try { + lowestRegionsCount = Integer.parseInt(args[1]); + } catch (final NumberFormatException ex) { + sender.sendMessage(Component.text("Highest utilisation count '" + args[1] + "' must be an integer", NamedTextColor.RED)); + return true; + } + } + + final List> regions = + new ArrayList<>(); + + for (final World bukkitWorld : Bukkit.getWorlds()) { + final ServerLevel world = ((CraftWorld)bukkitWorld).getHandle(); + world.regioniser.computeForAllRegions(regions::add); + } + + final double minTps; + final double medianTps; + final double maxTps; + double totalUtil = 0.0; + + final DoubleArrayList tpsByRegion = new DoubleArrayList(); + final List reportsByRegion = new ArrayList<>(); + final int maxThreadCount = TickRegions.getScheduler().getTotalThreadCount(); + + final long currTime = System.nanoTime(); + final TickData.TickReportData globalTickReport = RegionizedServer.getGlobalTickData().getTickReport15s(currTime); + + for (final ThreadedRegionizer.ThreadedRegion region : regions) { + final TickData.TickReportData report = region.getData().getRegionSchedulingHandle().getTickReport15s(currTime); + tpsByRegion.add(report == null ? 20.0 : report.tpsData().segmentAll().average()); + reportsByRegion.add(report); + totalUtil += (report == null ? 0.0 : report.utilisation()); + } + + totalUtil += globalTickReport.utilisation(); + + tpsByRegion.sort(null); + if (!tpsByRegion.isEmpty()) { + minTps = tpsByRegion.getDouble(0); + maxTps = tpsByRegion.getDouble(tpsByRegion.size() - 1); + + final int middle = tpsByRegion.size() >> 1; + if ((tpsByRegion.size() & 1) == 0) { + // even, average the two middle points + medianTps = (tpsByRegion.getDouble(middle - 1) + tpsByRegion.getDouble(middle)) / 2.0; + } else { + // odd, can just grab middle + medianTps = tpsByRegion.getDouble(middle); + } + } else { + // no regions = green + minTps = medianTps = maxTps = 20.0; + } + + final List, TickData.TickReportData>> + regionsBelowThreshold = new ArrayList<>(); + + for (int i = 0, len = regions.size(); i < len; ++i) { + final TickData.TickReportData report = reportsByRegion.get(i); + + regionsBelowThreshold.add(new ObjectObjectImmutablePair<>(regions.get(i), report)); + } + + regionsBelowThreshold.sort((p1, p2) -> { + final TickData.TickReportData report1 = p1.right(); + final TickData.TickReportData report2 = p2.right(); + final double util1 = report1 == null ? 0.0 : report1.utilisation(); + final double util2 = report2 == null ? 0.0 : report2.utilisation(); + + // we want the largest first + return Double.compare(util2, util1); + }); + + final TextComponent.Builder lowestRegionsBuilder = Component.text(); + + if (sender instanceof Player) { + lowestRegionsBuilder.append(Component.text(" Click to teleport\n", SECONDARY)); + } + for (int i = 0, len = Math.min(lowestRegionsCount, regionsBelowThreshold.size()); i < len; ++i) { + final ObjectObjectImmutablePair, TickData.TickReportData> + pair = regionsBelowThreshold.get(i); + + final TickData.TickReportData report = pair.right(); + final ThreadedRegionizer.ThreadedRegion region = + pair.left(); + + if (report == null) { + // skip regions with no data + continue; + } + + final ServerLevel world = region.regioniser.world; + final ChunkPos chunkCenter = region.getCenterChunk(); + if (chunkCenter == null) { + // region does not exist anymore + continue; + } + final int centerBlockX = ((chunkCenter.x << 4) | 7); + final int centerBlockZ = ((chunkCenter.z << 4) | 7); + final double util = report.utilisation(); + final double tps = report.tpsData().segmentAll().average(); + final double mspt = report.timePerTickData().segmentAll().average() / 1.0E6; + + final int yLoc = 80; + final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]"; + final Component line = Component.text() + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Region around block ", PRIMARY)) + .append(Component.text(location, INFORMATION)) + .append(Component.text(":\n", PRIMARY)) + + .append(Component.text(" ", PRIMARY)) + .append(Component.text(ONE_DECIMAL_PLACES.get().format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) + .append(Component.text("% util at ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(mspt), CommandUtil.getColourForMSPT(mspt))) + .append(Component.text(" MSPT at ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(tps), CommandUtil.getColourForTPS(tps))) + .append(Component.text(" TPS\n", PRIMARY)) + + .append(Component.text(" ", PRIMARY)) + .append(formatRegionStats(region.getData().getRegionStats(), (i + 1) != len)) + .build() + + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey().toString() + " run tp " + centerBlockX + ".5 " + yLoc + " " + centerBlockZ + ".5")) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("Click to teleport to " + location, SECONDARY))); + + lowestRegionsBuilder.append(line); + } + + sender.sendMessage( + Component.text() + .append(Component.text("Server Health Report\n", HEADER, TextDecoration.BOLD)) + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Online Players: ", PRIMARY)) + .append(Component.text(Bukkit.getOnlinePlayers().size() + "\n", INFORMATION)) + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Total regions: ", PRIMARY)) + .append(Component.text(regions.size() + "\n", INFORMATION)) + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Utilisation: ", PRIMARY)) + .append(Component.text(ONE_DECIMAL_PLACES.get().format(totalUtil * 100.0), CommandUtil.getUtilisationColourRegion(totalUtil / (double)maxThreadCount))) + .append(Component.text("% / ", PRIMARY)) + .append(Component.text(ONE_DECIMAL_PLACES.get().format(maxThreadCount * 100.0), INFORMATION)) + .append(Component.text("%\n", PRIMARY)) + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Lowest Region TPS: ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(minTps) + "\n", CommandUtil.getColourForTPS(minTps))) + + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Median Region TPS: ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(medianTps) + "\n", CommandUtil.getColourForTPS(medianTps))) + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Highest Region TPS: ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(maxTps) + "\n", CommandUtil.getColourForTPS(maxTps))) + + .append(Component.text("Highest ", HEADER, TextDecoration.BOLD)) + .append(Component.text(Integer.toString(lowestRegionsCount), INFORMATION, TextDecoration.BOLD)) + .append(Component.text(" utilisation regions\n", HEADER, TextDecoration.BOLD)) + + .append(lowestRegionsBuilder.build()) + .build() + ); + + return true; + } + + @Override + public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { + final String type; + if (args.length < 1) { + type = "server"; + } else { + type = args[0]; + } + + switch (type.toLowerCase(Locale.ROOT)) { + case "server": { + return executeServer(sender, commandLabel, args); + } + case "region": { + if (!(sender instanceof Entity)) { + sender.sendMessage(Component.text("Cannot see current region information as console", NamedTextColor.RED)); + return true; + } + return executeRegion(sender, commandLabel, args); + } + default: { + sender.sendMessage(Component.text("Type '" + args[0] + "' must be one of: [server, region]", NamedTextColor.RED)); + return true; + } + } + } + + @Override + public List tabComplete(final CommandSender sender, final String alias, final String[] args) throws IllegalArgumentException { + if (args.length == 0) { + if (sender instanceof Entity) { + return CommandUtil.getSortedList(Arrays.asList("server", "region")); + } else { + return CommandUtil.getSortedList(Arrays.asList("server")); + } + } else if (args.length == 1) { + if (sender instanceof Entity) { + return CommandUtil.getSortedList(Arrays.asList("server", "region"), args[0]); + } else { + return CommandUtil.getSortedList(Arrays.asList("server"), args[0]); + } + } + return new ArrayList<>(); + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/commands/CommandUtil.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..d016294fc7eafbddf6d2a758e5803498dfa207b8 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandUtil.java @@ -0,0 +1,121 @@ +package io.papermc.paper.threadedregions.commands; + +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.util.HSVLike; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public final class CommandUtil { + + public static List getSortedList(final Iterable iterable) { + final List ret = new ArrayList<>(); + for (final String val : iterable) { + ret.add(val); + } + + ret.sort(String.CASE_INSENSITIVE_ORDER); + + return ret; + } + + public static List getSortedList(final Iterable iterable, final String prefix) { + final List ret = new ArrayList<>(); + for (final String val : iterable) { + if (val.regionMatches(0, prefix, 0, prefix.length())) { + ret.add(val); + } + } + + ret.sort(String.CASE_INSENSITIVE_ORDER); + + return ret; + } + + public static List getSortedList(final Iterable iterable, final Function transform) { + final List ret = new ArrayList<>(); + for (final T val : iterable) { + final String transformed = transform.apply(val); + if (transformed != null) { + ret.add(transformed); + } + } + + ret.sort(String.CASE_INSENSITIVE_ORDER); + + return ret; + } + + public static List getSortedList(final Iterable iterable, final Function transform, final String prefix) { + final List ret = new ArrayList<>(); + for (final T val : iterable) { + final String string = transform.apply(val); + if (string != null && string.regionMatches(0, prefix, 0, prefix.length())) { + ret.add(string); + } + } + + ret.sort(String.CASE_INSENSITIVE_ORDER); + + return ret; + } + + public static TextColor getColourForTPS(final double tps) { + final double difference = Math.min(Math.abs(20.0 - tps), 20.0); + final double coordinate; + if (difference <= 2.0) { + // >= 18 tps + coordinate = 70.0 + ((140.0 - 70.0)/(0.0 - 2.0)) * (difference - 2.0); + } else if (difference <= 5.0) { + // >= 15 tps + coordinate = 30.0 + ((70.0 - 30.0)/(2.0 - 5.0)) * (difference - 5.0); + } else if (difference <= 10.0) { + // >= 10 tps + coordinate = 10.0 + ((30.0 - 10.0)/(5.0 - 10.0)) * (difference - 10.0); + } else { + // >= 0.0 tps + coordinate = 0.0 + ((10.0 - 0.0)/(10.0 - 20.0)) * (difference - 20.0); + } + + return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f)); + } + + public static TextColor getColourForMSPT(final double mspt) { + final double clamped = Math.min(Math.abs(mspt), 50.0); + final double coordinate; + if (clamped <= 15.0) { + coordinate = 130.0 + ((140.0 - 130.0)/(0.0 - 15.0)) * (clamped - 15.0); + } else if (clamped <= 25.0) { + coordinate = 90.0 + ((130.0 - 90.0)/(15.0 - 25.0)) * (clamped - 25.0); + } else if (clamped <= 35.0) { + coordinate = 30.0 + ((90.0 - 30.0)/(25.0 - 35.0)) * (clamped - 35.0); + } else if (clamped <= 40.0) { + coordinate = 15.0 + ((30.0 - 15.0)/(35.0 - 40.0)) * (clamped - 40.0); + } else { + coordinate = 0.0 + ((15.0 - 0.0)/(40.0 - 50.0)) * (clamped - 50.0); + } + + return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f)); + } + + public static TextColor getUtilisationColourRegion(final double util) { + // TODO anything better? + // assume 20TPS + return getColourForMSPT(util * 50.0); + } + + public static ServerPlayer getPlayer(final String name) { + for (final ServerPlayer player : MinecraftServer.getServer().getPlayerList().players) { + if (player.getGameProfile().getName().equalsIgnoreCase(name)) { + return player; + } + } + + return null; + } + + private CommandUtil() {} +} diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java new file mode 100644 index 0000000000000000000000000000000000000000..4874ca2ddf6b7bf7b818f97cfbc59d349a69f5ce --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java @@ -0,0 +1,327 @@ +package io.papermc.paper.threadedregions.scheduler; + +import ca.spottedleaf.concurrentutil.util.Validate; +import com.mojang.logging.LogUtils; +import org.bukkit.plugin.IllegalPluginAccessException; +import org.bukkit.plugin.Plugin; +import org.slf4j.Logger; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.logging.Level; + +public final class FoliaAsyncScheduler implements AsyncScheduler { + + private static final Logger LOGGER = LogUtils.getLogger(); + + private final Executor executors = new ThreadPoolExecutor(Math.max(4, Runtime.getRuntime().availableProcessors() / 2), Integer.MAX_VALUE, + 30L, TimeUnit.SECONDS, new SynchronousQueue<>(), + new ThreadFactory() { + private final AtomicInteger idGenerator = new AtomicInteger(); + + @Override + public Thread newThread(final Runnable run) { + final Thread ret = new Thread(run); + + ret.setName("Folia Async Scheduler Thread #" + this.idGenerator.getAndIncrement()); + ret.setPriority(Thread.NORM_PRIORITY - 1); + ret.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> { + LOGGER.error("Uncaught exception in thread: " + thread.getName(), thr); + }); + + return ret; + } + } + ); + + private final ScheduledExecutorService timerThread = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { + @Override + public Thread newThread(final Runnable run) { + final Thread ret = new Thread(run); + + ret.setName("Folia Async Scheduler Thread Timer"); + ret.setPriority(Thread.NORM_PRIORITY + 1); + ret.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> { + LOGGER.error("Uncaught exception in thread: " + thread.getName(), thr); + }); + + return ret; + } + }); + + private final Set tasks = ConcurrentHashMap.newKeySet(); + + @Override + public ScheduledTask runNow(final Plugin plugin, final Consumer task) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(task, "Task may not be null"); + + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); + } + + final AsyncScheduledTask ret = new AsyncScheduledTask(plugin, -1L, task, null, -1L); + + this.tasks.add(ret); + this.executors.execute(ret); + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + @Override + public ScheduledTask runDelayed(final Plugin plugin, final Consumer task, final long delay, + final TimeUnit unit) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(task, "Task may not be null"); + Validate.notNull(unit, "Time unit may not be null"); + if (delay < 0L) { + throw new IllegalArgumentException("Delay may not be < 0"); + } + + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); + } + + return this.scheduleTimerTask(plugin, task, delay, -1L, unit); + } + + @Override + public ScheduledTask runAtFixedRate(final Plugin plugin, final Consumer task, final long initialDelay, + final long period, final TimeUnit unit) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(task, "Task may not be null"); + Validate.notNull(unit, "Time unit may not be null"); + if (initialDelay < 0L) { + throw new IllegalArgumentException("Initial delay may not be < 0"); + } + if (period <= 0L) { + throw new IllegalArgumentException("Period may not be <= 0"); + } + + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); + } + + return this.scheduleTimerTask(plugin, task, initialDelay, period, unit); + } + + private AsyncScheduledTask scheduleTimerTask(final Plugin plugin, final Consumer task, final long initialDelay, + final long period, final TimeUnit unit) { + final AsyncScheduledTask ret = new AsyncScheduledTask( + plugin, period <= 0 ? period : unit.toNanos(period), task, null, + System.nanoTime() + unit.toNanos(initialDelay) + ); + + synchronized (ret) { + // even though ret is not published, we need to synchronise while scheduling to avoid a race condition + // for when a scheduled task immediately executes before we update the delay field and state field + ret.setDelay(this.timerThread.schedule(ret, initialDelay, unit)); + this.tasks.add(ret); + } + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + @Override + public void cancelTasks(final Plugin plugin) { + Validate.notNull(plugin, "Plugin may not be null"); + + for (final AsyncScheduledTask task : this.tasks) { + if (task.plugin == plugin) { + task.cancel(); + } + } + } + + private final class AsyncScheduledTask implements ScheduledTask, Runnable { + + private static final int STATE_ON_TIMER = 0; + private static final int STATE_SCHEDULED_EXECUTOR = 1; + private static final int STATE_EXECUTING = 2; + private static final int STATE_EXECUTING_CANCELLED = 3; + private static final int STATE_FINISHED = 4; + private static final int STATE_CANCELLED = 5; + + private final Plugin plugin; + private final long repeatDelay; // in ns + private Consumer run; + private ScheduledFuture delay; + private int state; + private long scheduleTarget; + + public AsyncScheduledTask(final Plugin plugin, final long repeatDelay, final Consumer run, + final ScheduledFuture delay, final long firstTarget) { + this.plugin = plugin; + this.repeatDelay = repeatDelay; + this.run = run; + this.delay = delay; + this.state = delay == null ? STATE_SCHEDULED_EXECUTOR : STATE_ON_TIMER; + this.scheduleTarget = firstTarget; + } + + private void setDelay(final ScheduledFuture delay) { + this.delay = delay; + this.state = STATE_SCHEDULED_EXECUTOR; + } + + @Override + public void run() { + final boolean repeating = this.isRepeatingTask(); + // try to advance state + final boolean timer; + synchronized (this) { + if (this.state == STATE_ON_TIMER) { + timer = true; + this.delay = null; + this.state = STATE_SCHEDULED_EXECUTOR; + } else if (this.state != STATE_SCHEDULED_EXECUTOR) { + // cancelled + if (this.state != STATE_CANCELLED) { + throw new IllegalStateException("Wrong state: " + this.state); + } + return; + } else { + timer = false; + this.state = STATE_EXECUTING; + } + } + + if (timer) { + // the scheduled executor is single thread, and unfortunately not expandable with threads + // so we just schedule onto the executor + FoliaAsyncScheduler.this.executors.execute(this); + return; + } + + try { + this.run.accept(this); + } catch (final Throwable throwable) { + this.plugin.getLogger().log(Level.WARNING, "Async task for " + this.plugin.getDescription().getFullName() + " generated an exception", throwable); + } finally { + boolean removeFromTasks = false; + synchronized (this) { + if (!repeating) { + // only want to execute once, so we're done + removeFromTasks = true; + this.state = STATE_FINISHED; + } else if (this.state != STATE_EXECUTING_CANCELLED) { + this.state = STATE_ON_TIMER; + // account for any delays, whether it be by task exec. or scheduler issues so that we keep + // the fixed schedule + final long currTime = System.nanoTime(); + final long delay = Math.max(0L, this.scheduleTarget + this.repeatDelay - currTime); + this.scheduleTarget = currTime + delay; + this.delay = FoliaAsyncScheduler.this.timerThread.schedule(this, delay, TimeUnit.NANOSECONDS); + } else { + // cancelled repeating task + removeFromTasks = true; + } + } + + if (removeFromTasks) { + this.run = null; + FoliaAsyncScheduler.this.tasks.remove(this); + } + } + } + + @Override + public Plugin getOwningPlugin() { + return this.plugin; + } + + @Override + public boolean isRepeatingTask() { + return this.repeatDelay > 0L; + } + + @Override + public CancelledState cancel() { + ScheduledFuture delay = null; + CancelledState ret; + synchronized (this) { + switch (this.state) { + case STATE_ON_TIMER: { + delay = this.delay; + this.delay = null; + this.state = STATE_CANCELLED; + ret = CancelledState.CANCELLED_BY_CALLER; + break; + } + case STATE_SCHEDULED_EXECUTOR: { + this.state = STATE_CANCELLED; + ret = CancelledState.CANCELLED_BY_CALLER; + break; + } + case STATE_EXECUTING: { + if (!this.isRepeatingTask()) { + return CancelledState.RUNNING; + } + this.state = STATE_EXECUTING_CANCELLED; + return CancelledState.NEXT_RUNS_CANCELLED; + } + case STATE_EXECUTING_CANCELLED: { + return CancelledState.NEXT_RUNS_CANCELLED_ALREADY; + } + case STATE_FINISHED: { + return CancelledState.ALREADY_EXECUTED; + } + case STATE_CANCELLED: { + return CancelledState.CANCELLED_ALREADY; + } + default: { + throw new IllegalStateException("Unknown state: " + this.state); + } + } + } + + if (delay != null) { + delay.cancel(false); + } + this.run = null; + FoliaAsyncScheduler.this.tasks.remove(this); + return ret; + } + + @Override + public ExecutionState getExecutionState() { + synchronized (this) { + switch (this.state) { + case STATE_ON_TIMER: + case STATE_SCHEDULED_EXECUTOR: + return ExecutionState.IDLE; + case STATE_EXECUTING: + return ExecutionState.RUNNING; + case STATE_EXECUTING_CANCELLED: + return ExecutionState.CANCELLED_RUNNING; + case STATE_FINISHED: + return ExecutionState.FINISHED; + case STATE_CANCELLED: + return ExecutionState.CANCELLED; + default: { + throw new IllegalStateException("Unknown state: " + this.state); + } + } + } + } + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java new file mode 100644 index 0000000000000000000000000000000000000000..2899d2fc3b9bd620ee838e24bcecc4587c9bd65c --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java @@ -0,0 +1,267 @@ +package io.papermc.paper.threadedregions.scheduler; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import net.minecraft.world.entity.Entity; +import org.bukkit.craftbukkit.entity.CraftEntity; +import org.bukkit.plugin.IllegalPluginAccessException; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.Nullable; +import java.lang.invoke.VarHandle; +import java.util.function.Consumer; +import java.util.logging.Level; + +public final class FoliaEntityScheduler implements EntityScheduler { + + private final CraftEntity entity; + + public FoliaEntityScheduler(final CraftEntity entity) { + this.entity = entity; + } + + private static Consumer wrap(final Plugin plugin, final Runnable runnable) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(runnable, "Runnable may not be null"); + + return (final net.minecraft.world.entity.Entity nmsEntity) -> { + if (!plugin.isEnabled()) { + // don't execute if the plugin is disabled + return; + } + try { + runnable.run(); + } catch (final Throwable throwable) { + plugin.getLogger().log(Level.WARNING, "Entity task for " + plugin.getDescription().getFullName() + " generated an exception", throwable); + } + }; + } + + @Override + public boolean execute(final Plugin plugin, final Runnable run, final Runnable retired, + final long delay) { + final Consumer runNMS = wrap(plugin, run); + final Consumer runRetired = retired == null ? null : wrap(plugin, retired); + + return this.entity.taskScheduler.schedule(runNMS, runRetired, delay); + } + + @Override + public @Nullable ScheduledTask run(final Plugin plugin, final Consumer task, final Runnable retired) { + return this.runDelayed(plugin, task, retired, 1); + } + + @Override + public @Nullable ScheduledTask runDelayed(final Plugin plugin, final Consumer task, final Runnable retired, + final long delayTicks) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(task, "Task may not be null"); + if (delayTicks <= 0) { + throw new IllegalArgumentException("Delay ticks may not be <= 0"); + } + + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); + } + + final EntityScheduledTask ret = new EntityScheduledTask(plugin, -1, task, retired); + + if (!this.scheduleInternal(ret, delayTicks)) { + return null; + } + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + @Override + public @Nullable ScheduledTask runAtFixedRate(final Plugin plugin, final Consumer task, + final Runnable retired, final long initialDelayTicks, final long periodTicks) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(task, "Task may not be null"); + if (initialDelayTicks <= 0) { + throw new IllegalArgumentException("Initial delay ticks may not be <= 0"); + } + if (periodTicks <= 0) { + throw new IllegalArgumentException("Period ticks may not be <= 0"); + } + + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); + } + + final EntityScheduledTask ret = new EntityScheduledTask(plugin, periodTicks, task, retired); + + if (!this.scheduleInternal(ret, initialDelayTicks)) { + return null; + } + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + private boolean scheduleInternal(final EntityScheduledTask ret, final long delay) { + return this.entity.taskScheduler.schedule(ret, ret, delay); + } + + private final class EntityScheduledTask implements ScheduledTask, Consumer { + + private static final int STATE_IDLE = 0; + private static final int STATE_EXECUTING = 1; + private static final int STATE_EXECUTING_CANCELLED = 2; + private static final int STATE_FINISHED = 3; + private static final int STATE_CANCELLED = 4; + + private final Plugin plugin; + private final long repeatDelay; // in ticks + private Consumer run; + private Runnable retired; + private volatile int state; + + private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(FoliaEntityScheduler.EntityScheduledTask.class, "state", int.class); + + private EntityScheduledTask(final Plugin plugin, final long repeatDelay, final Consumer run, final Runnable retired) { + this.plugin = plugin; + this.repeatDelay = repeatDelay; + this.run = run; + this.retired = retired; + } + + private final int getStateVolatile() { + return (int)STATE_HANDLE.get(this); + } + + private final int compareAndExchangeStateVolatile(final int expect, final int update) { + return (int)STATE_HANDLE.compareAndExchange(this, expect, update); + } + + private final void setStateVolatile(final int value) { + STATE_HANDLE.setVolatile(this, value); + } + + @Override + public void accept(final Entity entity) { + if (!this.plugin.isEnabled()) { + // don't execute if the plugin is disabled + this.setStateVolatile(STATE_CANCELLED); + return; + } + + final boolean repeating = this.isRepeatingTask(); + if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) { + // cancelled + return; + } + + final boolean retired = entity.isRemoved(); + + try { + if (!retired) { + this.run.accept(this); + } else { + if (this.retired != null) { + this.retired.run(); + } + } + } catch (final Throwable throwable) { + this.plugin.getLogger().log(Level.WARNING, "Entity task for " + this.plugin.getDescription().getFullName() + " generated an exception", throwable); + } finally { + boolean reschedule = false; + if (!repeating && !retired) { + this.setStateVolatile(STATE_FINISHED); + } else if (retired || !this.plugin.isEnabled()) { + this.setStateVolatile(STATE_CANCELLED); + } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) { + reschedule = true; + } // else: cancelled repeating task + + if (!reschedule) { + this.run = null; + this.retired = null; + } else { + if (!FoliaEntityScheduler.this.scheduleInternal(this, this.repeatDelay)) { + // the task itself must have removed the entity, so in this case we need to mark as cancelled + this.setStateVolatile(STATE_CANCELLED); + } + } + } + } + + @Override + public Plugin getOwningPlugin() { + return this.plugin; + } + + @Override + public boolean isRepeatingTask() { + return this.repeatDelay > 0; + } + + @Override + public CancelledState cancel() { + for (int curr = this.getStateVolatile();;) { + switch (curr) { + case STATE_IDLE: { + if (STATE_IDLE == (curr = this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_CANCELLED))) { + this.state = STATE_CANCELLED; + this.run = null; + this.retired = null; + return CancelledState.CANCELLED_BY_CALLER; + } + // try again + continue; + } + case STATE_EXECUTING: { + if (!this.isRepeatingTask()) { + return CancelledState.RUNNING; + } + if (STATE_EXECUTING == (curr = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_EXECUTING_CANCELLED))) { + return CancelledState.NEXT_RUNS_CANCELLED; + } + // try again + continue; + } + case STATE_EXECUTING_CANCELLED: { + return CancelledState.NEXT_RUNS_CANCELLED_ALREADY; + } + case STATE_FINISHED: { + return CancelledState.ALREADY_EXECUTED; + } + case STATE_CANCELLED: { + return CancelledState.CANCELLED_ALREADY; + } + default: { + throw new IllegalStateException("Unknown state: " + curr); + } + } + } + } + + @Override + public ExecutionState getExecutionState() { + final int state = this.getStateVolatile(); + switch (state) { + case STATE_IDLE: + return ExecutionState.IDLE; + case STATE_EXECUTING: + return ExecutionState.RUNNING; + case STATE_EXECUTING_CANCELLED: + return ExecutionState.CANCELLED_RUNNING; + case STATE_FINISHED: + return ExecutionState.FINISHED; + case STATE_CANCELLED: + return ExecutionState.CANCELLED; + default: { + throw new IllegalStateException("Unknown state: " + state); + } + } + } + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaGlobalRegionScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaGlobalRegionScheduler.java new file mode 100644 index 0000000000000000000000000000000000000000..eac8bc531dc11aa652bafd73e50d7930cbca19e5 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaGlobalRegionScheduler.java @@ -0,0 +1,266 @@ +package io.papermc.paper.threadedregions.scheduler; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import org.bukkit.plugin.IllegalPluginAccessException; +import org.bukkit.plugin.Plugin; +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.logging.Level; + +public final class FoliaGlobalRegionScheduler implements GlobalRegionScheduler { + + private long tickCount = 0L; + private final Object stateLock = new Object(); + private final Long2ObjectOpenHashMap> tasksByDeadline = new Long2ObjectOpenHashMap<>(); + + public void tick() { + final List run; + synchronized (this.stateLock) { + ++this.tickCount; + if (this.tasksByDeadline.isEmpty()) { + run = null; + } else { + run = this.tasksByDeadline.remove(this.tickCount); + } + } + + if (run == null) { + return; + } + + for (int i = 0, len = run.size(); i < len; ++i) { + run.get(i).run(); + } + } + + @Override + public void execute(final Plugin plugin, final Runnable run) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(run, "Runnable may not be null"); + + this.run(plugin, (final ScheduledTask task) -> { + run.run(); + }); + } + + @Override + public ScheduledTask run(final Plugin plugin, final Consumer task) { + return this.runDelayed(plugin, task, 1); + } + + @Override + public ScheduledTask runDelayed(final Plugin plugin, final Consumer task, final long delayTicks) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(task, "Task may not be null"); + if (delayTicks <= 0) { + throw new IllegalArgumentException("Delay ticks may not be <= 0"); + } + + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); + } + + final GlobalScheduledTask ret = new GlobalScheduledTask(plugin, -1, task); + + this.scheduleInternal(ret, delayTicks); + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + @Override + public ScheduledTask runAtFixedRate(final Plugin plugin, final Consumer task, final long initialDelayTicks, final long periodTicks) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(task, "Task may not be null"); + if (initialDelayTicks <= 0) { + throw new IllegalArgumentException("Initial delay ticks may not be <= 0"); + } + if (periodTicks <= 0) { + throw new IllegalArgumentException("Period ticks may not be <= 0"); + } + + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); + } + + final GlobalScheduledTask ret = new GlobalScheduledTask(plugin, periodTicks, task); + + this.scheduleInternal(ret, initialDelayTicks); + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + @Override + public void cancelTasks(final Plugin plugin) { + Validate.notNull(plugin, "Plugin may not be null"); + + final List toCancel = new ArrayList<>(); + synchronized (this.stateLock) { + for (final List tasks : this.tasksByDeadline.values()) { + for (int i = 0, len = tasks.size(); i < len; ++i) { + final GlobalScheduledTask task = tasks.get(i); + if (task.plugin == plugin) { + toCancel.add(task); + } + } + } + } + + for (int i = 0, len = toCancel.size(); i < len; ++i) { + toCancel.get(i).cancel(); + } + } + + private void scheduleInternal(final GlobalScheduledTask task, final long delay) { + // note: delay > 0 + synchronized (this.stateLock) { + this.tasksByDeadline.computeIfAbsent(this.tickCount + delay, (final long keyInMap) -> { + return new ArrayList<>(); + }).add(task); + } + } + + private final class GlobalScheduledTask implements ScheduledTask, Runnable { + + private static final int STATE_IDLE = 0; + private static final int STATE_EXECUTING = 1; + private static final int STATE_EXECUTING_CANCELLED = 2; + private static final int STATE_FINISHED = 3; + private static final int STATE_CANCELLED = 4; + + private final Plugin plugin; + private final long repeatDelay; // in ticks + private Consumer run; + private volatile int state; + + private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(GlobalScheduledTask.class, "state", int.class); + + private GlobalScheduledTask(final Plugin plugin, final long repeatDelay, final Consumer run) { + this.plugin = plugin; + this.repeatDelay = repeatDelay; + this.run = run; + } + + private final int getStateVolatile() { + return (int)STATE_HANDLE.get(this); + } + + private final int compareAndExchangeStateVolatile(final int expect, final int update) { + return (int)STATE_HANDLE.compareAndExchange(this, expect, update); + } + + private final void setStateVolatile(final int value) { + STATE_HANDLE.setVolatile(this, value); + } + + @Override + public void run() { + final boolean repeating = this.isRepeatingTask(); + if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) { + // cancelled + return; + } + + try { + this.run.accept(this); + } catch (final Throwable throwable) { + this.plugin.getLogger().log(Level.WARNING, "Global task for " + this.plugin.getDescription().getFullName() + " generated an exception", throwable); + } finally { + boolean reschedule = false; + if (!repeating) { + this.setStateVolatile(STATE_FINISHED); + } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) { + reschedule = true; + } // else: cancelled repeating task + + if (!reschedule) { + this.run = null; + } else { + FoliaGlobalRegionScheduler.this.scheduleInternal(this, this.repeatDelay); + } + } + } + + @Override + public Plugin getOwningPlugin() { + return this.plugin; + } + + @Override + public boolean isRepeatingTask() { + return this.repeatDelay > 0; + } + + @Override + public CancelledState cancel() { + for (int curr = this.getStateVolatile();;) { + switch (curr) { + case STATE_IDLE: { + if (STATE_IDLE == (curr = this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_CANCELLED))) { + this.state = STATE_CANCELLED; + this.run = null; + return CancelledState.CANCELLED_BY_CALLER; + } + // try again + continue; + } + case STATE_EXECUTING: { + if (!this.isRepeatingTask()) { + return CancelledState.RUNNING; + } + if (STATE_EXECUTING == (curr = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_EXECUTING_CANCELLED))) { + return CancelledState.NEXT_RUNS_CANCELLED; + } + // try again + continue; + } + case STATE_EXECUTING_CANCELLED: { + return CancelledState.NEXT_RUNS_CANCELLED_ALREADY; + } + case STATE_FINISHED: { + return CancelledState.ALREADY_EXECUTED; + } + case STATE_CANCELLED: { + return CancelledState.CANCELLED_ALREADY; + } + default: { + throw new IllegalStateException("Unknown state: " + curr); + } + } + } + } + + @Override + public ExecutionState getExecutionState() { + final int state = this.getStateVolatile(); + switch (state) { + case STATE_IDLE: + return ExecutionState.IDLE; + case STATE_EXECUTING: + return ExecutionState.RUNNING; + case STATE_EXECUTING_CANCELLED: + return ExecutionState.CANCELLED_RUNNING; + case STATE_FINISHED: + return ExecutionState.FINISHED; + case STATE_CANCELLED: + return ExecutionState.CANCELLED; + default: { + throw new IllegalStateException("Unknown state: " + state); + } + } + } + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java new file mode 100644 index 0000000000000000000000000000000000000000..aa10f3273e3bb35cf59d324644c269893cc12e99 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java @@ -0,0 +1,422 @@ +package io.papermc.paper.threadedregions.scheduler; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager; +import io.papermc.paper.threadedregions.RegionizedData; +import io.papermc.paper.threadedregions.RegionizedServer; +import io.papermc.paper.threadedregions.TickRegionScheduler; +import io.papermc.paper.threadedregions.TickRegions; +import io.papermc.paper.util.CoordinateUtils; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.util.Unit; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.plugin.IllegalPluginAccessException; +import org.bukkit.plugin.Plugin; +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; +import java.util.logging.Level; + +public final class FoliaRegionScheduler implements RegionScheduler { + + private static Runnable wrap(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) { + return () -> { + try { + run.run(); + } catch (final Throwable throwable) { + plugin.getLogger().log(Level.WARNING, "Location task for " + plugin.getDescription().getFullName() + + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable); + } + }; + } + + private static final RegionizedData SCHEDULER_DATA = new RegionizedData<>(null, Scheduler::new, Scheduler.REGIONISER_CALLBACK); + + private static void scheduleInternalOnRegion(final LocationScheduledTask task, final long delay) { + SCHEDULER_DATA.get().queueTask(task, delay); + } + + private static void scheduleInternalOffRegion(final LocationScheduledTask task, final long delay) { + final World world = task.world; + if (world == null) { + // cancelled + return; + } + + RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + ((CraftWorld) world).getHandle(), task.chunkX, task.chunkZ, () -> { + scheduleInternalOnRegion(task, delay); + } + ); + } + + @Override + public void execute(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(world, "World may not be null"); + Validate.notNull(run, "Runnable may not be null"); + + RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + ((CraftWorld) world).getHandle(), chunkX, chunkZ, wrap(plugin, world, chunkX, chunkZ, run) + ); + } + + @Override + public ScheduledTask run(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Consumer task) { + return this.runDelayed(plugin, world, chunkX, chunkZ, task, 1); + } + + @Override + public ScheduledTask runDelayed(final Plugin plugin, final World world, final int chunkX, final int chunkZ, + final Consumer task, final long delayTicks) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(world, "World may not be null"); + Validate.notNull(task, "Task may not be null"); + if (delayTicks <= 0) { + throw new IllegalArgumentException("Delay ticks may not be <= 0"); + } + + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); + } + + final LocationScheduledTask ret = new LocationScheduledTask(plugin, world, chunkX, chunkZ, -1, task); + + if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) { + scheduleInternalOnRegion(ret, delayTicks); + } else { + scheduleInternalOffRegion(ret, delayTicks); + } + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + @Override + public ScheduledTask runAtFixedRate(final Plugin plugin, final World world, final int chunkX, final int chunkZ, + final Consumer task, final long initialDelayTicks, final long periodTicks) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(world, "World may not be null"); + Validate.notNull(task, "Task may not be null"); + if (initialDelayTicks <= 0) { + throw new IllegalArgumentException("Initial delay ticks may not be <= 0"); + } + if (periodTicks <= 0) { + throw new IllegalArgumentException("Period ticks may not be <= 0"); + } + + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); + } + + final LocationScheduledTask ret = new LocationScheduledTask(plugin, world, chunkX, chunkZ, periodTicks, task); + + if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) { + scheduleInternalOnRegion(ret, initialDelayTicks); + } else { + scheduleInternalOffRegion(ret, initialDelayTicks); + } + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + public void tick() { + SCHEDULER_DATA.get().tick(); + } + + private static final class Scheduler { + private static final RegionizedData.RegioniserCallback REGIONISER_CALLBACK = new RegionizedData.RegioniserCallback<>() { + @Override + public void merge(final Scheduler from, final Scheduler into, final long fromTickOffset) { + for (final Iterator>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); + sectionIterator.hasNext();) { + final Long2ObjectMap.Entry>> entry = sectionIterator.next(); + final long sectionKey = entry.getLongKey(); + final Long2ObjectOpenHashMap> section = entry.getValue(); + + final Long2ObjectOpenHashMap> sectionAdjusted = new Long2ObjectOpenHashMap<>(section.size()); + + for (final Iterator>> iterator = section.long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry> e = iterator.next(); + final long newTick = e.getLongKey() + fromTickOffset; + final List tasks = e.getValue(); + + sectionAdjusted.put(newTick, tasks); + } + + into.tasksByDeadlineBySection.put(sectionKey, sectionAdjusted); + } + } + + @Override + public void split(final Scheduler from, final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData, + final ReferenceOpenHashSet dataSet) { + for (final Scheduler into : dataSet) { + into.tickCount = from.tickCount; + } + + for (final Iterator>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); + sectionIterator.hasNext();) { + final Long2ObjectMap.Entry>> entry = sectionIterator.next(); + final long sectionKey = entry.getLongKey(); + final Long2ObjectOpenHashMap> section = entry.getValue(); + + final Scheduler into = regionToData.get(sectionKey); + + into.tasksByDeadlineBySection.put(sectionKey, section); + } + } + }; + + private long tickCount = 0L; + // map of region section -> map of deadline -> list of tasks + private final Long2ObjectOpenHashMap>> tasksByDeadlineBySection = new Long2ObjectOpenHashMap<>(); + + private void addTicket(final int sectionX, final int sectionZ) { + final ServerLevel world = TickRegionScheduler.getCurrentRegionizedWorldData().world; + final int chunkX = sectionX << TickRegions.getRegionChunkShift(); + final int chunkZ = sectionZ << TickRegions.getRegionChunkShift(); + + world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel( + TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE + ); + } + + private void removeTicket(final long sectionKey) { + final ServerLevel world = TickRegionScheduler.getCurrentRegionizedWorldData().world; + final int chunkX = CoordinateUtils.getChunkX(sectionKey) << TickRegions.getRegionChunkShift(); + final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << TickRegions.getRegionChunkShift(); + + world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel( + TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE + ); + } + + private void queueTask(final LocationScheduledTask task, final long delay) { + // note: must be on the thread that owns this scheduler + // note: delay > 0 + + final World world = task.world; + if (world == null) { + // cancelled + return; + } + + final int sectionX = task.chunkX >> TickRegions.getRegionChunkShift(); + final int sectionZ = task.chunkZ >> TickRegions.getRegionChunkShift(); + + final Long2ObjectOpenHashMap> section = + this.tasksByDeadlineBySection.computeIfAbsent(CoordinateUtils.getChunkKey(sectionX, sectionZ), (final long keyInMap) -> { + return new Long2ObjectOpenHashMap<>(); + } + ); + + if (section.isEmpty()) { + // need to keep the scheduler loaded for this location in order for tick() to be called... + this.addTicket(sectionX, sectionZ); + } + + section.computeIfAbsent(this.tickCount + delay, (final long keyInMap) -> { + return new ArrayList<>(); + }).add(task); + } + + public void tick() { + ++this.tickCount; + + final List run = new ArrayList<>(); + + for (final Iterator>>> sectionIterator = this.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); + sectionIterator.hasNext();) { + final Long2ObjectMap.Entry>> entry = sectionIterator.next(); + final long sectionKey = entry.getLongKey(); + final Long2ObjectOpenHashMap> section = entry.getValue(); + + final List tasks = section.remove(this.tickCount); + + if (tasks == null) { + continue; + } + + run.addAll(tasks); + + if (section.isEmpty()) { + this.removeTicket(sectionKey); + sectionIterator.remove(); + } + } + + for (int i = 0, len = run.size(); i < len; ++i) { + run.get(i).run(); + } + } + } + + private static final class LocationScheduledTask implements ScheduledTask, Runnable { + + private static final int STATE_IDLE = 0; + private static final int STATE_EXECUTING = 1; + private static final int STATE_EXECUTING_CANCELLED = 2; + private static final int STATE_FINISHED = 3; + private static final int STATE_CANCELLED = 4; + + private final Plugin plugin; + private final int chunkX; + private final int chunkZ; + private final long repeatDelay; // in ticks + private World world; + private Consumer run; + + private volatile int state; + private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(LocationScheduledTask.class, "state", int.class); + + private LocationScheduledTask(final Plugin plugin, final World world, final int chunkX, final int chunkZ, + final long repeatDelay, final Consumer run) { + this.plugin = plugin; + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.repeatDelay = repeatDelay; + this.run = run; + } + + private final int getStateVolatile() { + return (int)STATE_HANDLE.get(this); + } + + private final int compareAndExchangeStateVolatile(final int expect, final int update) { + return (int)STATE_HANDLE.compareAndExchange(this, expect, update); + } + + private final void setStateVolatile(final int value) { + STATE_HANDLE.setVolatile(this, value); + } + + @Override + public void run() { + if (!this.plugin.isEnabled()) { + // don't execute if the plugin is disabled + return; + } + + final boolean repeating = this.isRepeatingTask(); + if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) { + // cancelled + return; + } + + try { + this.run.accept(this); + } catch (final Throwable throwable) { + this.plugin.getLogger().log(Level.WARNING, "Location task for " + this.plugin.getDescription().getFullName() + + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable); + } finally { + boolean reschedule = false; + if (!repeating) { + this.setStateVolatile(STATE_FINISHED); + } else if (!this.plugin.isEnabled()) { + this.setStateVolatile(STATE_CANCELLED); + } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) { + reschedule = true; + } // else: cancelled repeating task + + if (!reschedule) { + this.run = null; + this.world = null; + } else { + FoliaRegionScheduler.scheduleInternalOnRegion(this, this.repeatDelay); + } + } + } + + @Override + public Plugin getOwningPlugin() { + return this.plugin; + } + + @Override + public boolean isRepeatingTask() { + return this.repeatDelay > 0; + } + + @Override + public CancelledState cancel() { + for (int curr = this.getStateVolatile();;) { + switch (curr) { + case STATE_IDLE: { + if (STATE_IDLE == (curr = this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_CANCELLED))) { + this.state = STATE_CANCELLED; + this.run = null; + this.world = null; + return CancelledState.CANCELLED_BY_CALLER; + } + // try again + continue; + } + case STATE_EXECUTING: { + if (!this.isRepeatingTask()) { + return CancelledState.RUNNING; + } + if (STATE_EXECUTING == (curr = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_EXECUTING_CANCELLED))) { + return CancelledState.NEXT_RUNS_CANCELLED; + } + // try again + continue; + } + case STATE_EXECUTING_CANCELLED: { + return CancelledState.NEXT_RUNS_CANCELLED_ALREADY; + } + case STATE_FINISHED: { + return CancelledState.ALREADY_EXECUTED; + } + case STATE_CANCELLED: { + return CancelledState.CANCELLED_ALREADY; + } + default: { + throw new IllegalStateException("Unknown state: " + curr); + } + } + } + } + + @Override + public ExecutionState getExecutionState() { + final int state = this.getStateVolatile(); + switch (state) { + case STATE_IDLE: + return ExecutionState.IDLE; + case STATE_EXECUTING: + return ExecutionState.RUNNING; + case STATE_EXECUTING_CANCELLED: + return ExecutionState.CANCELLED_RUNNING; + case STATE_FINISHED: + return ExecutionState.FINISHED; + case STATE_CANCELLED: + return ExecutionState.CANCELLED; + default: { + throw new IllegalStateException("Unknown state: " + state); + } + } + } + } +} diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java index e08f4e39db4ee3fed62e37364d17dcc5c5683504..7e92e14cdb9f3d895550991b2ea154a60f9c091b 100644 --- a/src/main/java/io/papermc/paper/util/CachedLists.java +++ b/src/main/java/io/papermc/paper/util/CachedLists.java @@ -9,49 +9,57 @@ import java.util.List; public final class CachedLists { // Paper start - optimise collisions - static final UnsafeList TEMP_COLLISION_LIST = new UnsafeList<>(1024); - static boolean tempCollisionListInUse; + // Folia - region threading public static UnsafeList getTempCollisionList() { - if (!Bukkit.isPrimaryThread() || tempCollisionListInUse) { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); + if (worldData == null) { return new UnsafeList<>(16); } - tempCollisionListInUse = true; - return TEMP_COLLISION_LIST; + return worldData.tempCollisionList.get(); + // Folia end - region threading } public static void returnTempCollisionList(List list) { - if (list != TEMP_COLLISION_LIST) { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); + if (worldData == null) { return; } - ((UnsafeList)list).setSize(0); - tempCollisionListInUse = false; + worldData.tempCollisionList.ret(list); + // Folia end - region threading } - static final UnsafeList TEMP_GET_ENTITIES_LIST = new UnsafeList<>(1024); - static boolean tempGetEntitiesListInUse; + // Folia - region threading public static UnsafeList getTempGetEntitiesList() { - if (!Bukkit.isPrimaryThread() || tempGetEntitiesListInUse) { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); + if (worldData == null) { return new UnsafeList<>(16); } - tempGetEntitiesListInUse = true; - return TEMP_GET_ENTITIES_LIST; + return worldData.tempEntitiesList.get(); + // Folia end - region threading } public static void returnTempGetEntitiesList(List list) { - if (list != TEMP_GET_ENTITIES_LIST) { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); + if (worldData == null) { return; } - ((UnsafeList)list).setSize(0); - tempGetEntitiesListInUse = false; + worldData.tempEntitiesList.ret(list); + // Folia end - region threading } // Paper end - optimise collisions public static void reset() { - // Paper start - optimise collisions - TEMP_COLLISION_LIST.completeReset(); - TEMP_GET_ENTITIES_LIST.completeReset(); - // Paper end - optimise collisions + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); + if (worldData != null) { + worldData.resetCollisionLists(); + } + // Folia end - region threading } } diff --git a/src/main/java/io/papermc/paper/util/CollisionUtil.java b/src/main/java/io/papermc/paper/util/CollisionUtil.java index a87f6380b2c387fb0cdd40d5087b5c93492e3c88..77d369ca6be814d1f5a19ec37b44800529e6cda3 100644 --- a/src/main/java/io/papermc/paper/util/CollisionUtil.java +++ b/src/main/java/io/papermc/paper/util/CollisionUtil.java @@ -480,7 +480,12 @@ public final class CollisionUtil { if (chunkProvider == null) { chunk = (ChunkAccess)getter.getChunkForCollisions(currChunkX, currChunkZ); } else { + // Folia start - ignore chunk if we do not own the region + if (!io.papermc.paper.util.TickThread.isTickThreadFor(chunkProvider.chunkMap.level, currChunkX, currChunkZ)) { + chunk = null; + } else { // Folia end - ignore chunk if we do not own the region chunk = loadChunks ? chunkProvider.getChunk(currChunkX, currChunkZ, true) : chunkProvider.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ); + } // Folia - ignore chunk if we do not own the region } if (chunk == null) { diff --git a/src/main/java/io/papermc/paper/util/CoordinateUtils.java b/src/main/java/io/papermc/paper/util/CoordinateUtils.java index 413e4b6da027876dbbe8eb78f2568a440f431547..3a7dbcb9964723b8ed5e6b0a1ee4267923c746e4 100644 --- a/src/main/java/io/papermc/paper/util/CoordinateUtils.java +++ b/src/main/java/io/papermc/paper/util/CoordinateUtils.java @@ -5,6 +5,7 @@ import net.minecraft.core.SectionPos; import net.minecraft.util.Mth; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.phys.Vec3; public final class CoordinateUtils { @@ -122,6 +123,31 @@ public final class CoordinateUtils { return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54); } + // TODO rebase + public static int getBlockX(final Vec3 pos) { + return Mth.floor(pos.x); + } + + public static int getBlockY(final Vec3 pos) { + return Mth.floor(pos.y); + } + + public static int getBlockZ(final Vec3 pos) { + return Mth.floor(pos.z); + } + + public static int getChunkX(final Vec3 pos) { + return Mth.floor(pos.x) >> 4; + } + + public static int getChunkY(final Vec3 pos) { + return Mth.floor(pos.y) >> 4; + } + + public static int getChunkZ(final Vec3 pos) { + return Mth.floor(pos.z) >> 4; + } + private CoordinateUtils() { throw new RuntimeException(); } diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java index c856a9a0d085b278da416c59996fc131811f790c..915cbf8c02c4bba0c62e5589229ee27e30535c07 100644 --- a/src/main/java/io/papermc/paper/util/MCUtil.java +++ b/src/main/java/io/papermc/paper/util/MCUtil.java @@ -29,6 +29,7 @@ import net.minecraft.world.level.ClipContext; import net.minecraft.world.level.Level; import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.phys.Vec3; import org.apache.commons.lang.exception.ExceptionUtils; import com.mojang.authlib.GameProfile; import org.bukkit.Location; @@ -333,6 +334,7 @@ public final class MCUtil { */ public static void ensureMain(String reason, Runnable run) { if (!isMainThread()) { + if (true) throw new UnsupportedOperationException(); // Folia - region threading if (reason != null) { MinecraftServer.LOGGER.warn("Asynchronous " + reason + "!", new IllegalStateException()); } @@ -473,6 +475,30 @@ public final class MCUtil { return new Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ()); } + // Folia start - TODO MERGE INTO MCUTIL + /** + * Converts a NMS World/Vector to Bukkit Location + * @param world + * @param pos + * @return + */ + public static Location toLocation(Level world, Vec3 pos) { + return new Location(world.getWorld(), pos.x(), pos.y(), pos.z()); + } + + /** + * Converts a NMS World/Vector to Bukkit Location + * @param world + * @param pos + * @param yaw + * @param pitch + * @return + */ + public static Location toLocation(Level world, Vec3 pos, float yaw, float pitch) { + return new Location(world.getWorld(), pos.x(), pos.y(), pos.z(), yaw, pitch); + } + // Folia end - TODO MERGE INTO MCUTIL + /** * Converts an NMS entity's current location to a Bukkit Location * @param entity diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java index fc57850b80303fcade89ca95794f63910404a407..294ea54d59e1e0b00f67d1623a5c807f9d368df6 100644 --- a/src/main/java/io/papermc/paper/util/TickThread.java +++ b/src/main/java/io/papermc/paper/util/TickThread.java @@ -1,8 +1,22 @@ package io.papermc.paper.util; +import io.papermc.paper.threadedregions.RegionShutdownThread; +import io.papermc.paper.threadedregions.RegionizedServer; +import io.papermc.paper.threadedregions.RegionizedWorldData; +import io.papermc.paper.threadedregions.ThreadedRegionizer; +import io.papermc.paper.threadedregions.TickRegionScheduler; +import io.papermc.paper.threadedregions.TickRegions; +import net.minecraft.core.BlockPos; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.util.Mth; import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; import org.bukkit.Bukkit; import java.util.concurrent.atomic.AtomicInteger; @@ -38,6 +52,20 @@ public class TickThread extends Thread { } } + public static void ensureTickThread(final ServerLevel world, final BlockPos pos, final String reason) { + if (!isTickThreadFor(world, pos)) { + MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); + throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final ServerLevel world, final ChunkPos pos, final String reason) { + if (!isTickThreadFor(world, pos)) { + MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); + throw new IllegalStateException(reason); + } + } + public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) { if (!isTickThreadFor(world, chunkX, chunkZ)) { MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); @@ -52,6 +80,20 @@ public class TickThread extends Thread { } } + public static void ensureTickThread(final ServerLevel world, final AABB aabb, final String reason) { + if (!isTickThreadFor(world, aabb)) { + MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); + throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final ServerLevel world, final double blockX, final double blockZ, final String reason) { + if (!isTickThreadFor(world, blockX, blockZ)) { + MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); + throw new IllegalStateException(reason); + } + } + public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */ private static final AtomicInteger ID_GENERATOR = new AtomicInteger(); @@ -77,11 +119,126 @@ public class TickThread extends Thread { return Thread.currentThread() instanceof TickThread; } + public static boolean isShutdownThread() { + return Thread.currentThread().getClass() == RegionShutdownThread.class; + } + + public static boolean isTickThreadFor(final ServerLevel world, final BlockPos pos) { + return isTickThreadFor(world, pos.getX() >> 4, pos.getZ() >> 4); + } + + public static boolean isTickThreadFor(final ServerLevel world, final ChunkPos pos) { + return isTickThreadFor(world, pos.x, pos.z); + } + + public static boolean isTickThreadFor(final ServerLevel world, final Vec3 pos) { + return isTickThreadFor(world, Mth.floor(pos.x) >> 4, Mth.floor(pos.z) >> 4); + } + public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) { - return Thread.currentThread() instanceof TickThread; + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + if (region == null) { + return isShutdownThread(); + } + return world.regioniser.getRegionAtUnsynchronised(chunkX, chunkZ) == region; + } + + public static boolean isTickThreadFor(final ServerLevel world, final AABB aabb) { + return isTickThreadFor( + world, + CoordinateUtils.getChunkCoordinate(aabb.minX), CoordinateUtils.getChunkCoordinate(aabb.minZ), + CoordinateUtils.getChunkCoordinate(aabb.maxX), CoordinateUtils.getChunkCoordinate(aabb.maxZ) + ); + } + + public static boolean isTickThreadFor(final ServerLevel world, final double blockX, final double blockZ) { + return isTickThreadFor(world, CoordinateUtils.getChunkCoordinate(blockX), CoordinateUtils.getChunkCoordinate(blockZ)); + } + + public static boolean isTickThreadFor(final ServerLevel world, final Vec3 position, final Vec3 deltaMovement, final int buffer) { + final int fromChunkX = CoordinateUtils.getChunkX(position); + final int fromChunkZ = CoordinateUtils.getChunkZ(position); + + final int toChunkX = CoordinateUtils.getChunkCoordinate(position.x + deltaMovement.x); + final int toChunkZ = CoordinateUtils.getChunkCoordinate(position.z + deltaMovement.z); + + // expect from < to, but that may not be the case + return isTickThreadFor( + world, + Math.min(fromChunkX, toChunkX) - buffer, + Math.min(fromChunkZ, toChunkZ) - buffer, + Math.max(fromChunkX, toChunkX) + buffer, + Math.max(fromChunkZ, toChunkZ) + buffer + ); + } + + public static boolean isTickThreadFor(final ServerLevel world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) { + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + if (region == null) { + return isShutdownThread(); + } + + final int shift = world.regioniser.sectionChunkShift; + + final int minSectionX = fromChunkX >> shift; + final int maxSectionX = toChunkX >> shift; + final int minSectionZ = fromChunkZ >> shift; + final int maxSectionZ = toChunkZ >> shift; + + for (int secZ = minSectionZ; secZ <= maxSectionZ; ++secZ) { + for (int secX = minSectionX; secX <= maxSectionX; ++secX) { + final int lowerLeftCX = secX << shift; + final int lowerLeftCZ = secZ << shift; + if (world.regioniser.getRegionAtUnsynchronised(lowerLeftCX, lowerLeftCZ) != region) { + return false; + } + } + } + + return true; + } + + public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) { + return isTickThreadFor(world, chunkX - radius, chunkZ - radius, chunkX + radius, chunkZ + radius); } public static boolean isTickThreadFor(final Entity entity) { - return Thread.currentThread() instanceof TickThread; + if (entity == null) { + return true; + } + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + if (region == null) { + if (RegionizedServer.isGlobalTickThread()) { + if (entity instanceof ServerPlayer serverPlayer) { + return serverPlayer.connection == null; + } else { + return false; + } + } + return isShutdownThread(); + } + + final Level level = entity.level; + if (level != region.regioniser.world) { + // world mismatch + return false; + } + + final RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); + + // pass through the check if the entity is removed and we own its chunk + if (worldData.hasEntity(entity)) { + return true; + } + + if (entity instanceof ServerPlayer serverPlayer) { + ServerGamePacketListenerImpl conn = serverPlayer.connection; + return conn != null && worldData.connections.contains(conn.connection); + } else { + return ((entity.hasNullCallback() || entity.isRemoved())) && isTickThreadFor((ServerLevel)level, entity.chunkPosition()); + } } } diff --git a/src/main/java/io/papermc/paper/util/set/LinkedSortedSet.java b/src/main/java/io/papermc/paper/util/set/LinkedSortedSet.java new file mode 100644 index 0000000000000000000000000000000000000000..cf9b66afc1762dbe2c625f09f9e804ca7dc0f128 --- /dev/null +++ b/src/main/java/io/papermc/paper/util/set/LinkedSortedSet.java @@ -0,0 +1,273 @@ +package io.papermc.paper.util.set; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.NoSuchElementException; + +// TODO rebase into util patch +public final class LinkedSortedSet implements Iterable { + + public final Comparator comparator; + + protected Link head; + protected Link tail; + + public LinkedSortedSet() { + this((Comparator)Comparator.naturalOrder()); + } + + public LinkedSortedSet(final Comparator comparator) { + this.comparator = comparator; + } + + public void clear() { + this.head = this.tail = null; + } + + public boolean isEmpty() { + return this.head == null; + } + + public E first() { + final Link head = this.head; + return head == null ? null : head.element; + } + + public E last() { + final Link tail = this.tail; + return tail == null ? null : tail.element; + } + + public boolean containsFirst(final E element) { + final Comparator comparator = this.comparator; + for (Link curr = this.head; curr != null; curr = curr.next) { + if (comparator.compare(element, curr.element) == 0) { + return true; + } + } + return false; + } + + public boolean containsLast(final E element) { + final Comparator comparator = this.comparator; + for (Link curr = this.tail; curr != null; curr = curr.prev) { + if (comparator.compare(element, curr.element) == 0) { + return true; + } + } + return false; + } + + private void removeNode(final Link node) { + final Link prev = node.prev; + final Link next = node.next; + + // help GC + node.element = null; + node.prev = null; + node.next = null; + + if (prev == null) { + this.head = next; + } else { + prev.next = next; + } + + if (next == null) { + this.tail = prev; + } else { + next.prev = prev; + } + } + + public boolean remove(final Link link) { + if (link.element == null) { + return false; + } + + this.removeNode(link); + return true; + } + + public boolean removeFirst(final E element) { + final Comparator comparator = this.comparator; + for (Link curr = this.head; curr != null; curr = curr.next) { + if (comparator.compare(element, curr.element) == 0) { + this.removeNode(curr); + return true; + } + } + return false; + } + + public boolean removeLast(final E element) { + final Comparator comparator = this.comparator; + for (Link curr = this.tail; curr != null; curr = curr.prev) { + if (comparator.compare(element, curr.element) == 0) { + this.removeNode(curr); + return true; + } + } + return false; + } + + @Override + public Iterator iterator() { + return new Iterator<>() { + private Link next = LinkedSortedSet.this.head; + + @Override + public boolean hasNext() { + return this.next != null; + } + + @Override + public E next() { + final Link next = this.next; + if (next == null) { + throw new NoSuchElementException(); + } + this.next = next.next; + return next.element; + } + }; + } + + public E pollFirst() { + final Link head = this.head; + if (head == null) { + return null; + } + + final E ret = head.element; + final Link next = head.next; + + // unlink head + this.head = next; + if (next == null) { + this.tail = null; + } else { + next.prev = null; + } + + // help GC + head.element = null; + head.next = null; + + return ret; + } + + public E pollLast() { + final Link tail = this.tail; + if (tail == null) { + return null; + } + + final E ret = tail.element; + final Link prev = tail.prev; + + // unlink tail + this.tail = prev; + if (prev == null) { + this.head = null; + } else { + prev.next = null; + } + + // help GC + tail.element = null; + tail.prev = null; + + return ret; + } + + public Link addLast(final E element) { + final Comparator comparator = this.comparator; + + Link curr = this.tail; + if (curr != null) { + int compare; + + while ((compare = comparator.compare(element, curr.element)) < 0) { + Link prev = curr; + curr = curr.prev; + if (curr != null) { + continue; + } + return this.head = prev.prev = new Link<>(element, null, prev); + } + + if (compare != 0) { + // insert after curr + final Link next = curr.next; + final Link insert = new Link<>(element, curr, next); + curr.next = insert; + + if (next == null) { + this.tail = insert; + } else { + next.prev = insert; + } + return insert; + } + + return null; + } else { + return this.head = this.tail = new Link<>(element); + } + } + + public Link addFirst(final E element) { + final Comparator comparator = this.comparator; + + Link curr = this.head; + if (curr != null) { + int compare; + + while ((compare = comparator.compare(element, curr.element)) > 0) { + Link prev = curr; + curr = curr.next; + if (curr != null) { + continue; + } + return this.tail = prev.next = new Link<>(element, prev, null); + } + + if (compare != 0) { + // insert before curr + final Link prev = curr.prev; + final Link insert = new Link<>(element, prev, curr); + curr.prev = insert; + + if (prev == null) { + this.head = insert; + } else { + prev.next = insert; + } + return insert; + } + + return null; + } else { + return this.head = this.tail = new Link<>(element); + } + } + + public static final class Link { + private E element; + private Link prev; + private Link next; + + private Link() {} + + private Link(final E element) { + this.element = element; + } + + private Link(final E element, final Link prev, final Link next) { + this.element = element; + this.prev = prev; + this.next = next; + } + } +} diff --git a/src/main/java/net/minecraft/advancements/CriterionProgress.java b/src/main/java/net/minecraft/advancements/CriterionProgress.java index f40d6eaa6ebbd775cd3feb41546423fe4cbf2b22..e43c95e3be6cb41eab0a1cecbf154350e70b7b79 100644 --- a/src/main/java/net/minecraft/advancements/CriterionProgress.java +++ b/src/main/java/net/minecraft/advancements/CriterionProgress.java @@ -12,7 +12,7 @@ import javax.annotation.Nullable; import net.minecraft.network.FriendlyByteBuf; public class CriterionProgress { - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT); + private static final ThreadLocal DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT)); // Folia - region threading - SDF is not thread-safe @Nullable private Date obtained; @@ -43,7 +43,7 @@ public class CriterionProgress { } public JsonElement serializeToJson() { - return (JsonElement)(this.obtained != null ? new JsonPrimitive(DATE_FORMAT.format(this.obtained)) : JsonNull.INSTANCE); + return (JsonElement)(this.obtained != null ? new JsonPrimitive(DATE_FORMAT.get().format(this.obtained)) : JsonNull.INSTANCE); // Folia - region threading - SDF is not thread-safe } public static CriterionProgress fromNetwork(FriendlyByteBuf buf) { @@ -56,7 +56,7 @@ public class CriterionProgress { CriterionProgress criterionProgress = new CriterionProgress(); try { - criterionProgress.obtained = DATE_FORMAT.parse(datetime); + criterionProgress.obtained = DATE_FORMAT.get().parse(datetime); // Folia - region threading - SDF is not thread-safe return criterionProgress; } catch (ParseException var3) { throw new JsonSyntaxException("Invalid datetime: " + datetime, var3); diff --git a/src/main/java/net/minecraft/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java index 7b6b51392b123d34382233adcf4c3d4867bdaa32..4c793605566f1755c99496c00dc1ef4f38c4ac7d 100644 --- a/src/main/java/net/minecraft/commands/CommandSourceStack.java +++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java @@ -66,7 +66,7 @@ public class CommandSourceStack implements SharedSuggestionProvider, com.destroy public CommandSourceStack(CommandSource output, Vec3 pos, Vec2 rot, ServerLevel world, int level, String name, Component displayName, MinecraftServer server, @Nullable Entity entity) { this(output, pos, rot, world, level, name, displayName, server, entity, false, (commandcontext, flag, j) -> { - }, EntityAnchorArgument.Anchor.FEET, CommandSigningContext.ANONYMOUS, TaskChainer.immediate(server)); + }, EntityAnchorArgument.Anchor.FEET, CommandSigningContext.ANONYMOUS, TaskChainer.immediate((Runnable run) -> { io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run);})); // Folia - region threading } protected CommandSourceStack(CommandSource output, Vec3 pos, Vec2 rot, ServerLevel world, int level, String name, Component displayName, MinecraftServer server, @Nullable Entity entity, boolean silent, @Nullable ResultConsumer consumer, EntityAnchorArgument.Anchor entityAnchor, CommandSigningContext signedArguments, TaskChainer messageChainTaskQueue) { diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java index 87cc7562e4a166d078fe11b7f6980497fc0bd33e..e970a9807f1497b8b9f4155f558f4fcb719cb368 100644 --- a/src/main/java/net/minecraft/commands/Commands.java +++ b/src/main/java/net/minecraft/commands/Commands.java @@ -143,13 +143,13 @@ public class Commands { AdvancementCommands.register(this.dispatcher); AttributeCommand.register(this.dispatcher, commandRegistryAccess); ExecuteCommand.register(this.dispatcher, commandRegistryAccess); - BossBarCommands.register(this.dispatcher); + //BossBarCommands.register(this.dispatcher); // Folia - region threading - TODO ClearInventoryCommands.register(this.dispatcher, commandRegistryAccess); - CloneCommands.register(this.dispatcher, commandRegistryAccess); + //CloneCommands.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO DamageCommand.register(this.dispatcher, commandRegistryAccess); - DataCommands.register(this.dispatcher); - DataPackCommand.register(this.dispatcher); - DebugCommand.register(this.dispatcher); + //DataCommands.register(this.dispatcher); // Folia - region threading - TODO + //DataPackCommand.register(this.dispatcher); // Folia - region threading - TODO + //DebugCommand.register(this.dispatcher); // Folia - region threading - TODO DefaultGameModeCommands.register(this.dispatcher); DifficultyCommand.register(this.dispatcher); EffectCommands.register(this.dispatcher, commandRegistryAccess); @@ -159,45 +159,45 @@ public class Commands { FillCommand.register(this.dispatcher, commandRegistryAccess); FillBiomeCommand.register(this.dispatcher, commandRegistryAccess); ForceLoadCommand.register(this.dispatcher); - FunctionCommand.register(this.dispatcher); + //FunctionCommand.register(this.dispatcher); // Folia - region threading - TODO GameModeCommand.register(this.dispatcher); GameRuleCommand.register(this.dispatcher); GiveCommand.register(this.dispatcher, commandRegistryAccess); HelpCommand.register(this.dispatcher); - ItemCommands.register(this.dispatcher, commandRegistryAccess); + //ItemCommands.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO later KickCommand.register(this.dispatcher); KillCommand.register(this.dispatcher); ListPlayersCommand.register(this.dispatcher); LocateCommand.register(this.dispatcher, commandRegistryAccess); - LootCommand.register(this.dispatcher, commandRegistryAccess); + //LootCommand.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO later MsgCommand.register(this.dispatcher); ParticleCommand.register(this.dispatcher, commandRegistryAccess); PlaceCommand.register(this.dispatcher); PlaySoundCommand.register(this.dispatcher); - ReloadCommand.register(this.dispatcher); + //ReloadCommand.register(this.dispatcher); // Folia - region threading RecipeCommand.register(this.dispatcher); - RideCommand.register(this.dispatcher); + //RideCommand.register(this.dispatcher); // Folia - region threading - TODO later SayCommand.register(this.dispatcher); - ScheduleCommand.register(this.dispatcher); - ScoreboardCommand.register(this.dispatcher); + //ScheduleCommand.register(this.dispatcher); // Folia - region threading + //ScoreboardCommand.register(this.dispatcher); // Folia - region threading - TODO later SeedCommand.register(this.dispatcher, environment != Commands.CommandSelection.INTEGRATED); SetBlockCommand.register(this.dispatcher, commandRegistryAccess); SetSpawnCommand.register(this.dispatcher); SetWorldSpawnCommand.register(this.dispatcher); - SpectateCommand.register(this.dispatcher); - SpreadPlayersCommand.register(this.dispatcher); + //SpectateCommand.register(this.dispatcher); // Folia - region threading - TODO later + //SpreadPlayersCommand.register(this.dispatcher); // Folia - region threading - TODO later StopSoundCommand.register(this.dispatcher); SummonCommand.register(this.dispatcher, commandRegistryAccess); - TagCommand.register(this.dispatcher); - TeamCommand.register(this.dispatcher); - TeamMsgCommand.register(this.dispatcher); + //TagCommand.register(this.dispatcher); // Folia - region threading - TODO later + //TeamCommand.register(this.dispatcher); // Folia - region threading - TODO later + //TeamMsgCommand.register(this.dispatcher); // Folia - region threading - TODO later TeleportCommand.register(this.dispatcher); TellRawCommand.register(this.dispatcher); TimeCommand.register(this.dispatcher); TitleCommand.register(this.dispatcher); - TriggerCommand.register(this.dispatcher); + //TriggerCommand.register(this.dispatcher); // Folia - region threading - TODO later WeatherCommand.register(this.dispatcher); - WorldBorderCommand.register(this.dispatcher); + //WorldBorderCommand.register(this.dispatcher); // Folia - region threading - TODO later if (JvmProfiler.INSTANCE.isAvailable()) { JfrCommand.register(this.dispatcher); } @@ -215,8 +215,8 @@ public class Commands { OpCommand.register(this.dispatcher); PardonCommand.register(this.dispatcher); PardonIpCommand.register(this.dispatcher); - PerfCommand.register(this.dispatcher); - SaveAllCommand.register(this.dispatcher); + //PerfCommand.register(this.dispatcher); // Folia - region threading - TODO later + //SaveAllCommand.register(this.dispatcher); // Folia - region threading - TODO later SaveOffCommand.register(this.dispatcher); SaveOnCommand.register(this.dispatcher); SetPlayerIdleTimeoutCommand.register(this.dispatcher); @@ -446,9 +446,12 @@ public class Commands { } // Paper start - Async command map building new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent(player.getBukkitEntity(), (RootCommandNode) rootcommandnode, false).callEvent(); // Paper - net.minecraft.server.MinecraftServer.getServer().execute(() -> { - runSync(player, bukkit, rootcommandnode); - }); + // Folia start - region threading + // ignore if retired + player.getBukkitEntity().taskScheduler.schedule((updatedPlayer) -> { + runSync((ServerPlayer)updatedPlayer, bukkit, rootcommandnode); + }, null, 1L); + // Folia end - region threading } private void runSync(ServerPlayer player, Collection bukkit, RootCommandNode rootcommandnode) { diff --git a/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java index 309ad5a1da6b3a297d5526cd9247359ac5f49406..5a85fcbcd2966af95683106d4f459653983a28e6 100644 --- a/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java +++ b/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java @@ -33,7 +33,7 @@ public abstract class AbstractProjectileDispenseBehavior extends DefaultDispense CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) enumdirection.getStepX(), (double) ((float) enumdirection.getStepY() + 0.1F), (double) enumdirection.getStepZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java index 958134519befadc27a5b647caf64acf272ee2db4..a0712ad55c8e02a88ddf55bb0e70e05dc1ddbcdc 100644 --- a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java +++ b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java @@ -58,7 +58,7 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d0, d1 + d3, d2)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java index 1e6ba6d9cceda1d4867b183c3dbc03d317ed287f..de8cf0f0d34708b960f1c81cb10d813a797df02b 100644 --- a/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java +++ b/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java @@ -74,7 +74,7 @@ public class DefaultDispenseItemBehavior implements DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack); BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), CraftVector.toBukkit(entityitem.getDeltaMovement())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading world.getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java index 9598aa381978194fee859721731196f0e6ee08fc..cb6fc325200483396d466bb694fa9217d8b21260 100644 --- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java @@ -222,7 +222,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -277,7 +277,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -333,7 +333,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) list.get(0).getBukkitEntity()); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading world.getCraftServer().getPluginManager().callEvent(event); } @@ -389,7 +389,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityhorseabstract.getBukkitEntity()); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading world.getCraftServer().getPluginManager().callEvent(event); } @@ -463,7 +463,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityhorsechestedabstract.getBukkitEntity()); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading world.getCraftServer().getPluginManager().callEvent(event); } @@ -502,7 +502,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(enumdirection.getStepX(), enumdirection.getStepY(), enumdirection.getStepZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -560,7 +560,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d3, d4, d5)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -634,7 +634,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -707,7 +707,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -754,7 +754,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -815,7 +815,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -833,7 +833,8 @@ public interface DispenseItemBehavior { } } - worldserver.captureTreeGeneration = true; + io.papermc.paper.threadedregions.RegionizedWorldData worldData = worldserver.getCurrentWorldData(); // Folia - region threading + worldData.captureTreeGeneration = true; // Folia - region threading // CraftBukkit end if (!BoneMealItem.growCrop(stack, worldserver, blockposition) && !BoneMealItem.growWaterPlant(stack, worldserver, blockposition, (Direction) null)) { @@ -842,13 +843,13 @@ public interface DispenseItemBehavior { worldserver.levelEvent(1505, blockposition, 0); } // CraftBukkit start - worldserver.captureTreeGeneration = false; - if (worldserver.capturedBlockStates.size() > 0) { - TreeType treeType = SaplingBlock.treeType; - SaplingBlock.treeType = null; + worldData.captureTreeGeneration = false; // Folia - region threading + if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading + TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading + SaplingBlock.treeTypeRT.set(null); // Folia - region threading Location location = new Location(worldserver.getWorld(), blockposition.getX(), blockposition.getY(), blockposition.getZ()); - List blocks = new java.util.ArrayList<>(worldserver.capturedBlockStates.values()); - worldserver.capturedBlockStates.clear(); + List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading StructureGrowEvent structureEvent = null; if (treeType != null) { structureEvent = new StructureGrowEvent(location, treeType, false, null, blocks); @@ -883,7 +884,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -940,7 +941,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -989,7 +990,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -1062,7 +1063,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - only single item in event BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java index d1127d93a85a837933d0d73c24cacac4adc3a5b9..ac9f4f2ac817e5fe9a15759c549a57ad8473b6ac 100644 --- a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +++ b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java @@ -40,7 +40,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java index 0159ed9cbc644c39fa79e62327f13375193fdc98..a930c8eb64d6c7044646d6b0156e202ea334a1f9 100644 --- a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java +++ b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java @@ -37,7 +37,7 @@ public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading pointer.getLevel().getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java index fa1d325034dafdb9f1da546a6f9c5e88d2b67749..61bcbcacc1718b66f5b93548b1777c536cf3916b 100644 --- a/src/main/java/net/minecraft/network/Connection.java +++ b/src/main/java/net/minecraft/network/Connection.java @@ -75,7 +75,7 @@ public class Connection extends SimpleChannelInboundHandler> { return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper }); private final PacketFlow receiving; - private final Queue queue = Queues.newConcurrentLinkedQueue(); + private final Queue queue = new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>(); public Channel channel; public SocketAddress address; // Spigot Start @@ -83,7 +83,7 @@ public class Connection extends SimpleChannelInboundHandler> { public com.mojang.authlib.properties.Property[] spoofedProfile; public boolean preparing = true; // Spigot End - private PacketListener packetListener; + private volatile PacketListener packetListener; // Folia - region threading private Component disconnectedReason; private boolean encrypted; private boolean disconnectionHandled; @@ -179,6 +179,32 @@ public class Connection extends SimpleChannelInboundHandler> { this.receiving = side; } + // Folia start - region threading + private volatile boolean becomeActive; + + public boolean becomeActive() { + return this.becomeActive; + } + + private static record DisconnectReq(Component disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {} + + private final ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue disconnectReqs = + new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>(); + + /** + * Safely disconnects the connection while possibly on another thread. Note: This call will not block, even if on the + * same thread that could disconnect. + */ + public final void disconnectSafely(Component disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) { + this.disconnectReqs.add(new DisconnectReq(disconnectReason, cause)); + // We can't halt packet processing here because a plugin could cancel a kick request. + } + + public final boolean isPlayerConnected() { + return this.packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl; + } + // Folia end - region threading + public void channelActive(ChannelHandlerContext channelhandlercontext) throws Exception { super.channelActive(channelhandlercontext); this.channel = channelhandlercontext.channel(); @@ -193,6 +219,7 @@ public class Connection extends SimpleChannelInboundHandler> { Connection.LOGGER.error(LogUtils.FATAL_MARKER, "Failed to change protocol to handshake", throwable); } + this.becomeActive = true; // Folia - region threading } public void setProtocol(ConnectionProtocol state) { @@ -375,13 +402,6 @@ public class Connection extends SimpleChannelInboundHandler> { return; // Do nothing } packet.onPacketDispatch(getPlayer()); - if (connected && (InnerUtil.canSendImmediate(this, packet) || ( - io.papermc.paper.util.MCUtil.isMainThread() && packet.isReady() && this.queue.isEmpty() && - (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty()) - ))) { - this.sendPacket(packet, callbacks, null); // Paper - return; - } // write the packets to the queue, then flush - antixray hooks there already java.util.List extraPackets = InnerUtil.buildExtraPackets(packet); boolean hasExtraPackets = extraPackets != null && !extraPackets.isEmpty(); @@ -503,66 +523,58 @@ public class Connection extends SimpleChannelInboundHandler> { // Paper start - rewrite this to be safer if ran off main thread private boolean flushQueue() { // void -> boolean - if (!isConnected()) { + if (!this.isConnected()) { return true; } - if (io.papermc.paper.util.MCUtil.isMainThread()) { - return processQueue(); - } else if (isPending) { - // Should only happen during login/status stages - synchronized (this.queue) { - return this.processQueue(); - } - } - return false; + return this.processQueue(); + } + + // allow only one thread to be flushing the queue at once to ensure packets are written in the order they are sent + // into the queue + private final java.util.concurrent.atomic.AtomicBoolean flushingQueue = new java.util.concurrent.atomic.AtomicBoolean(); + + private boolean canWritePackets() { + PacketHolder holder = this.queue.peek(); + return holder != null && holder.packet.isReady(); } + private boolean processQueue() { - try { // Paper - add pending task queue - if (this.queue.isEmpty()) return true; - // Paper start - make only one flush call per sendPacketQueue() call final boolean needsFlush = this.canFlush; - boolean hasWrotePacket = false; - // Paper end - make only one flush call per sendPacketQueue() call - // If we are on main, we are safe here in that nothing else should be processing queue off main anymore - // But if we are not on main due to login/status, the parent is synchronized on packetQueue - java.util.Iterator iterator = this.queue.iterator(); - while (iterator.hasNext()) { - PacketHolder queued = iterator.next(); // poll -> peek - - // Fix NPE (Spigot bug caused by handleDisconnection()) - if (false && queued == null) { // Paper - diff on change, this logic is redundant: iterator guarantees ret of an element - on change, hook the flush logic here - return true; - } + while (this.canWritePackets()) { + final boolean set = this.flushingQueue.getAndSet(true); + try { + if (set) { + // we didn't acquire the lock, break + return false; + } - // Paper start - checking isConsumed flag and skipping packet sending - if (queued.isConsumed()) { - continue; - } - // Paper end - checking isConsumed flag and skipping packet sending + boolean justFlushed = true; - Packet packet = queued.packet; - if (!packet.isReady()) { - // Paper start - make only one flush call per sendPacketQueue() call - if (hasWrotePacket && (needsFlush || this.canFlush)) { + PacketHolder holder; + for (;;) { + // synchronise so that queue clears appear atomic + synchronized (this.queue) { + holder = ((ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue)this.queue).pollIf((PacketHolder h) -> { + return h.packet.isReady(); + }); + } + if (holder == null) { + break; + } + justFlushed = (!this.canWritePackets() && (needsFlush || this.canFlush)); + this.sendPacket(holder.packet, holder.listener, justFlushed ? Boolean.TRUE : Boolean.FALSE); // Paper - make only one flush call per sendPacketQueue() call + } + + if (!justFlushed) { this.flush(); } - // Paper end - make only one flush call per sendPacketQueue() call - return false; - } else { - iterator.remove(); - if (queued.tryMarkConsumed()) { // Paper - try to mark isConsumed flag for de-duplicating packet - this.sendPacket(packet, queued.listener, (!iterator.hasNext() && (needsFlush || this.canFlush)) ? Boolean.TRUE : Boolean.FALSE); // Paper - make only one flush call per sendPacketQueue() call - hasWrotePacket = true; // Paper - make only one flush call per sendPacketQueue() call + } finally { + if (!set) { + this.flushingQueue.set(false); } } } return true; - } finally { // Paper start - add pending task queue - Runnable r; - while ((r = this.pendingTasks.poll()) != null) { - this.channel.eventLoop().execute(r); - } - } // Paper end - add pending task queue } // Paper end @@ -571,21 +583,41 @@ public class Connection extends SimpleChannelInboundHandler> { private static int currTick; // Paper public void tick() { this.flushQueue(); - // Paper start - if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) { - Connection.currTick = net.minecraft.server.MinecraftServer.currentTick; - Connection.joinAttemptsThisTick = 0; + // Folia start - region threading + // handle disconnect requests, but only after flushQueue() + DisconnectReq disconnectReq; + while ((disconnectReq = this.disconnectReqs.poll()) != null) { + PacketListener packetlistener = this.packetListener; + + if (packetlistener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { + loginPacketListener.disconnect(disconnectReq.disconnectReason); + // this doesn't fail, so abort any further attempts + return; + } else if (packetlistener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl gamePacketListener) { + gamePacketListener.disconnect(disconnectReq.disconnectReason, disconnectReq.cause); + // may be cancelled by a plugin, if not cancelled then any further calls do nothing + continue; + } else { + // no idea what packet to send + this.disconnect(disconnectReq.disconnectReason); + this.setReadOnly(); + return; + } } - // Paper end + if (!this.isConnected()) { + // disconnected from above + this.handleDisconnection(); + return; + } + // Folia end - region threading + // Folia - this is broken PacketListener packetlistener = this.packetListener; if (packetlistener instanceof TickablePacketListener) { TickablePacketListener tickablepacketlistener = (TickablePacketListener) packetlistener; // Paper start - limit the number of joins which can be processed each tick - if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) - || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT - || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) { + if (true) { // Folia - region threading // Paper start - detailed watchdog information net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener); try { // Paper end - detailed watchdog information @@ -625,13 +657,21 @@ public class Connection extends SimpleChannelInboundHandler> { // Paper start public void clearPacketQueue() { net.minecraft.server.level.ServerPlayer player = getPlayer(); - queue.forEach(queuedPacket -> { + java.util.List queuedPackets = new java.util.ArrayList<>(); + // synchronise so that flushQueue does not poll values while the queue is being cleared + synchronized (this.queue) { + Connection.PacketHolder packetHolder; + while ((packetHolder = this.queue.poll()) != null) { + queuedPackets.add(packetHolder); + } + } + + for (Connection.PacketHolder queuedPacket : queuedPackets) { Packet packet = queuedPacket.packet; if (packet.hasFinishListener()) { packet.onPacketDispatchFinish(player, null); } - }); - queue.clear(); + } } // Paper end public void disconnect(Component disconnectReason) { @@ -643,6 +683,7 @@ public class Connection extends SimpleChannelInboundHandler> { this.channel.close(); // We can't wait as this may be called from an event loop. this.disconnectedReason = disconnectReason; } + this.becomeActive = true; // Folia - region threading } @@ -802,13 +843,27 @@ public class Connection extends SimpleChannelInboundHandler> { final net.minecraft.server.network.ServerGamePacketListenerImpl playerConnection = (net.minecraft.server.network.ServerGamePacketListenerImpl) packetListener; new com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent(playerConnection.player.getUUID(), playerConnection.player.getScoreboardName(), ((java.net.InetSocketAddress)address).getAddress(), false).callEvent(); + // Note: It can be in the connection set if it is in ready to accept if handleAcceptedLogin fails + // Folia start - region threading + net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection( + playerConnection.player.getScoreboardName(), + playerConnection.player.getUUID(), this + ); + // Folia end - region threading } else if (packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl) { /* Player is login stage */ final net.minecraft.server.network.ServerLoginPacketListenerImpl loginListener = (net.minecraft.server.network.ServerLoginPacketListenerImpl) packetListener; - switch (loginListener.state) { - case READY_TO_ACCEPT: - case DELAY_ACCEPT: - case ACCEPTED: + // Folia start - region threading + if (loginListener.state.ordinal() >= net.minecraft.server.network.ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT.ordinal()) { + // Note: It can be in the connection set if it is in ready to accept if handleAcceptedLogin fails + net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection( + loginListener.gameProfile.getName(), + net.minecraft.core.UUIDUtil.getOrCreatePlayerUUID(loginListener.gameProfile), + this + ); + } + // Folia end - region threading + if (loginListener.state.ordinal() >= net.minecraft.server.network.ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT.ordinal()) { // Folia - region threading - rewrite login process final com.mojang.authlib.GameProfile profile = loginListener.gameProfile; /* Should be non-null at this stage */ new com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent(profile.getId(), profile.getName(), ((java.net.InetSocketAddress)address).getAddress(), false).callEvent(); diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java index d2f0a0755317f5fa9a1ccf7db346aa77fd287d80..b07df826a3028c14b48b09dbaeccc9078d7cc992 100644 --- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java +++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java @@ -2,6 +2,7 @@ package net.minecraft.network.protocol; import com.mojang.logging.LogUtils; import net.minecraft.network.PacketListener; +import net.minecraft.server.level.ServerPlayer; import org.slf4j.Logger; // CraftBukkit start @@ -41,7 +42,7 @@ public class PacketUtils { public static void ensureRunningOnSameThread(Packet packet, T listener, BlockableEventLoop engine) throws RunningOnDifferentThreadException { if (!engine.isSameThread()) { - engine.execute(() -> { // Paper - Fix preemptive player kick on a server shutdown. + Runnable run = () -> { // Folia - region threading packetProcessing.push(listener); // Paper - detailed watchdog information try { // Paper - detailed watchdog information if (MinecraftServer.getServer().hasStopped() || (listener instanceof ServerGamePacketListenerImpl && ((ServerGamePacketListenerImpl) listener).processedDisconnect)) return; // CraftBukkit, MC-142590 @@ -66,7 +67,17 @@ public class PacketUtils { } // Paper end - detailed watchdog information - }); + }; // Folia start - region threading + ServerGamePacketListenerImpl actualListener = (ServerGamePacketListenerImpl)listener; + // ignore retired state, if removed then we don't want the packet to be handled + actualListener.player.getBukkitEntity().taskScheduler.schedule( + (ServerPlayer player) -> { + run.run(); + }, + null, + 1L + ); + // Folia end - region threading throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD; // CraftBukkit start - SPIGOT-5477, MC-142590 } else if (MinecraftServer.getServer().hasStopped() || (listener instanceof ServerGamePacketListenerImpl && ((ServerGamePacketListenerImpl) listener).processedDisconnect)) { diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 3219482b96cab8262e393a790c88d903d7de5166..f5977082a31d44088ab5ba31fad88e823cfe56e0 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -295,7 +295,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); public int autosavePeriod; public Commands vanillaCommandDispatcher; @@ -308,12 +308,40 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { AtomicReference atomicreference = new AtomicReference(); Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system @@ -605,7 +633,21 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop> 4); + world.randomSpawnSelection = new ChunkPos(world.getChunkSource().randomState().sampler().findSpawnPosition()); + for (int currX = -loadRegionRadius; currX <= loadRegionRadius; ++currX) { + for (int currZ = -loadRegionRadius; currZ <= loadRegionRadius; ++currZ) { + ChunkPos pos = new ChunkPos(currX, currZ); + world.chunkSource.addTicketAtLevel( + TicketType.UNKNOWN, pos, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, pos + ); + } + } + // Folia end - region threading // Paper - move up this.getPlayerList().addWorldborderListener(world); @@ -617,6 +659,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + return scheduledEnd - System.nanoTime() > targetBuffer; + }; + new com.destroystokyo.paper.event.server.ServerTickStartEvent((int)region.getCurrentTick()).callEvent(); // Paper + // Folia end - region threading co.aikar.timings.TimingsManager.FULL_SERVER_TICK.startTiming(); // Paper - long i = Util.getNanos(); + long i = startTime; // Folia - region threading // Paper start - move oversleep into full server tick + if (region == null) { // Folia - region threading isOversleep = true;MinecraftTimings.serverOversleep.startTiming(); this.managedBlock(() -> { return !this.canOversleep(); }); isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); + } // Folia - region threading // Paper end - new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper + // Folia - region threading - move up + + // Folia start - region threading + if (region != null) { + region.getTaskQueueData().drainTasks(); + ((io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler)Bukkit.getRegionScheduler()).tick(); + // now run all the entity schedulers + // TODO there has got to be a more efficient variant of this crap + for (Entity entity : region.world.getCurrentWorldData().getLocalEntitiesCopy()) { + if (!io.papermc.paper.util.TickThread.isTickThreadFor(entity) || entity.isRemoved()) { + continue; + } + org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw(); + if (bukkit != null) { + bukkit.taskScheduler.executeTick(); + } + } + // now tick connections + region.world.getCurrentWorldData().tickConnections(); // Folia - region threading + } + // Folia end - region threading ++this.tickCount; - this.tickChildren(shouldKeepTicking); - if (i - this.lastServerStatus >= 5000000000L) { + this.tickChildren(shouldKeepTicking, region); // Folia - region threading + if (region == null && i - this.lastServerStatus >= 5000000000L) { // Folia - region threading - moved to global tick this.lastServerStatus = i; this.status = this.buildServerStatus(); } @@ -1412,9 +1557,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0) { this.playerList.saveAll(playerSaveInterval); } - for (ServerLevel level : this.getAllLevels()) { + for (ServerLevel level : (region == null ? this.getAllLevels() : Arrays.asList(region.world))) { // Folia - region threading if (level.paperConfig().chunks.autoSaveInterval.value() > 0) { - level.saveIncrementally(fullSave); + level.saveIncrementally(region == null && fullSave); // Folia - region threading - don't save level.dat } } } finally { @@ -1424,16 +1569,17 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop list = this.playerList.getPlayers(); + List list = new java.util.ArrayList<>(this.playerList.getPlayers()); // Folia - region threading int i = this.getMaxPlayers(); if (this.hidesOnlinePlayers()) { @@ -1478,14 +1631,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper + // Folia - region threading this.profiler.push(() -> { return worldserver + " " + worldserver.dimension().location(); @@ -1543,7 +1694,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().invalidateStatus(); + }); + return; + } + // Folia end - region threading this.lastServerStatus = 0L; } @@ -1991,6 +2151,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= MAX_CHUNK_EXEC_TIME) { if (!moreTasks) { - lastMidTickExecuteFailure = currTime; + worldData.lastMidTickExecuteFailure = currTime; // Folia - region threading } // note: negative values reduce the time @@ -2794,7 +2940,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { // Folia - region threading + operation.perform(serverPlayer, selection); + }, null, 1L); // Folia - region threading + } if (i == 0) { @@ -99,9 +103,13 @@ public class AdvancementCommands { throw new CommandRuntimeException(Component.translatable("commands.advancement.criterionNotFound", advancement.getChatComponent(), criterion)); } else { for(ServerPlayer serverPlayer : targets) { + ++i; // Folia - region threading + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading if (operation.performCriterion(serverPlayer, advancement, criterion)) { - ++i; + // Folia - region threading } + }, null, 1L); // Folia - region threading + } if (i == 0) { diff --git a/src/main/java/net/minecraft/server/commands/AttributeCommand.java b/src/main/java/net/minecraft/server/commands/AttributeCommand.java index e846bd5db018f79c083d29f8f7b305a3d7ab45f5..b01aeb7bae3b6d3d291f76d19e8807980452646c 100644 --- a/src/main/java/net/minecraft/server/commands/AttributeCommand.java +++ b/src/main/java/net/minecraft/server/commands/AttributeCommand.java @@ -92,58 +92,113 @@ public class AttributeCommand { } } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int getAttributeValue(CommandSourceStack source, Entity target, Holder attribute, double multiplier) throws CommandSyntaxException { - LivingEntity livingEntity = getEntityWithAttribute(target, attribute); - double d = livingEntity.getAttributeValue(attribute); - source.sendSuccess(Component.translatable("commands.attribute.value.get.success", getAttributeDescription(attribute), target.getName(), d), false); - return (int)(d * multiplier); + // Folia start - region threading + target.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { + try { + LivingEntity livingEntity = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading + double d = livingEntity.getAttributeValue(attribute); + source.sendSuccess(Component.translatable("commands.attribute.value.get.success", getAttributeDescription(attribute), nmsEntity.getName(), d), false); + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + return 0; + // Folia end - region threading } private static int getAttributeBase(CommandSourceStack source, Entity target, Holder attribute, double multiplier) throws CommandSyntaxException { - LivingEntity livingEntity = getEntityWithAttribute(target, attribute); - double d = livingEntity.getAttributeBaseValue(attribute); - source.sendSuccess(Component.translatable("commands.attribute.base_value.get.success", getAttributeDescription(attribute), target.getName(), d), false); - return (int)(d * multiplier); + // Folia start - region threading + target.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { + try { + LivingEntity livingEntity = getEntityWithAttribute(nmsEntity, attribute); + double d = livingEntity.getAttributeBaseValue(attribute); + source.sendSuccess(Component.translatable("commands.attribute.base_value.get.success", getAttributeDescription(attribute), nmsEntity.getName(), d), false); + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + return 0; + // Folia end - region threading + } private static int getAttributeModifier(CommandSourceStack source, Entity target, Holder attribute, UUID uuid, double multiplier) throws CommandSyntaxException { - LivingEntity livingEntity = getEntityWithAttribute(target, attribute); - AttributeMap attributeMap = livingEntity.getAttributes(); - if (!attributeMap.hasModifier(attribute, uuid)) { - throw ERROR_NO_SUCH_MODIFIER.create(target.getName(), getAttributeDescription(attribute), uuid); - } else { - double d = attributeMap.getModifierValue(attribute, uuid); - source.sendSuccess(Component.translatable("commands.attribute.modifier.value.get.success", uuid, getAttributeDescription(attribute), target.getName(), d), false); - return (int)(d * multiplier); - } + // Folia start - region threading + target.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { + try { + LivingEntity livingEntity = getEntityWithAttribute(nmsEntity, attribute); + AttributeMap attributeMap = livingEntity.getAttributes(); + if (!attributeMap.hasModifier(attribute, uuid)) { + throw ERROR_NO_SUCH_MODIFIER.create(nmsEntity.getName(), getAttributeDescription(attribute), uuid); + } else { + double d = attributeMap.getModifierValue(attribute, uuid); + source.sendSuccess(Component.translatable("commands.attribute.modifier.value.get.success", uuid, getAttributeDescription(attribute), nmsEntity.getName(), d), false); + } + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + return 0; + // Folia end - region threading } private static int setAttributeBase(CommandSourceStack source, Entity target, Holder attribute, double value) throws CommandSyntaxException { - getAttributeInstance(target, attribute).setBaseValue(value); - source.sendSuccess(Component.translatable("commands.attribute.base_value.set.success", getAttributeDescription(attribute), target.getName(), value), false); + // Folia start - region threading + target.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { + try { + getAttributeInstance(nmsEntity, attribute).setBaseValue(value); + source.sendSuccess(Component.translatable("commands.attribute.base_value.set.success", getAttributeDescription(attribute), nmsEntity.getName(), value), false); + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + // Folia end - region threading return 1; } private static int addModifier(CommandSourceStack source, Entity target, Holder attribute, UUID uuid, String name, double value, AttributeModifier.Operation operation) throws CommandSyntaxException { - AttributeInstance attributeInstance = getAttributeInstance(target, attribute); - AttributeModifier attributeModifier = new AttributeModifier(uuid, name, value, operation); - if (attributeInstance.hasModifier(attributeModifier)) { - throw ERROR_MODIFIER_ALREADY_PRESENT.create(target.getName(), getAttributeDescription(attribute), uuid); - } else { - attributeInstance.addPermanentModifier(attributeModifier); - source.sendSuccess(Component.translatable("commands.attribute.modifier.add.success", uuid, getAttributeDescription(attribute), target.getName()), false); - return 1; - } + // Folia start - region threading + target.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { + try { + AttributeInstance attributeInstance = getAttributeInstance(nmsEntity, attribute); + AttributeModifier attributeModifier = new AttributeModifier(uuid, name, value, operation); + if (attributeInstance.hasModifier(attributeModifier)) { + throw ERROR_MODIFIER_ALREADY_PRESENT.create(nmsEntity.getName(), getAttributeDescription(attribute), uuid); + } else { + attributeInstance.addPermanentModifier(attributeModifier); + source.sendSuccess(Component.translatable("commands.attribute.modifier.add.success", uuid, getAttributeDescription(attribute), nmsEntity.getName()), false); + } + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + return 1; + // Folia end - region threading } private static int removeModifier(CommandSourceStack source, Entity target, Holder attribute, UUID uuid) throws CommandSyntaxException { - AttributeInstance attributeInstance = getAttributeInstance(target, attribute); - if (attributeInstance.removePermanentModifier(uuid)) { - source.sendSuccess(Component.translatable("commands.attribute.modifier.remove.success", uuid, getAttributeDescription(attribute), target.getName()), false); - return 1; - } else { - throw ERROR_NO_SUCH_MODIFIER.create(target.getName(), getAttributeDescription(attribute), uuid); - } + // Folia start - region threading + target.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { + try { + AttributeInstance attributeInstance = getAttributeInstance(nmsEntity, attribute); + if (attributeInstance.removePermanentModifier(uuid)) { + source.sendSuccess(Component.translatable("commands.attribute.modifier.remove.success", uuid, getAttributeDescription(attribute), nmsEntity.getName()), false); + } else { + throw ERROR_NO_SUCH_MODIFIER.create(nmsEntity.getName(), getAttributeDescription(attribute), uuid); + } + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + return 1; + // Folia end - region threading } private static Component getAttributeDescription(Holder attribute) { diff --git a/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java b/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java index 74623df731de543d3ef5832e818b10adec7b0f01..74a5e35c66e4d6aeae61733ad3ef1e51c0cfd593 100644 --- a/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java +++ b/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java @@ -46,9 +46,12 @@ public class ClearInventoryCommands { int i = 0; for(ServerPlayer serverPlayer : targets) { - i += serverPlayer.getInventory().clearOrCountMatchingItems(item, maxCount, serverPlayer.inventoryMenu.getCraftSlots()); + ++i; + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading + serverPlayer.getInventory().clearOrCountMatchingItems(item, maxCount, serverPlayer.inventoryMenu.getCraftSlots()); serverPlayer.containerMenu.broadcastChanges(); serverPlayer.inventoryMenu.slotsChanged(serverPlayer.getInventory()); + }, null, 1L); // Folia - region threading } if (i == 0) { diff --git a/src/main/java/net/minecraft/server/commands/DamageCommand.java b/src/main/java/net/minecraft/server/commands/DamageCommand.java index 95d11f48d201ccf5d9f73a3fcf78782669612f75..c96355360a379849a2f2285e41d7bd7ee6a22fa8 100644 --- a/src/main/java/net/minecraft/server/commands/DamageCommand.java +++ b/src/main/java/net/minecraft/server/commands/DamageCommand.java @@ -34,12 +34,28 @@ public class DamageCommand { }))))))))); } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int damage(CommandSourceStack source, Entity target, float amount, DamageSource damageSource) throws CommandSyntaxException { - if (target.hurt(damageSource, amount)) { - source.sendSuccess(Component.translatable("commands.damage.success", amount, target.getDisplayName()), true); - return 1; + // Folia start - region threading + target.getBukkitEntity().taskScheduler.schedule((Entity t) -> { + try { // Folia end - region threading + if (t.hurt(damageSource, amount)) { // Folia - region threading + source.sendSuccess(Component.translatable("commands.damage.success", amount, t.getDisplayName()), true); // Folia - region threading + return; // Folia - region threading } else { throw ERROR_INVULNERABLE.create(); } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + return 0; + // Folia end - region threading } } diff --git a/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java b/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java index 1bf4c5b36f53ef1e71d50d1a9af8e1410e5dff60..fd455c794fa52b565a5741b376bc394ac8dda07c 100644 --- a/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java +++ b/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java @@ -25,12 +25,14 @@ public class DefaultGameModeCommands { GameType gameType = minecraftServer.getForcedGameType(); if (gameType != null) { for(ServerPlayer serverPlayer : minecraftServer.getPlayerList().getPlayers()) { + serverPlayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading // Paper start - extend PlayerGameModeChangeEvent org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty()); if (event != null && event.isCancelled()) { source.sendSuccess(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), false); } // Paper end + }, null, 1L); // Folia - region threading ++i; } } diff --git a/src/main/java/net/minecraft/server/commands/EffectCommands.java b/src/main/java/net/minecraft/server/commands/EffectCommands.java index 959b82e38f3442a6b19a1f9d7615b6ddab967819..6633e7a3946e06438633a3a6276c6eaa9062778c 100644 --- a/src/main/java/net/minecraft/server/commands/EffectCommands.java +++ b/src/main/java/net/minecraft/server/commands/EffectCommands.java @@ -84,7 +84,15 @@ public class EffectCommands { if (entity instanceof LivingEntity) { MobEffectInstance mobeffect = new MobEffectInstance(mobeffectlist, k, amplifier, false, showParticles); - if (((LivingEntity) entity).addEffect(mobeffect, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { + if (!(nmsEntity instanceof LivingEntity)) { + return; + } + ((LivingEntity) nmsEntity).addEffect(mobeffect, null, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); + }, null, 1L); + // Folia end - region threading + if (true) { // CraftBukkit // Folia - region threading ++j; } } @@ -110,8 +118,16 @@ public class EffectCommands { while (iterator.hasNext()) { Entity entity = (Entity) iterator.next(); - if (entity instanceof LivingEntity && ((LivingEntity) entity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit + if (entity instanceof LivingEntity) { // CraftBukkit // Folia - region threading ++i; + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { + if (!(nmsEntity instanceof LivingEntity)) { + return; + } + ((LivingEntity) nmsEntity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); + }, null, 1L); + // Folia end - region threading } } @@ -136,8 +152,16 @@ public class EffectCommands { while (iterator.hasNext()) { Entity entity = (Entity) iterator.next(); - if (entity instanceof LivingEntity && ((LivingEntity) entity).removeEffect(mobeffectlist, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit + if (entity instanceof LivingEntity) { // CraftBukkit // Folia - region threading ++i; + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { + if (!(nmsEntity instanceof LivingEntity)) { + return; + } + ((LivingEntity) nmsEntity).removeEffect(mobeffectlist, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); + }, null, 1L); + // Folia end - region threading } } diff --git a/src/main/java/net/minecraft/server/commands/EnchantCommand.java b/src/main/java/net/minecraft/server/commands/EnchantCommand.java index e639c0ec642910e66b1d68ae0b9208ef58d91fce..abe7463f1e87449d65870955e58f257d4b8168c1 100644 --- a/src/main/java/net/minecraft/server/commands/EnchantCommand.java +++ b/src/main/java/net/minecraft/server/commands/EnchantCommand.java @@ -46,43 +46,69 @@ public class EnchantCommand { }))))); } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int enchant(CommandSourceStack source, Collection targets, Holder enchantment, int level) throws CommandSyntaxException { Enchantment enchantment2 = enchantment.value(); if (level > enchantment2.getMaxLevel()) { throw ERROR_LEVEL_TOO_HIGH.create(level, enchantment2.getMaxLevel()); } else { - int i = 0; + final java.util.concurrent.atomic.AtomicInteger changed = new java.util.concurrent.atomic.AtomicInteger(0); // Folia - region threading + final java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger(targets.size()); // Folia - region threading + final java.util.concurrent.atomic.AtomicReference possibleSingleDisplayName = new java.util.concurrent.atomic.AtomicReference<>(); // Folia - region threading for(Entity entity : targets) { if (entity instanceof LivingEntity) { - LivingEntity livingEntity = (LivingEntity)entity; - ItemStack itemStack = livingEntity.getMainHandItem(); - if (!itemStack.isEmpty()) { - if (enchantment2.canEnchant(itemStack) && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantments(itemStack).keySet(), enchantment2)) { - itemStack.enchant(enchantment2, level); - ++i; - } else if (targets.size() == 1) { - throw ERROR_INCOMPATIBLE.create(itemStack.getItem().getName(itemStack).getString()); + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { + try { + LivingEntity livingEntity = (LivingEntity)nmsEntity; + ItemStack itemStack = livingEntity.getMainHandItem(); + if (!itemStack.isEmpty()) { + if (enchantment2.canEnchant(itemStack) && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantments(itemStack).keySet(), enchantment2)) { + itemStack.enchant(enchantment2, level); + possibleSingleDisplayName.set(livingEntity.getDisplayName()); + changed.incrementAndGet(); + } else if (targets.size() == 1) { + throw ERROR_INCOMPATIBLE.create(itemStack.getItem().getName(itemStack).getString()); + } + } else if (targets.size() == 1) { + throw ERROR_NO_ITEM.create(livingEntity.getName().getString()); + } + } catch (final CommandSyntaxException exception) { + sendMessage(source, exception); + return; // don't send feedback twice } - } else if (targets.size() == 1) { - throw ERROR_NO_ITEM.create(livingEntity.getName().getString()); - } + sendFeedback(source, enchantment2, level, possibleSingleDisplayName, count, changed); + }, ignored -> sendFeedback(source, enchantment2, level, possibleSingleDisplayName, count, changed), 1L); } else if (targets.size() == 1) { throw ERROR_NOT_LIVING_ENTITY.create(entity.getName().getString()); + } else { + sendFeedback(source, enchantment2, level, possibleSingleDisplayName, count, changed); + // Folia end - region threading } } - + return targets.size(); // Folia - region threading + } + } + // Folia start - region threading + private static void sendFeedback(final CommandSourceStack source, final Enchantment enchantment2, final int level, final java.util.concurrent.atomic.AtomicReference possibleSingleDisplayName, final java.util.concurrent.atomic.AtomicInteger count, final java.util.concurrent.atomic.AtomicInteger changed) { + if (count.decrementAndGet() == 0) { + final int i = changed.get(); if (i == 0) { - throw ERROR_NOTHING_HAPPENED.create(); + sendMessage(source, ERROR_NOTHING_HAPPENED.create()); } else { - if (targets.size() == 1) { - source.sendSuccess(Component.translatable("commands.enchant.success.single", enchantment2.getFullname(level), targets.iterator().next().getDisplayName()), true); + if (i == 1) { + source.sendSuccess(Component.translatable("commands.enchant.success.single", enchantment2.getFullname(level), possibleSingleDisplayName.get()), true); } else { - source.sendSuccess(Component.translatable("commands.enchant.success.multiple", enchantment2.getFullname(level), targets.size()), true); + source.sendSuccess(Component.translatable("commands.enchant.success.multiple", enchantment2.getFullname(level), i), true); } - - return i; } } } + // Folia end - region threading } diff --git a/src/main/java/net/minecraft/server/commands/ExperienceCommand.java b/src/main/java/net/minecraft/server/commands/ExperienceCommand.java index a628e3730b1c26c2e6a85c449440af0afe4c0d8d..6651376603c3fb2331ae0955343285ac7c37726f 100644 --- a/src/main/java/net/minecraft/server/commands/ExperienceCommand.java +++ b/src/main/java/net/minecraft/server/commands/ExperienceCommand.java @@ -46,14 +46,18 @@ public class ExperienceCommand { } private static int queryExperience(CommandSourceStack source, ServerPlayer player, ExperienceCommand.Type component) { + player.getBukkitEntity().taskScheduler.schedule((ServerPlayer p) -> { // Folia - region threading int i = component.query.applyAsInt(player); source.sendSuccess(Component.translatable("commands.experience.query." + component.name, player.getDisplayName(), i), false); - return i; + }, null, 1L); // Folia - region threading + return 0; } private static int addExperience(CommandSourceStack source, Collection targets, int amount, ExperienceCommand.Type component) { for(ServerPlayer serverPlayer : targets) { + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading component.add.accept(serverPlayer, amount); + }, null, 1L); // Folia - region threading } if (targets.size() == 1) { @@ -69,9 +73,12 @@ public class ExperienceCommand { int i = 0; for(ServerPlayer serverPlayer : targets) { + ++i; // Folia - region threading + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading if (component.set.test(serverPlayer, amount)) { - ++i; + // Folia - region threading } + }, null, 1L); // Folia - region threading } if (i == 0) { diff --git a/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java b/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java index c8977042f26ea8f78cee3a115d2d7dec99b291ee..83a4b72b7f895d29d1e3e0b6b9f288bd5f3cb52c 100644 --- a/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java +++ b/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java @@ -69,6 +69,12 @@ public class FillBiomeCommand { }; } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int fill(CommandSourceStack source, BlockPos from, BlockPos to, Holder.Reference biome, Predicate> filter) throws CommandSyntaxException { BlockPos blockPos = quantize(from); BlockPos blockPos2 = quantize(to); @@ -79,8 +85,19 @@ public class FillBiomeCommand { throw ERROR_VOLUME_TOO_LARGE.create(j, i); } else { ServerLevel serverLevel = source.getLevel(); + // Folia start - region threading + int buffer = 0; + // no buffer, we do not touch neighbours + serverLevel.loadChunksAsync( + (boundingBox.minX() - buffer) >> 4, + (boundingBox.maxX() + buffer) >> 4, + (boundingBox.minZ() - buffer) >> 4, + (boundingBox.maxZ() + buffer) >> 4, + net.minecraft.world.level.chunk.ChunkStatus.FULL, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (chunks) -> { + try { // Folia end - region threading List list = new ArrayList<>(); - for(int k = SectionPos.blockToSectionCoord(boundingBox.minZ()); k <= SectionPos.blockToSectionCoord(boundingBox.maxZ()); ++k) { for(int l = SectionPos.blockToSectionCoord(boundingBox.minX()); l <= SectionPos.blockToSectionCoord(boundingBox.maxX()); ++l) { ChunkAccess chunkAccess = serverLevel.getChunk(l, k, ChunkStatus.FULL, false); @@ -101,7 +118,11 @@ public class FillBiomeCommand { serverLevel.getChunkSource().chunkMap.resendBiomesForChunks(list); source.sendSuccess(Component.translatable("commands.fillbiome.success.count", mutableInt.getValue(), boundingBox.minX(), boundingBox.minY(), boundingBox.minZ(), boundingBox.maxX(), boundingBox.maxY(), boundingBox.maxZ()), true); - return mutableInt.getValue(); + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); return 0; // Folia end - region threading } } } diff --git a/src/main/java/net/minecraft/server/commands/FillCommand.java b/src/main/java/net/minecraft/server/commands/FillCommand.java index b25b2ed0eb3c6716a7c92cb1f1066a1447d88bcf..3b55e806204cbe2df2cadcec91fbc84eed5f15b0 100644 --- a/src/main/java/net/minecraft/server/commands/FillCommand.java +++ b/src/main/java/net/minecraft/server/commands/FillCommand.java @@ -57,6 +57,12 @@ public class FillCommand { })))))); } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int fillBlocks(CommandSourceStack source, BoundingBox range, BlockInput block, FillCommand.Mode mode, @Nullable Predicate filter) throws CommandSyntaxException { int i = range.getXSpan() * range.getYSpan() * range.getZSpan(); int j = source.getLevel().getGameRules().getInt(GameRules.RULE_COMMAND_MODIFICATION_BLOCK_LIMIT); @@ -65,6 +71,18 @@ public class FillCommand { } else { List list = Lists.newArrayList(); ServerLevel serverLevel = source.getLevel(); + // Folia start - region threading + int buffer = 32; + // physics may spill into neighbour chunks, so use a buffer + serverLevel.loadChunksAsync( + (range.minX() - buffer) >> 4, + (range.maxX() + buffer) >> 4, + (range.minZ() - buffer) >> 4, + (range.maxZ() + buffer) >> 4, + net.minecraft.world.level.chunk.ChunkStatus.FULL, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (chunks) -> { + try { // Folia end - region threading int k = 0; for(BlockPos blockPos : BlockPos.betweenClosed(range.minX(), range.minY(), range.minZ(), range.maxX(), range.maxY(), range.maxZ())) { @@ -90,8 +108,13 @@ public class FillCommand { throw ERROR_FAILED.create(); } else { source.sendSuccess(Component.translatable("commands.fill.success", k), true); - return k; + return; // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); return 0; // Folia end - region threading } } diff --git a/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java b/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java index de484336165891d16220fdc0363e5283ba92b75d..494292dd2aa45771d42e5f271675f9ca267e8458 100644 --- a/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java +++ b/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java @@ -49,96 +49,126 @@ public class ForceLoadCommand { })))); } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int queryForceLoad(CommandSourceStack source, ColumnPos pos) throws CommandSyntaxException { ChunkPos chunkPos = pos.toChunkPos(); ServerLevel serverLevel = source.getLevel(); ResourceKey resourceKey = serverLevel.dimension(); - boolean bl = serverLevel.getForcedChunks().contains(chunkPos.toLong()); - if (bl) { - source.sendSuccess(Component.translatable("commands.forceload.query.success", chunkPos, resourceKey.location()), false); - return 1; - } else { - throw ERROR_NOT_TICKING.create(chunkPos, resourceKey.location()); - } + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + try { + boolean bl = serverLevel.getForcedChunks().contains(chunkPos.toLong()); + if (bl) { + source.sendSuccess(Component.translatable("commands.forceload.query.success", chunkPos, resourceKey.location()), false); + return; + } else { + throw ERROR_NOT_TICKING.create(chunkPos, resourceKey.location()); + } + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); + return 1; + // Folia end - region threading } private static int listForceLoad(CommandSourceStack source) { ServerLevel serverLevel = source.getLevel(); ResourceKey resourceKey = serverLevel.dimension(); - LongSet longSet = serverLevel.getForcedChunks(); - int i = longSet.size(); - if (i > 0) { - String string = Joiner.on(", ").join(longSet.stream().sorted().map(ChunkPos::new).map(ChunkPos::toString).iterator()); - if (i == 1) { - source.sendSuccess(Component.translatable("commands.forceload.list.single", resourceKey.location(), string), false); + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + LongSet longSet = serverLevel.getForcedChunks(); + int i = longSet.size(); + if (i > 0) { + String string = Joiner.on(", ").join(longSet.stream().sorted().map(ChunkPos::new).map(ChunkPos::toString).iterator()); + if (i == 1) { + source.sendSuccess(Component.translatable("commands.forceload.list.single", resourceKey.location(), string), false); + } else { + source.sendSuccess(Component.translatable("commands.forceload.list.multiple", i, resourceKey.location(), string), false); + } } else { - source.sendSuccess(Component.translatable("commands.forceload.list.multiple", i, resourceKey.location(), string), false); + source.sendFailure(Component.translatable("commands.forceload.added.none", resourceKey.location())); } - } else { - source.sendFailure(Component.translatable("commands.forceload.added.none", resourceKey.location())); - } - return i; + }); + return 1; + // Folia end - region threading } private static int removeAll(CommandSourceStack source) { ServerLevel serverLevel = source.getLevel(); ResourceKey resourceKey = serverLevel.dimension(); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading LongSet longSet = serverLevel.getForcedChunks(); longSet.forEach((chunkPos) -> { serverLevel.setChunkForced(ChunkPos.getX(chunkPos), ChunkPos.getZ(chunkPos), false); }); source.sendSuccess(Component.translatable("commands.forceload.removed.all", resourceKey.location()), true); + }); // Folia - region threading return 0; } private static int changeForceLoad(CommandSourceStack source, ColumnPos from, ColumnPos to, boolean forceLoaded) throws CommandSyntaxException { - int i = Math.min(from.x(), to.x()); - int j = Math.min(from.z(), to.z()); - int k = Math.max(from.x(), to.x()); - int l = Math.max(from.z(), to.z()); - if (i >= -30000000 && j >= -30000000 && k < 30000000 && l < 30000000) { - int m = SectionPos.blockToSectionCoord(i); - int n = SectionPos.blockToSectionCoord(j); - int o = SectionPos.blockToSectionCoord(k); - int p = SectionPos.blockToSectionCoord(l); - long q = ((long)(o - m) + 1L) * ((long)(p - n) + 1L); - if (q > 256L) { - throw ERROR_TOO_MANY_CHUNKS.create(256, q); - } else { - ServerLevel serverLevel = source.getLevel(); - ResourceKey resourceKey = serverLevel.dimension(); - ChunkPos chunkPos = null; - int r = 0; + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + try { + int i = Math.min(from.x(), to.x()); + int j = Math.min(from.z(), to.z()); + int k = Math.max(from.x(), to.x()); + int l = Math.max(from.z(), to.z()); + if (i >= -30000000 && j >= -30000000 && k < 30000000 && l < 30000000) { + int m = SectionPos.blockToSectionCoord(i); + int n = SectionPos.blockToSectionCoord(j); + int o = SectionPos.blockToSectionCoord(k); + int p = SectionPos.blockToSectionCoord(l); + long q = ((long)(o - m) + 1L) * ((long)(p - n) + 1L); + if (q > 256L) { + throw ERROR_TOO_MANY_CHUNKS.create(256, q); + } else { + ServerLevel serverLevel = source.getLevel(); + ResourceKey resourceKey = serverLevel.dimension(); + ChunkPos chunkPos = null; + int r = 0; - for(int s = m; s <= o; ++s) { - for(int t = n; t <= p; ++t) { - boolean bl = serverLevel.setChunkForced(s, t, forceLoaded); - if (bl) { - ++r; - if (chunkPos == null) { - chunkPos = new ChunkPos(s, t); + for(int s = m; s <= o; ++s) { + for(int t = n; t <= p; ++t) { + boolean bl = serverLevel.setChunkForced(s, t, forceLoaded); + if (bl) { + ++r; + if (chunkPos == null) { + chunkPos = new ChunkPos(s, t); + } + } } } - } - } - if (r == 0) { - throw (forceLoaded ? ERROR_ALL_ADDED : ERROR_NONE_REMOVED).create(); - } else { - if (r == 1) { - source.sendSuccess(Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".single", chunkPos, resourceKey.location()), true); - } else { - ChunkPos chunkPos2 = new ChunkPos(m, n); - ChunkPos chunkPos3 = new ChunkPos(o, p); - source.sendSuccess(Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".multiple", r, resourceKey.location(), chunkPos2, chunkPos3), true); - } + if (r == 0) { + throw (forceLoaded ? ERROR_ALL_ADDED : ERROR_NONE_REMOVED).create(); + } else { + if (r == 1) { + source.sendSuccess(Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".single", chunkPos, resourceKey.location()), true); + } else { + ChunkPos chunkPos2 = new ChunkPos(m, n); + ChunkPos chunkPos3 = new ChunkPos(o, p); + source.sendSuccess(Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".multiple", r, resourceKey.location(), chunkPos2, chunkPos3), true); + } - return r; + return; + } + } + } else { + throw BlockPosArgument.ERROR_OUT_OF_WORLD.create(); } + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); } - } else { - throw BlockPosArgument.ERROR_OUT_OF_WORLD.create(); - } + }); + return 1; + // Folia end - region threading } } diff --git a/src/main/java/net/minecraft/server/commands/GameModeCommand.java b/src/main/java/net/minecraft/server/commands/GameModeCommand.java index 27c0aaf123c3e945eb24e8a3892bd8ac42115733..2f9f73e75b6c730a9cf327767ba1c34e34c64ed8 100644 --- a/src/main/java/net/minecraft/server/commands/GameModeCommand.java +++ b/src/main/java/net/minecraft/server/commands/GameModeCommand.java @@ -44,15 +44,18 @@ public class GameModeCommand { int i = 0; for(ServerPlayer serverPlayer : targets) { + serverPlayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading // Paper start - extend PlayerGameModeChangeEvent org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty()); if (event != null && !event.isCancelled()) { logGamemodeChange(context.getSource(), serverPlayer, gameMode); - ++i; + // Folia - region threading } else if (event != null && event.cancelMessage() != null) { context.getSource().sendSuccess(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), true); // Paper end } + }, null, 1L); // Folia - region threading + ++i; // Folia - region threading } return i; diff --git a/src/main/java/net/minecraft/server/commands/GiveCommand.java b/src/main/java/net/minecraft/server/commands/GiveCommand.java index ee7d29d85c8b024c9b23cba8ecd4192aa7e8aa7b..9fc4e60750552dae8412ed07982ea025f934af61 100644 --- a/src/main/java/net/minecraft/server/commands/GiveCommand.java +++ b/src/main/java/net/minecraft/server/commands/GiveCommand.java @@ -55,6 +55,7 @@ public class GiveCommand { l -= i1; ItemStack itemstack = item.createItemStack(i1, false); + entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading boolean flag = entityplayer.getInventory().add(itemstack); ItemEntity entityitem; @@ -74,6 +75,7 @@ public class GiveCommand { entityitem.setTarget(entityplayer.getUUID()); } } + }, null, 1L); // Folia - region threading } } diff --git a/src/main/java/net/minecraft/server/commands/KillCommand.java b/src/main/java/net/minecraft/server/commands/KillCommand.java index a6e4bd9243dab7feaed1bd968108a324d6c37ed7..4637e60292128e8c4053fb3a5fed48e53ec6553f 100644 --- a/src/main/java/net/minecraft/server/commands/KillCommand.java +++ b/src/main/java/net/minecraft/server/commands/KillCommand.java @@ -22,7 +22,9 @@ public class KillCommand { private static int kill(CommandSourceStack source, Collection targets) { for(Entity entity : targets) { - entity.kill(); + entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading + nmsEntity.kill(); // Folia - region threading + }, null, 1L); // Folia - region threading } if (targets.size() == 1) { diff --git a/src/main/java/net/minecraft/server/commands/PlaceCommand.java b/src/main/java/net/minecraft/server/commands/PlaceCommand.java index c9ee0b8a0f72c21f29448c3a8fd53a7007c740f8..6ab0bbfb5e04b13082999d825e8445fbbbc184a3 100644 --- a/src/main/java/net/minecraft/server/commands/PlaceCommand.java +++ b/src/main/java/net/minecraft/server/commands/PlaceCommand.java @@ -83,82 +83,130 @@ public class PlaceCommand { }))))))))); } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + public static int placeFeature(CommandSourceStack source, Holder.Reference> feature, BlockPos pos) throws CommandSyntaxException { ServerLevel serverLevel = source.getLevel(); ConfiguredFeature configuredFeature = feature.value(); ChunkPos chunkPos = new ChunkPos(pos); checkLoaded(serverLevel, new ChunkPos(chunkPos.x - 1, chunkPos.z - 1), new ChunkPos(chunkPos.x + 1, chunkPos.z + 1)); - if (!configuredFeature.place(serverLevel, serverLevel.getChunkSource().getGenerator(), serverLevel.getRandom(), pos)) { - throw ERROR_FEATURE_FAILED.create(); - } else { - String string = feature.key().location().toString(); - source.sendSuccess(Component.translatable("commands.place.feature.success", string, pos.getX(), pos.getY(), pos.getZ()), true); - return 1; - } + // Folia start - region threading + serverLevel.loadChunksAsync( + pos, 16, net.minecraft.world.level.chunk.ChunkStatus.FULL, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (chunks) -> { + try { + if (!configuredFeature.place(serverLevel, serverLevel.getChunkSource().getGenerator(), serverLevel.getRandom(), pos)) { + throw ERROR_FEATURE_FAILED.create(); + } else { + String string = feature.key().location().toString(); + source.sendSuccess(Component.translatable("commands.place.feature.success", string, pos.getX(), pos.getY(), pos.getZ()), true); + } + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + } + ); + return 1; + // Folia end - region threading } public static int placeJigsaw(CommandSourceStack source, Holder structurePool, ResourceLocation id, int maxDepth, BlockPos pos) throws CommandSyntaxException { ServerLevel serverLevel = source.getLevel(); - if (!JigsawPlacement.generateJigsaw(serverLevel, structurePool, id, maxDepth, pos, false)) { - throw ERROR_JIGSAW_FAILED.create(); - } else { - source.sendSuccess(Component.translatable("commands.place.jigsaw.success", pos.getX(), pos.getY(), pos.getZ()), true); - return 1; - } + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + serverLevel, pos.getX() >> 4, pos.getZ() >> 4, () -> { + try { + if (!JigsawPlacement.generateJigsaw(serverLevel, structurePool, id, maxDepth, pos, false)) { + throw ERROR_JIGSAW_FAILED.create(); + } else { + source.sendSuccess(Component.translatable("commands.place.jigsaw.success", pos.getX(), pos.getY(), pos.getZ()), true); + } + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + } + ); + return 1; + // Folia end - region threading } public static int placeStructure(CommandSourceStack source, Holder.Reference structure, BlockPos pos) throws CommandSyntaxException { ServerLevel serverLevel = source.getLevel(); Structure structure2 = structure.value(); ChunkGenerator chunkGenerator = serverLevel.getChunkSource().getGenerator(); - StructureStart structureStart = structure2.generate(source.registryAccess(), chunkGenerator, chunkGenerator.getBiomeSource(), serverLevel.getChunkSource().randomState(), serverLevel.getStructureManager(), serverLevel.getSeed(), new ChunkPos(pos), 0, serverLevel, (biome) -> { - return true; - }); - if (!structureStart.isValid()) { - throw ERROR_STRUCTURE_FAILED.create(); - } else { - BoundingBox boundingBox = structureStart.getBoundingBox(); - ChunkPos chunkPos = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.minX()), SectionPos.blockToSectionCoord(boundingBox.minZ())); - ChunkPos chunkPos2 = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.maxX()), SectionPos.blockToSectionCoord(boundingBox.maxZ())); - checkLoaded(serverLevel, chunkPos, chunkPos2); - ChunkPos.rangeClosed(chunkPos, chunkPos2).forEach((chunkPosx) -> { - structureStart.placeInChunk(serverLevel, serverLevel.structureManager(), chunkGenerator, serverLevel.getRandom(), new BoundingBox(chunkPosx.getMinBlockX(), serverLevel.getMinBuildHeight(), chunkPosx.getMinBlockZ(), chunkPosx.getMaxBlockX(), serverLevel.getMaxBuildHeight(), chunkPosx.getMaxBlockZ()), chunkPosx); - }); - String string = structure.key().location().toString(); - source.sendSuccess(Component.translatable("commands.place.structure.success", string, pos.getX(), pos.getY(), pos.getZ()), true); - return 1; - } + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + serverLevel, pos.getX() >> 4, pos.getZ() >> 4, () -> { + try { + StructureStart structureStart = structure2.generate(source.registryAccess(), chunkGenerator, chunkGenerator.getBiomeSource(), serverLevel.getChunkSource().randomState(), serverLevel.getStructureManager(), serverLevel.getSeed(), new ChunkPos(pos), 0, serverLevel, (biome) -> { + return true; + }); + if (!structureStart.isValid()) { + throw ERROR_STRUCTURE_FAILED.create(); + } else { + BoundingBox boundingBox = structureStart.getBoundingBox(); + ChunkPos chunkPos = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.minX()), SectionPos.blockToSectionCoord(boundingBox.minZ())); + ChunkPos chunkPos2 = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.maxX()), SectionPos.blockToSectionCoord(boundingBox.maxZ())); + checkLoaded(serverLevel, chunkPos, chunkPos2); + ChunkPos.rangeClosed(chunkPos, chunkPos2).forEach((chunkPosx) -> { + structureStart.placeInChunk(serverLevel, serverLevel.structureManager(), chunkGenerator, serverLevel.getRandom(), new BoundingBox(chunkPosx.getMinBlockX(), serverLevel.getMinBuildHeight(), chunkPosx.getMinBlockZ(), chunkPosx.getMaxBlockX(), serverLevel.getMaxBuildHeight(), chunkPosx.getMaxBlockZ()), chunkPosx); + }); + String string = structure.key().location().toString(); + source.sendSuccess(Component.translatable("commands.place.structure.success", string, pos.getX(), pos.getY(), pos.getZ()), true); + } + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + } + ); + return 1; + // Folia end - region threading } public static int placeTemplate(CommandSourceStack source, ResourceLocation id, BlockPos pos, Rotation rotation, Mirror mirror, float integrity, int seed) throws CommandSyntaxException { ServerLevel serverLevel = source.getLevel(); - StructureTemplateManager structureTemplateManager = serverLevel.getStructureManager(); + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + serverLevel, pos.getX() >> 4, pos.getZ() >> 4, () -> { + try { + StructureTemplateManager structureTemplateManager = serverLevel.getStructureManager(); - Optional optional; - try { - optional = structureTemplateManager.get(id); - } catch (ResourceLocationException var13) { - throw ERROR_TEMPLATE_INVALID.create(id); - } + Optional optional; + try { + optional = structureTemplateManager.get(id); + } catch (ResourceLocationException var13) { + throw ERROR_TEMPLATE_INVALID.create(id); + } - if (optional.isEmpty()) { - throw ERROR_TEMPLATE_INVALID.create(id); - } else { - StructureTemplate structureTemplate = optional.get(); - checkLoaded(serverLevel, new ChunkPos(pos), new ChunkPos(pos.offset(structureTemplate.getSize()))); - StructurePlaceSettings structurePlaceSettings = (new StructurePlaceSettings()).setMirror(mirror).setRotation(rotation); - if (integrity < 1.0F) { - structurePlaceSettings.clearProcessors().addProcessor(new BlockRotProcessor(integrity)).setRandom(StructureBlockEntity.createRandom((long)seed)); - } + if (optional.isEmpty()) { + throw ERROR_TEMPLATE_INVALID.create(id); + } else { + StructureTemplate structureTemplate = optional.get(); + checkLoaded(serverLevel, new ChunkPos(pos), new ChunkPos(pos.offset(structureTemplate.getSize()))); + StructurePlaceSettings structurePlaceSettings = (new StructurePlaceSettings()).setMirror(mirror).setRotation(rotation); + if (integrity < 1.0F) { + structurePlaceSettings.clearProcessors().addProcessor(new BlockRotProcessor(integrity)).setRandom(StructureBlockEntity.createRandom((long)seed)); + } - boolean bl = structureTemplate.placeInWorld(serverLevel, pos, pos, structurePlaceSettings, StructureBlockEntity.createRandom((long)seed), 2); - if (!bl) { - throw ERROR_TEMPLATE_FAILED.create(); - } else { - source.sendSuccess(Component.translatable("commands.place.template.success", id, pos.getX(), pos.getY(), pos.getZ()), true); - return 1; + boolean bl = structureTemplate.placeInWorld(serverLevel, pos, pos, structurePlaceSettings, StructureBlockEntity.createRandom((long)seed), 2); + if (!bl) { + throw ERROR_TEMPLATE_FAILED.create(); + } else { + source.sendSuccess(Component.translatable("commands.place.template.success", id, pos.getX(), pos.getY(), pos.getZ()), true); + } + } + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } } - } + ); + return 1; + // Folia end - region threading } private static void checkLoaded(ServerLevel world, ChunkPos pos1, ChunkPos pos2) throws CommandSyntaxException { diff --git a/src/main/java/net/minecraft/server/commands/RecipeCommand.java b/src/main/java/net/minecraft/server/commands/RecipeCommand.java index 2a92e542e4b3e4dfb26adfc4b21490a629b79382..d3405192a705637daba66735c717d64708362bd1 100644 --- a/src/main/java/net/minecraft/server/commands/RecipeCommand.java +++ b/src/main/java/net/minecraft/server/commands/RecipeCommand.java @@ -36,7 +36,12 @@ public class RecipeCommand { int i = 0; for(ServerPlayer serverPlayer : targets) { - i += serverPlayer.awardRecipes(recipes); + // Folia start - region threading + ++i; + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { + serverPlayer.awardRecipes(recipes); + }, null, 1L); + // Folia end - region threading } if (i == 0) { @@ -56,7 +61,12 @@ public class RecipeCommand { int i = 0; for(ServerPlayer serverPlayer : targets) { - i += serverPlayer.resetRecipes(recipes); + // Folia start - region threading + ++i; + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { + serverPlayer.resetRecipes(recipes); + }, null, 1L); + // Folia end - region threading } if (i == 0) { diff --git a/src/main/java/net/minecraft/server/commands/SetBlockCommand.java b/src/main/java/net/minecraft/server/commands/SetBlockCommand.java index ad435815e56ca5a8d5ea6046ee4a3ed4d3673a48..98f314064987e3c0b87e415459f6c263e75ea48d 100644 --- a/src/main/java/net/minecraft/server/commands/SetBlockCommand.java +++ b/src/main/java/net/minecraft/server/commands/SetBlockCommand.java @@ -38,29 +38,45 @@ public class SetBlockCommand { }))))); } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int setBlock(CommandSourceStack source, BlockPos pos, BlockInput block, SetBlockCommand.Mode mode, @Nullable Predicate condition) throws CommandSyntaxException { ServerLevel serverLevel = source.getLevel(); - if (condition != null && !condition.test(new BlockInWorld(serverLevel, pos, true))) { - throw ERROR_FAILED.create(); - } else { - boolean bl; - if (mode == SetBlockCommand.Mode.DESTROY) { - serverLevel.destroyBlock(pos, true); - bl = !block.getState().isAir() || !serverLevel.getBlockState(pos).isAir(); - } else { - BlockEntity blockEntity = serverLevel.getBlockEntity(pos); - Clearable.tryClear(blockEntity); - bl = true; - } + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + serverLevel, pos.getX() >> 4, pos.getZ() >> 4, () -> { + try { + if (condition != null && !condition.test(new BlockInWorld(serverLevel, pos, true))) { + throw ERROR_FAILED.create(); + } else { + boolean bl; + if (mode == SetBlockCommand.Mode.DESTROY) { + serverLevel.destroyBlock(pos, true); + bl = !block.getState().isAir() || !serverLevel.getBlockState(pos).isAir(); + } else { + BlockEntity blockEntity = serverLevel.getBlockEntity(pos); + Clearable.tryClear(blockEntity); + bl = true; + } - if (bl && !block.place(serverLevel, pos, 2)) { - throw ERROR_FAILED.create(); - } else { - serverLevel.blockUpdated(pos, block.getState().getBlock()); - source.sendSuccess(Component.translatable("commands.setblock.success", pos.getX(), pos.getY(), pos.getZ()), true); - return 1; + if (bl && !block.place(serverLevel, pos, 2)) { + throw ERROR_FAILED.create(); + } else { + serverLevel.blockUpdated(pos, block.getState().getBlock()); + source.sendSuccess(Component.translatable("commands.setblock.success", pos.getX(), pos.getY(), pos.getZ()), true); + } + } + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } } - } + ); + return 1; + // Folia end - region threading } public interface Filter { diff --git a/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java b/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java index b0ed00b95b273f0916cbade0e3bac47aa464d3fd..ebabc7919562acc1e43014aa702e65765567175f 100644 --- a/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java +++ b/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java @@ -35,7 +35,12 @@ public class SetSpawnCommand { final Collection actualTargets = new java.util.ArrayList<>(); // Paper for(ServerPlayer serverPlayer : targets) { // Paper start - PlayerSetSpawnEvent - if (serverPlayer.setRespawnPosition(resourceKey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND)) { + // Folia start - region threading + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { + player.setRespawnPosition(resourceKey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND); + }, null, 1L); + // Folia end - region threading + if (true) { // Folia - region threading actualTargets.add(serverPlayer); } // Paper end diff --git a/src/main/java/net/minecraft/server/commands/SummonCommand.java b/src/main/java/net/minecraft/server/commands/SummonCommand.java index 108b8ddbb724c6a2aca97bd7d5b29bea87662e70..6da8e88a64e0904a9e5f27da21559ab60659dd88 100644 --- a/src/main/java/net/minecraft/server/commands/SummonCommand.java +++ b/src/main/java/net/minecraft/server/commands/SummonCommand.java @@ -63,11 +63,18 @@ public class SummonCommand { if (entity == null) { throw SummonCommand.ERROR_FAILED.create(); } else { - if (initialize && entity instanceof Mob) { - ((Mob) entity).finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), MobSpawnType.COMMAND, (SpawnGroupData) null, (CompoundTag) null); - } + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + worldserver, entity.chunkPosition().x, entity.chunkPosition().z, () -> { + if (initialize && entity instanceof Mob) { + ((Mob) entity).finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), MobSpawnType.COMMAND, (SpawnGroupData) null, (CompoundTag) null); + } + worldserver.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND); + } + ); + // Folia end - region threading - if (!worldserver.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND)) { // CraftBukkit - pass a spawn reason of "COMMAND" + if (false) { // CraftBukkit - pass a spawn reason of "COMMAND" // Folia - region threading throw SummonCommand.ERROR_DUPLICATE_UUID.create(); } else { return entity; diff --git a/src/main/java/net/minecraft/server/commands/TeleportCommand.java b/src/main/java/net/minecraft/server/commands/TeleportCommand.java index 6511af08d8db1f8fc62bdb4a8f357941f524886d..8dee4f9a408194f2bbbd9028517d33504cbe188d 100644 --- a/src/main/java/net/minecraft/server/commands/TeleportCommand.java +++ b/src/main/java/net/minecraft/server/commands/TeleportCommand.java @@ -77,7 +77,7 @@ public class TeleportCommand { while (iterator.hasNext()) { Entity entity1 = (Entity) iterator.next(); - TeleportCommand.performTeleport(source, entity1, (ServerLevel) destination.level, destination.getX(), destination.getY(), destination.getZ(), EnumSet.noneOf(RelativeMovement.class), destination.getYRot(), destination.getXRot(), (TeleportCommand.LookAt) null); + io.papermc.paper.threadedregions.TeleportUtils.teleport(entity1, false, destination, Float.valueOf(destination.getYRot()), Float.valueOf(destination.getXRot()), Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND, null); // Folia - region threading } if (targets.size() == 1) { @@ -153,6 +153,24 @@ public class TeleportCommand { float f2 = Mth.wrapDegrees(yaw); float f3 = Mth.wrapDegrees(pitch); + // Folia start - region threading + if (true) { + ServerLevel worldFinal = world; + Vec3 posFinal = new Vec3(x, y, z); + Float yawFinal = Float.valueOf(f2); + Float pitchFinal = Float.valueOf(f3); + target.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { + nmsEntity.unRide(); + nmsEntity.teleportAsync( + worldFinal, posFinal, yawFinal, pitchFinal, Vec3.ZERO, + org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND, + Entity.TELEPORT_FLAG_LOAD_CHUNK, + null + ); + }, null, 1L); + return; + } + // Folia end - region threading // CraftBukkit start - Teleport event boolean result; if (target instanceof ServerPlayer player) { diff --git a/src/main/java/net/minecraft/server/commands/TimeCommand.java b/src/main/java/net/minecraft/server/commands/TimeCommand.java index f0a7a8df3caa2ea765bb0a87cfede71d0995d276..b666adf0d0eddea1beb59d6f7a2968b5c0296b37 100644 --- a/src/main/java/net/minecraft/server/commands/TimeCommand.java +++ b/src/main/java/net/minecraft/server/commands/TimeCommand.java @@ -56,6 +56,7 @@ public class TimeCommand { while (iterator.hasNext()) { ServerLevel worldserver = (ServerLevel) iterator.next(); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading // CraftBukkit start TimeSkipEvent event = new TimeSkipEvent(worldserver.getWorld(), TimeSkipEvent.SkipReason.COMMAND, time - worldserver.getDayTime()); Bukkit.getPluginManager().callEvent(event); @@ -63,6 +64,7 @@ public class TimeCommand { worldserver.setDayTime((long) worldserver.getDayTime() + event.getSkipAmount()); } // CraftBukkit end + }); // Folia - region threading } source.sendSuccess(Component.translatable("commands.time.set", time), true); @@ -75,6 +77,7 @@ public class TimeCommand { while (iterator.hasNext()) { ServerLevel worldserver = (ServerLevel) iterator.next(); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading // CraftBukkit start TimeSkipEvent event = new TimeSkipEvent(worldserver.getWorld(), TimeSkipEvent.SkipReason.COMMAND, time); Bukkit.getPluginManager().callEvent(event); @@ -82,11 +85,14 @@ public class TimeCommand { worldserver.setDayTime(worldserver.getDayTime() + event.getSkipAmount()); } // CraftBukkit end + }); // Folia - region threading } + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading int j = TimeCommand.getDayTime(source.getLevel()); source.sendSuccess(Component.translatable("commands.time.set", j), true); - return j; + }); // Folia - region threading + return 0; // Folia - region threading } } diff --git a/src/main/java/net/minecraft/server/commands/WeatherCommand.java b/src/main/java/net/minecraft/server/commands/WeatherCommand.java index 471e9a2d8ad92c8f45856487a2919e4113ed4468..67c401bb45c1c972f1cd249547c094c854d50cc3 100644 --- a/src/main/java/net/minecraft/server/commands/WeatherCommand.java +++ b/src/main/java/net/minecraft/server/commands/WeatherCommand.java @@ -35,20 +35,26 @@ public class WeatherCommand { } private static int setClear(CommandSourceStack source, int duration) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading source.getLevel().setWeatherParameters(getDuration(source, duration, ServerLevel.RAIN_DELAY), 0, false, false); source.sendSuccess(Component.translatable("commands.weather.set.clear"), true); + }); // Folia - region threading return duration; } private static int setRain(CommandSourceStack source, int duration) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading source.getLevel().setWeatherParameters(0, getDuration(source, duration, ServerLevel.RAIN_DURATION), true, false); source.sendSuccess(Component.translatable("commands.weather.set.rain"), true); + }); // Folia - region threading return duration; } private static int setThunder(CommandSourceStack source, int duration) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading source.getLevel().setWeatherParameters(0, getDuration(source, duration, ServerLevel.THUNDER_DURATION), true, true); source.sendSuccess(Component.translatable("commands.weather.set.thunder"), true); + }); // Folia - region threading return duration; } } diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java index a7e133f3495e9132a5fdae2c24f225e7b026295a..7abd4f38ae59a6019137345af960fd60a3c7adf0 100644 --- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java @@ -443,9 +443,9 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface } @Override - public void tickChildren(BooleanSupplier shouldKeepTicking) { - super.tickChildren(shouldKeepTicking); - this.handleConsoleInputs(); + public void tickChildren(BooleanSupplier shouldKeepTicking, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - region threading + super.tickChildren(shouldKeepTicking, region); // Folia - region threading + if (region == null) this.handleConsoleInputs(); // Folia - region threading } @Override @@ -748,6 +748,12 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface @Override public String runCommand(String command) { + // Folia start - region threading + // RIP RCON + if (true) { + throw new UnsupportedOperationException(); + } + // Folia end - region threading Waitable[] waitableArray = new Waitable[1]; this.rconConsoleSource.prepareForCommand(); this.executeBlocking(() -> { diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java index 6fce2a9bce051e21eba8f331007a9752607f69f2..a880cbd4afdf2e540bbe6b62587091c9b5a76ef0 100644 --- a/src/main/java/net/minecraft/server/level/ChunkHolder.java +++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java @@ -85,18 +85,18 @@ public class ChunkHolder { public void onChunkAdd() { // Paper start - optimise anyPlayerCloseEnoughForSpawning long key = io.papermc.paper.util.MCUtil.getCoordinateKey(this.pos); - this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key); - this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key); + this.playersInMobSpawnRange = null; // Folia - region threading + this.playersInChunkTickRange = null; // Folia - region threading // Paper end - optimise anyPlayerCloseEnoughForSpawning // Paper start - optimise chunk tick iteration if (this.needsBroadcastChanges()) { - this.chunkMap.needsChangeBroadcasting.add(this); + this.chunkMap.level.needsChangeBroadcasting.add(this); // Folia - region threading } // Paper end - optimise chunk tick iteration // Paper start - optimise checkDespawn LevelChunk chunk = this.getFullChunkNowUnchecked(); if (chunk != null) { - chunk.updateGeneralAreaCache(); + //chunk.updateGeneralAreaCache(); // Folia - region threading } // Paper end - optimise checkDespawn } @@ -108,13 +108,13 @@ public class ChunkHolder { // Paper end - optimise anyPlayerCloseEnoughForSpawning // Paper start - optimise chunk tick iteration if (this.needsBroadcastChanges()) { - this.chunkMap.needsChangeBroadcasting.remove(this); + this.chunkMap.level.needsChangeBroadcasting.remove(this); // Folia - region threading } // Paper end - optimise chunk tick iteration // Paper start - optimise checkDespawn LevelChunk chunk = this.getFullChunkNowUnchecked(); if (chunk != null) { - chunk.removeGeneralAreaCache(); + //chunk.removeGeneralAreaCache(); // Folia - region threading } // Paper end - optimise checkDespawn } @@ -302,8 +302,8 @@ public class ChunkHolder { } private void addToBroadcastMap() { - org.spigotmc.AsyncCatcher.catchOp("ChunkHolder update"); - this.chunkMap.needsChangeBroadcasting.add(this); + io.papermc.paper.util.TickThread.ensureTickThread(this.chunkMap.level, this.pos, "Cannot update chunk holder asynchronously"); // Folia - region threading + this.chunkMap.level.needsChangeBroadcasting.add(this); // Folia - region threading } // Paper end - optimise chunk tick iteration diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java index 2d133ae656f7420d6ceec14bc591721cff815479..6f12d91f73e04c25fb0bc9054dc7d49de16e614a 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -149,21 +149,21 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider private final AtomicInteger tickingGenerated; public final StructureTemplateManager structureTemplateManager; // Paper - rewrite chunk system private final String storageName; - private final PlayerMap playerMap; - public final Int2ObjectMap entityMap; + //private final PlayerMap playerMap; // Folia - region threading + //public final Int2ObjectMap entityMap; // Folia - region threading private final Long2ByteMap chunkTypeCache; private final Long2LongMap chunkSaveCooldowns; private final Queue unloadQueue; int viewDistance; - public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobDistanceMap; // Paper - public final ReferenceOpenHashSet needsChangeBroadcasting = new ReferenceOpenHashSet<>(); + //public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobDistanceMap; // Paper // Folia - region threading + //public final ReferenceOpenHashSet needsChangeBroadcasting = new ReferenceOpenHashSet<>(); // Folia - region threading // Paper - rewrite chunk system // Paper start - optimise checkDespawn public static final int GENERAL_AREA_MAP_SQUARE_RADIUS = 40; public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE = 16.0 * (GENERAL_AREA_MAP_SQUARE_RADIUS - 1); public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE_SQUARED = GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE * GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE; - public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerGeneralAreaMap; + //public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerGeneralAreaMap; // Folia - region threading // Paper end - optimise checkDespawn // Paper start - distance maps @@ -177,8 +177,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // obviously this means a spawn range > 8 cannot be implemented // these maps are named after spigot's uses - public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap; // this map is absent from updateMaps since it's controlled at the start of the chunkproviderserver tick - public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap; + //public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap; // this map is absent from updateMaps since it's controlled at the start of the chunkproviderserver tick // Folia - region threading + //public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap; // Folia - region threading // Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning // Paper start - use distance map to optimise tracker public static boolean isLegacyTrackingEntity(Entity entity) { @@ -187,11 +187,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // inlined EnumMap, TrackingRange.TrackingRangeType static final org.spigotmc.TrackingRange.TrackingRangeType[] TRACKING_RANGE_TYPES = org.spigotmc.TrackingRange.TrackingRangeType.values(); - public final com.destroystokyo.paper.util.misc.PlayerAreaMap[] playerEntityTrackerTrackMaps; - final int[] entityTrackerTrackRanges; - public final int getEntityTrackerRange(final int ordinal) { - return this.entityTrackerTrackRanges[ordinal]; - } + // Folia - region threading private int convertSpigotRangeToVanilla(final int vanilla) { return MinecraftServer.getServer().getScaledTrackingDistance(vanilla); @@ -203,40 +199,29 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider 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 - this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning + //this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning // Folia - region threading // Paper start - per player mob spawning - if (this.playerMobDistanceMap != null) { - this.playerMobDistanceMap.add(player, chunkX, chunkZ, io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(player)); - } + // Folia - region threading // Paper end - per player mob spawning // Paper start - use distance map to optimise entity tracker - for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) { - com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; - int trackRange = this.entityTrackerTrackRanges[i]; - - trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player))); - } + // Folia - region threading // Paper end - use distance map to optimise entity tracker - this.playerGeneralAreaMap.add(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn + //this.playerGeneralAreaMap.add(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn // Folia - region threading } void removePlayerFromDistanceMaps(ServerPlayer player) { this.level.playerChunkLoader.removePlayer(player); // Paper - replace chunk loader // Paper start - optimise ChunkMap#anyPlayerCloseEnoughForSpawning - this.playerMobSpawnMap.remove(player); - this.playerChunkTickRangeMap.remove(player); + this.level.getCurrentWorldData().mobSpawnMap.remove(player); // Folia - region threading + //this.playerChunkTickRangeMap.remove(player); // Folia - region threading // Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning - this.playerGeneralAreaMap.remove(player); // Paper - optimise checkDespawns + //this.playerGeneralAreaMap.remove(player); // Paper - optimise checkDespawns // Folia - region threading // Paper start - per player mob spawning - if (this.playerMobDistanceMap != null) { - this.playerMobDistanceMap.remove(player); - } + // Folia - region threading // Paper end - per player mob spawning // Paper start - use distance map to optimise tracker - for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) { - this.playerEntityTrackerTrackMaps[i].remove(player); - } + // Folia - region threading // Paper end - use distance map to optimise tracker } @@ -245,21 +230,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider int chunkZ = MCUtil.getChunkCoordinate(player.getZ()); // Note: players need to be explicitly added to distance maps before they can be updated this.level.playerChunkLoader.updatePlayer(player); // Paper - replace chunk loader - this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning + //this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning // Folia - region threading // Paper start - per player mob spawning - if (this.playerMobDistanceMap != null) { - this.playerMobDistanceMap.update(player, chunkX, chunkZ, io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(player)); - } + // Folia - region threading // Paper end - per player mob spawning // Paper start - use distance map to optimise entity tracker - for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) { - com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; - int trackRange = this.entityTrackerTrackRanges[i]; - - trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player))); - } + // Folia - region threading // Paper end - use distance map to optimise entity tracker - this.playerGeneralAreaMap.update(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn + //this.playerGeneralAreaMap.update(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn // Folia - region threading } // Paper end // Paper start @@ -297,8 +275,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider super(session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync); // Paper - rewrite chunk system this.tickingGenerated = new AtomicInteger(); - this.playerMap = new PlayerMap(); - this.entityMap = new Int2ObjectOpenHashMap(); + //this.playerMap = new PlayerMap(); // Folia - region threading + //this.entityMap = new Int2ObjectOpenHashMap(); // Folia - region threading this.chunkTypeCache = new Long2ByteOpenHashMap(); this.chunkSaveCooldowns = new Long2LongOpenHashMap(); this.unloadQueue = Queues.newConcurrentLinkedQueue(); @@ -343,96 +321,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider 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); + //this.regionManagers.add(this.dataRegionManager); // Folia - region threading // Paper end - this.playerMobDistanceMap = this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets) : null; // Paper + //this.playerMobDistanceMap = this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets) : null; // Paper // Folia - region threading // Paper start - use distance map to optimise entity tracker - this.playerEntityTrackerTrackMaps = new com.destroystokyo.paper.util.misc.PlayerAreaMap[TRACKING_RANGE_TYPES.length]; - this.entityTrackerTrackRanges = new int[TRACKING_RANGE_TYPES.length]; - - org.spigotmc.SpigotWorldConfig spigotWorldConfig = this.level.spigotConfig; - - for (int ordinal = 0, len = TRACKING_RANGE_TYPES.length; ordinal < len; ++ordinal) { - org.spigotmc.TrackingRange.TrackingRangeType trackingRangeType = TRACKING_RANGE_TYPES[ordinal]; - int configuredSpigotValue; - switch (trackingRangeType) { - case PLAYER: - configuredSpigotValue = spigotWorldConfig.playerTrackingRange; - break; - case ANIMAL: - configuredSpigotValue = spigotWorldConfig.animalTrackingRange; - break; - case MONSTER: - configuredSpigotValue = spigotWorldConfig.monsterTrackingRange; - break; - case MISC: - configuredSpigotValue = spigotWorldConfig.miscTrackingRange; - break; - case OTHER: - configuredSpigotValue = spigotWorldConfig.otherTrackingRange; - break; - case ENDERDRAGON: - configuredSpigotValue = EntityType.ENDER_DRAGON.clientTrackingRange() * 16; - break; - default: - throw new IllegalStateException("Missing case for enum " + trackingRangeType); - } - configuredSpigotValue = convertSpigotRangeToVanilla(configuredSpigotValue); - - int trackRange = (configuredSpigotValue >>> 4) + ((configuredSpigotValue & 15) != 0 ? 1 : 0); - this.entityTrackerTrackRanges[ordinal] = trackRange; - - this.playerEntityTrackerTrackMaps[ordinal] = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets); - } + // Folia - region threading // Paper end - use distance map to optimise entity tracker // Paper start - optimise ChunkMap#anyPlayerCloseEnoughForSpawning - this.playerChunkTickRangeMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, - (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { - ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ)); - if (playerChunk != null) { - playerChunk.playersInChunkTickRange = newState; - } - }, - (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { - ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ)); - if (playerChunk != null) { - playerChunk.playersInChunkTickRange = newState; - } - }); - this.playerMobSpawnMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, - (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { - ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ)); - if (playerChunk != null) { - playerChunk.playersInMobSpawnRange = newState; - } - }, - (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { - ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ)); - if (playerChunk != null) { - playerChunk.playersInMobSpawnRange = newState; - } - }); + // Folia - region threading + // Folia - region threading // Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning // Paper start - optimise checkDespawn - this.playerGeneralAreaMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, - (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { - LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ); - if (chunk != null) { - chunk.updateGeneralAreaCache(newState); - } - }, - (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { - LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ); - if (chunk != null) { - chunk.updateGeneralAreaCache(newState); - } - }); + // Folia - region threading // Paper end - optimise checkDespawn } @@ -460,28 +360,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } // Paper start - public void updatePlayerMobTypeMap(Entity entity) { - if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { - return; - } - int index = entity.getType().getCategory().ordinal(); - - final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = this.playerMobDistanceMap.getObjectsInRange(entity.chunkPosition()); - if (inRange == null) { - return; - } - final Object[] backingSet = inRange.getBackingSet(); - for (int i = 0; i < backingSet.length; i++) { - if (!(backingSet[i] instanceof final ServerPlayer player)) { - continue; - } - ++player.mobCounts[index]; - } - } - - public int getMobCountNear(ServerPlayer entityPlayer, net.minecraft.world.entity.MobCategory mobCategory) { - return entityPlayer.mobCounts[mobCategory.ordinal()]; - } + // Folia - region threading - revert per player mob caps // Paper end private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) { @@ -750,6 +629,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper start // rets true if to prevent the entity from being added public static boolean checkDupeUUID(ServerLevel level, Entity entity) { + // Folia start - region threading + if (true) { + // TODO fix this shit later + return false; + } + // Folia end - region threading io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode mode = level.paperConfig().entities.spawning.duplicateUuid.mode; if (mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.WARN && mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.DELETE @@ -1010,6 +895,38 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } final boolean anyPlayerCloseEnoughForSpawning(ChunkHolder playerchunk, ChunkPos chunkcoordintpair, boolean reducedRange) { + // Folia start - region threading + if (true) { + java.util.List players = this.level.getLocalPlayers(); + if (reducedRange) { + for (int i = 0, len = players.size(); i < len; ++i) { + ServerPlayer player = players.get(i); + if (!player.affectsSpawning || player.isSpectator()) { + continue; + } + // don't check spectator and whatnot, already handled by mob spawn map update + if (euclideanDistanceSquared(chunkcoordintpair, player) < player.lastEntitySpawnRadiusSquared) { + return true; // in range + } + } + } else { + final double range = (DistanceManager.MOB_SPAWN_RANGE * 16) * (DistanceManager.MOB_SPAWN_RANGE * 16); + // before spigot, mob spawn range was actually mob spawn range + tick range, but it was split + for (int i = 0, len = players.size(); i < len; ++i) { + ServerPlayer player = players.get(i); + if (!player.affectsSpawning || player.isSpectator()) { + continue; + } + // don't check spectator and whatnot, already handled by mob spawn map update + if (euclideanDistanceSquared(chunkcoordintpair, player) < range) { + return true; // in range + } + } + } + // no players in range + return false; + } + // Folia end - region threading // this function is so hot that removing the map lookup call can have an order of magnitude impact on its performance // tested and confirmed via System.nanoTime() com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInRange = reducedRange ? playerchunk.playersInMobSpawnRange : playerchunk.playersInChunkTickRange; @@ -1055,7 +972,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider return List.of(); } else { Builder builder = ImmutableList.builder(); - Iterator iterator = this.playerMap.getPlayers(i).iterator(); + Iterator iterator = this.level.getLocalPlayers().iterator(); // Folia - region threading while (iterator.hasNext()) { ServerPlayer entityplayer = (ServerPlayer) iterator.next(); @@ -1084,25 +1001,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } void updatePlayerStatus(ServerPlayer player, boolean added) { - boolean flag1 = this.skipPlayer(player); - boolean flag2 = this.playerMap.ignoredOrUnknown(player); - int i = SectionPos.blockToSectionCoord(player.getBlockX()); - int j = SectionPos.blockToSectionCoord(player.getBlockZ()); + // Folia - region threading if (added) { - this.playerMap.addPlayer(ChunkPos.asLong(i, j), player, flag1); + // Folia - region threading this.updatePlayerPos(player); - if (!flag1) { - this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player); - } + // Folia - region threading this.addPlayerToDistanceMaps(player); // Paper - distance maps } else { SectionPos sectionposition = player.getLastSectionPos(); - this.playerMap.removePlayer(sectionposition.chunk().toLong(), player); - if (!flag2) { - this.distanceManager.removePlayer(sectionposition, player); - } + // Folia - region threading + // Folia - region threading this.removePlayerFromDistanceMaps(player); // Paper - distance maps } @@ -1121,43 +1031,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public void move(ServerPlayer player) { // Paper - delay this logic for the entity tracker tick, no need to duplicate it - int i = SectionPos.blockToSectionCoord(player.getBlockX()); - int j = SectionPos.blockToSectionCoord(player.getBlockZ()); - SectionPos sectionposition = player.getLastSectionPos(); - SectionPos sectionposition1 = SectionPos.of((EntityAccess) player); - long k = sectionposition.chunk().toLong(); - long l = sectionposition1.chunk().toLong(); - boolean flag = this.playerMap.ignored(player); - boolean flag1 = this.skipPlayer(player); - boolean flag2 = sectionposition.asLong() != sectionposition1.asLong(); - - if (flag2 || flag != flag1) { - this.updatePlayerPos(player); - if (!flag) { - this.distanceManager.removePlayer(sectionposition, player); - } - - if (!flag1) { - this.distanceManager.addPlayer(sectionposition1, player); - } - - if (!flag && flag1) { - this.playerMap.ignorePlayer(player); - } - - if (flag && !flag1) { - this.playerMap.unIgnorePlayer(player); - } - - if (k != l) { - this.playerMap.updatePlayer(k, l, player); - } - } - - int i1 = sectionposition.x(); - int j1 = sectionposition.z(); - int k1; - int l1; + // Folia - region threading - none of this logic is relevant anymore thanks to the player chunk loader // Paper - replaced by PlayerChunkLoader @@ -1180,9 +1054,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public void addEntity(Entity entity) { org.spigotmc.AsyncCatcher.catchOp("entity track"); // Spigot // Paper start - ignore and warn about illegal addEntity calls instead of crashing server - if (!entity.valid || entity.level != this.level || this.entityMap.containsKey(entity.getId())) { + if (!entity.valid || entity.level != this.level || entity.tracker != null) { // Folia - region threading LOGGER.error("Illegal ChunkMap::addEntity for world " + this.level.getWorld().getName() - + ": " + entity + (this.entityMap.containsKey(entity.getId()) ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable()); + + ": " + entity + (entity.tracker != null ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable()); return; } if (entity instanceof ServerPlayer && ((ServerPlayer) entity).supressTrackerForLogin) return; // Delay adding to tracker until after list packets @@ -1195,27 +1069,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (i != 0) { int j = entitytypes.updateInterval(); - if (this.entityMap.containsKey(entity.getId())) { + if (entity.tracker != null) { // Folia - region threading throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Entity is already tracked!")); } else { ChunkMap.TrackedEntity playerchunkmap_entitytracker = new ChunkMap.TrackedEntity(entity, i, j, entitytypes.trackDeltas()); entity.tracker = playerchunkmap_entitytracker; // Paper - Fast access to tracker - this.entityMap.put(entity.getId(), playerchunkmap_entitytracker); - playerchunkmap_entitytracker.updatePlayers(entity.getPlayersInTrackRange()); // Paper - don't search all players + // Folia - region threading + playerchunkmap_entitytracker.updatePlayers(this.level.getLocalPlayers()); // Paper - don't search all players // Folia - region threading if (entity instanceof ServerPlayer) { ServerPlayer entityplayer = (ServerPlayer) entity; this.updatePlayerStatus(entityplayer, true); - ObjectIterator objectiterator = this.entityMap.values().iterator(); - - while (objectiterator.hasNext()) { - ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = (ChunkMap.TrackedEntity) objectiterator.next(); - - if (playerchunkmap_entitytracker1.entity != entityplayer) { - playerchunkmap_entitytracker1.updatePlayer(entityplayer); + // Folia start - region threading + for (Entity possible : this.level.getCurrentWorldData().getLocalEntities()) { + if (possible.tracker != null) { + possible.tracker.updatePlayer(entityplayer); } } + // Folia end - region threading } } @@ -1229,16 +1101,16 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider ServerPlayer entityplayer = (ServerPlayer) entity; this.updatePlayerStatus(entityplayer, false); - ObjectIterator objectiterator = this.entityMap.values().iterator(); - - while (objectiterator.hasNext()) { - ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); - - playerchunkmap_entitytracker.removePlayer(entityplayer); + // Folia start - region threading + for (Entity possible : this.level.getCurrentWorldData().getLocalEntities()) { + if (possible.tracker != null) { + possible.tracker.removePlayer(entityplayer); + } } + // Folia end - region threading } - ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = (ChunkMap.TrackedEntity) this.entityMap.remove(entity.getId()); + ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = entity.tracker; // Folia - region threading if (playerchunkmap_entitytracker1 != null) { playerchunkmap_entitytracker1.broadcastRemoved(); @@ -1248,25 +1120,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper start - optimised tracker private final void processTrackQueue() { - this.level.timings.tracker1.startTiming(); - try { - for (TrackedEntity tracker : this.entityMap.values()) { - // update tracker entry - tracker.updatePlayers(tracker.entity.getPlayersInTrackRange()); - } - } finally { - this.level.timings.tracker1.stopTiming(); - } - - - this.level.timings.tracker2.startTiming(); - try { - for (TrackedEntity tracker : this.entityMap.values()) { - tracker.serverEntity.sendChanges(); + // Folia start - region threading + List players = this.level.getLocalPlayers(); // Folia - region threading + for (Entity entity : this.level.getCurrentWorldData().getLocalEntities()) { + TrackedEntity tracker = entity.tracker; + if (tracker == null) { + continue; } - } finally { - this.level.timings.tracker2.stopTiming(); + tracker.updatePlayers(players); + tracker.removeNonTickThreadPlayers(); + tracker.serverEntity.sendChanges(); } + // Folia end - region threading } // Paper end - optimised tracker @@ -1277,51 +1142,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider return; } // Paper end - optimized tracker - List list = Lists.newArrayList(); - List list1 = this.level.players(); - ObjectIterator objectiterator = this.entityMap.values().iterator(); - level.timings.tracker1.startTiming(); // Paper - - ChunkMap.TrackedEntity playerchunkmap_entitytracker; - - while (objectiterator.hasNext()) { - playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); - SectionPos sectionposition = playerchunkmap_entitytracker.lastSectionPos; - SectionPos sectionposition1 = SectionPos.of((EntityAccess) playerchunkmap_entitytracker.entity); - boolean flag = !Objects.equals(sectionposition, sectionposition1); - - if (flag) { - playerchunkmap_entitytracker.updatePlayers(list1); - Entity entity = playerchunkmap_entitytracker.entity; - - if (entity instanceof ServerPlayer) { - list.add((ServerPlayer) entity); - } - - playerchunkmap_entitytracker.lastSectionPos = sectionposition1; - } - - if (flag || this.distanceManager.inEntityTickingRange(sectionposition1.chunk().toLong())) { - playerchunkmap_entitytracker.serverEntity.sendChanges(); - } - } - level.timings.tracker1.stopTiming(); // Paper - - if (!list.isEmpty()) { - objectiterator = this.entityMap.values().iterator(); - - level.timings.tracker2.startTiming(); // Paper - while (objectiterator.hasNext()) { - playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); - playerchunkmap_entitytracker.updatePlayers(list); - } - level.timings.tracker2.stopTiming(); // Paper - } + // Folia - region threading } public void broadcast(Entity entity, Packet packet) { - ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) this.entityMap.get(entity.getId()); + ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) entity.tracker; // Folia - region threading if (playerchunkmap_entitytracker != null) { playerchunkmap_entitytracker.broadcast(packet); @@ -1330,7 +1156,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } protected void broadcastAndSend(Entity entity, Packet packet) { - ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) this.entityMap.get(entity.getId()); + ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) entity.tracker; // Folia - region threading if (playerchunkmap_entitytracker != null) { playerchunkmap_entitytracker.broadcastAndSend(packet); @@ -1517,41 +1343,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.lastSectionPos = SectionPos.of((EntityAccess) entity); } - // Paper start - use distance map to optimise tracker - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet lastTrackerCandidates; - - final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newTrackerCandidates) { - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet oldTrackerCandidates = this.lastTrackerCandidates; - this.lastTrackerCandidates = newTrackerCandidates; - - if (newTrackerCandidates != null) { - Object[] rawData = newTrackerCandidates.getBackingSet(); - for (int i = 0, len = rawData.length; i < len; ++i) { - Object raw = rawData[i]; - if (!(raw instanceof ServerPlayer)) { - continue; - } - ServerPlayer player = (ServerPlayer)raw; - this.updatePlayer(player); - } - } - - if (oldTrackerCandidates == newTrackerCandidates) { - // this is likely the case. - // means there has been no range changes, so we can just use the above for tracking. - return; - } - - // stuff could have been removed, so we need to check the trackedPlayers set - // for players that were removed - - for (ServerPlayerConnection conn : this.seenBy.toArray(new ServerPlayerConnection[0])) { // avoid CME - if (newTrackerCandidates == null || !newTrackerCandidates.contains(conn.getPlayer())) { - this.updatePlayer(conn.getPlayer()); - } - } - } - // Paper end - use distance map to optimise tracker + // Folia - region threading public boolean equals(Object object) { return object instanceof ChunkMap.TrackedEntity ? ((ChunkMap.TrackedEntity) object).entity.getId() == this.entity.getId() : false; @@ -1598,6 +1390,28 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } } + // Folia start - region threading + public void removeNonTickThreadPlayers() { + boolean foundToRemove = false; + for (ServerPlayerConnection conn : this.seenBy) { + if (!io.papermc.paper.util.TickThread.isTickThreadFor(conn.getPlayer())) { + foundToRemove = true; + break; + } + } + + if (!foundToRemove) { + return; + } + + for (ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) { + ServerPlayer player = conn.getPlayer(); + if (!io.papermc.paper.util.TickThread.isTickThreadFor(player)) { + this.removePlayer(player); + } + } + } + // Folia end - region threading public void updatePlayer(ServerPlayer player) { org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot @@ -1613,10 +1427,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider boolean flag = d1 <= d2 && this.entity.broadcastToPlayer(player); // CraftBukkit start - respect vanish API - if (!player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { + if (!io.papermc.paper.util.TickThread.isTickThreadFor(player) || !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Folia - region threading flag = false; } // CraftBukkit end + // Folia start - region threading + if ((this.entity instanceof ServerPlayer thisEntity) && thisEntity.broadcastedDeath) { + flag = false; + } + // Folia end - region threading if (flag) { if (this.seenBy.add(player.connection)) { this.serverEntity.addPairing(player); diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java index 88fca8b160df6804f30ed2cf8cf1f645085434e2..341650384498eebe3f7a3315c398bec994a3195b 100644 --- a/src/main/java/net/minecraft/server/level/DistanceManager.java +++ b/src/main/java/net/minecraft/server/level/DistanceManager.java @@ -200,14 +200,14 @@ public abstract class DistanceManager { public int getNaturalSpawnChunkCount() { // Paper start - use distance map to implement // note: this is the spawn chunk count - return this.chunkMap.playerChunkTickRangeMap.size(); + return this.chunkMap.level.getCurrentWorldData().mobSpawnMap.size(); // Folia - region threading // Paper end - use distance map to implement } public boolean hasPlayersNearby(long chunkPos) { // Paper start - use distance map to implement // note: this is the is spawn chunk method - return this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(chunkPos) != null; + return this.chunkMap.level.getCurrentWorldData().mobSpawnMap.getObjectsInRange(chunkPos) != null; // Folia - region threading // Paper end - use distance map to implement } diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java index 736f37979c882e41e7571202df38eb6a2923fcb0..a493d8f2677c776951fbc20ebf1e61c91b9864ee 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -61,73 +61,42 @@ public class ServerChunkCache extends ChunkSource { public final ServerChunkCache.MainThreadExecutor mainThreadProcessor; public final ChunkMap chunkMap; private final DimensionDataStorage dataStorage; - private long lastInhabitedUpdate; + //private long lastInhabitedUpdate; // Folia - region threading public boolean spawnEnemies = true; public boolean spawnFriendlies = true; private static final int CACHE_SIZE = 4; private final long[] lastChunkPos = new long[4]; private final ChunkStatus[] lastChunkStatus = new ChunkStatus[4]; private final ChunkAccess[] lastChunk = new ChunkAccess[4]; - @Nullable - @VisibleForDebug - private NaturalSpawner.SpawnState lastSpawnState; + // Folia - moved to regionised world data // 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); + // Folia - region threading + final ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable loadedChunkMap = new ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable<>(8192, 0.5f); // Folia - region threading - private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4]; + // Folia - region threading private static int getChunkCacheKey(int x, int z) { return x & 3 | ((z & 3) << 2); } public void addLoadedChunk(LevelChunk chunk) { - this.loadedChunkMapSeqLock.acquireWrite(); - try { + synchronized (this.loadedChunkMap) { // Folia - region threading 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); + } // Folia - region threading - this.lastLoadedChunks[cacheKey] = chunk; + // Folia - region threading } public void removeLoadedChunk(LevelChunk chunk) { - this.loadedChunkMapSeqLock.acquireWrite(); - try { + synchronized (this.loadedChunkMap) { // Folia - region threading 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); + } // Folia - region threading - LevelChunk cachedChunk = this.lastLoadedChunks[cacheKey]; - if (cachedChunk != null && cachedChunk.coordinateKey == chunk.coordinateKey) { - this.lastLoadedChunks[cacheKey] = null; - } + // Folia - region threading } 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; + return this.loadedChunkMap.get(ChunkPos.asLong(x, z)); // Folia - region threading } public final LevelChunk getChunkAtIfLoadedMainThreadNoCache(int x, int z) { @@ -142,7 +111,7 @@ public class ServerChunkCache extends ChunkSource { return (LevelChunk)this.getChunk(x, z, ChunkStatus.FULL, true); } - long chunkFutureAwaitCounter; // Paper - private -> package private + final java.util.concurrent.atomic.AtomicLong chunkFutureAwaitCounter = new java.util.concurrent.atomic.AtomicLong(); // Paper - private -> package private // Folia - region threading - TODO MERGE INTO CHUNK SYSTEM PATCH public void getEntityTickingChunkAsync(int x, int z, java.util.function.Consumer onLoad) { io.papermc.paper.chunk.system.ChunkSystem.scheduleTickingState( @@ -293,8 +262,7 @@ public class ServerChunkCache extends ChunkSource { 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); + // Folia - region threading // 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) { @@ -368,26 +336,7 @@ public class ServerChunkCache extends ChunkSource { public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) { long k = ChunkPos.asLong(x, z); - if (io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system - return this.getChunkAtIfLoadedMainThread(x, z); - } - - LevelChunk ret = null; - long readlock; - do { - readlock = this.loadedChunkMapSeqLock.acquireRead(); - try { - ret = this.loadedChunkMap.get(k); - } catch (Throwable thr) { - if (thr instanceof ThreadDeath) { - throw (ThreadDeath)thr; - } - // re-try, this means a CME occurred... - continue; - } - } while (!this.loadedChunkMapSeqLock.tryReleaseRead(readlock)); - - return ret; + return this.loadedChunkMap.get(k); // Folia - region threading } // Paper end // Paper start - async chunk io @@ -483,6 +432,7 @@ public class ServerChunkCache extends ChunkSource { } public CompletableFuture> getChunkFuture(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { + if (true) throw new UnsupportedOperationException(); // Folia - region threading boolean flag1 = io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system CompletableFuture completablefuture; @@ -659,10 +609,11 @@ public class ServerChunkCache extends ChunkSource { } private void tickChunks() { + io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading long i = this.level.getGameTime(); - long j = i - this.lastInhabitedUpdate; + long j = 1; // Folia - region threading - this.lastInhabitedUpdate = i; + //this.lastInhabitedUpdate = i; // Folia - region threading boolean flag = this.level.isDebug(); if (flag) { @@ -670,9 +621,11 @@ public class ServerChunkCache extends ChunkSource { } else { // Paper start - optimize isOutisdeRange ChunkMap playerChunkMap = this.chunkMap; - for (ServerPlayer player : this.level.players) { + // Folia - region threading + + for (ServerPlayer player : this.level.getLocalPlayers()) { // Folia - region threading if (!player.affectsSpawning || player.isSpectator()) { - playerChunkMap.playerMobSpawnMap.remove(player); + regionizedWorldData.mobSpawnMap.remove(player); // Folia - region threading continue; } @@ -685,8 +638,9 @@ public class ServerChunkCache extends ChunkSource { com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(player.getBukkitEntity(), (byte)chunkRange); event.callEvent(); - if (event.isCancelled() || event.getSpawnRadius() < 0 || playerChunkMap.playerChunkTickRangeMap.getLastViewDistance(player) == -1) { - playerChunkMap.playerMobSpawnMap.remove(player); + if (event.isCancelled() || event.getSpawnRadius() < 0) { // Folia - region threading + player.lastEntitySpawnRadiusSquared = -1.0; player.playerNaturallySpawnedEvent = null; // Folia - region threading + regionizedWorldData.mobSpawnMap.remove(player); // Folia - region threading continue; } @@ -694,7 +648,7 @@ public class ServerChunkCache extends ChunkSource { int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX()); int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ()); - playerChunkMap.playerMobSpawnMap.addOrUpdate(player, chunkX, chunkZ, range); + regionizedWorldData.mobSpawnMap.addOrUpdate(player, chunkX, chunkZ, range); // Folia - region threading player.lastEntitySpawnRadiusSquared = (double)((range << 4) * (range << 4)); // used in anyPlayerCloseEnoughForSpawning player.playerNaturallySpawnedEvent = event; } @@ -704,26 +658,19 @@ public class ServerChunkCache extends ChunkSource { gameprofilerfiller.push("pollingChunks"); int k = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); - boolean flag1 = level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && worlddata.getGameTime() % level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit + boolean flag1 = level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getRedstoneGameTime() % level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit // Folia - region threading gameprofilerfiller.push("naturalSpawnCount"); this.level.timings.countNaturalMobs.startTiming(); // Paper - timings int l = this.distanceManager.getNaturalSpawnChunkCount(); // Paper start - per player mob spawning NaturalSpawner.SpawnState spawnercreature_d; // moved down - if ((this.spawnFriendlies || this.spawnEnemies) && this.chunkMap.playerMobDistanceMap != null) { // don't count mobs when animals and monsters are disabled - // re-set mob counts - for (ServerPlayer player : this.level.players) { - Arrays.fill(player.mobCounts, 0); - } - spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, null, true); - } else { - spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, this.chunkMap.playerMobDistanceMap == null ? new LocalMobCapCalculator(this.chunkMap) : null, false); - } - // Paper end + // Folia start - threaded regions - revert per-player mob caps + spawnercreature_d = this.spawnFriendlies || this.spawnEnemies ? NaturalSpawner.createState(l, regionizedWorldData.getLocalEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap)) : null; // Folia - region threading + // Folia end - threaded regions - revert per-player mob caps this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings - this.lastSpawnState = spawnercreature_d; + regionizedWorldData.lastSpawnState = spawnercreature_d; // Folia - region threading gameprofilerfiller.popPush("filteringLoadedChunks"); // Paper - moved down this.level.timings.chunkTicks.startTiming(); // Paper @@ -731,17 +678,17 @@ public class ServerChunkCache extends ChunkSource { // Paper - moved down gameprofilerfiller.popPush("spawnAndTick"); - boolean flag2 = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit + boolean flag2 = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !regionizedWorldData.getLocalPlayers().isEmpty(); // CraftBukkit // Folia - region threading // Paper - only shuffle if per-player mob spawning is disabled // Paper - moved natural spawn event up // Paper start - optimise chunk tick iteration Iterator iterator1; - if (this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { - iterator1 = this.entityTickingChunks.iterator(); + if (true) { // Folia - region threading - revert per player mob caps, except for this - WTF are they doing? + iterator1 = regionizedWorldData.getEntityTickingChunks().iterator(); // Folia - region threading } else { - iterator1 = this.entityTickingChunks.unsafeIterator(); - List shuffled = Lists.newArrayListWithCapacity(this.entityTickingChunks.size()); + iterator1 = regionizedWorldData.getEntityTickingChunks().unsafeIterator(); // Folia - region threading + List shuffled = Lists.newArrayListWithCapacity(regionizedWorldData.getEntityTickingChunks().size()); // Folia - region threading while (iterator1.hasNext()) { shuffled.add(iterator1.next()); } @@ -791,14 +738,19 @@ public class ServerChunkCache extends ChunkSource { // Paper start - use set of chunks requiring updates, rather than iterating every single one loaded gameprofilerfiller.popPush("broadcast"); this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing - if (!this.chunkMap.needsChangeBroadcasting.isEmpty()) { - ReferenceOpenHashSet copy = this.chunkMap.needsChangeBroadcasting.clone(); - this.chunkMap.needsChangeBroadcasting.clear(); - for (ChunkHolder holder : copy) { + // Folia start - region threading + if (!this.level.needsChangeBroadcasting.isEmpty()) { + for (Iterator iterator = this.level.needsChangeBroadcasting.iterator(); iterator.hasNext();) { + ChunkHolder holder = iterator.next(); + if (!io.papermc.paper.util.TickThread.isTickThreadFor(holder.newChunkHolder.world, holder.pos)) { + continue; + } + // don't need to worry about chunk holder remove, as that can only be done by this tick thread + // Folia end - region threading holder.broadcastChanges(holder.getFullChunkNowUnchecked()); // LevelChunks are NEVER unloaded - if (holder.needsBroadcastChanges()) { + if (!holder.needsBroadcastChanges()) { // Folia - region threading // I DON'T want to KNOW what DUMB plugins might be doing. - this.chunkMap.needsChangeBroadcasting.add(holder); + iterator.remove(); // Folia - region threading } } } @@ -806,8 +758,8 @@ public class ServerChunkCache extends ChunkSource { gameprofilerfiller.pop(); // Paper end - use set of chunks requiring updates, rather than iterating every single one loaded // Paper start - controlled flush for entity tracker packets - List disabledFlushes = new java.util.ArrayList<>(this.level.players.size()); - for (ServerPlayer player : this.level.players) { + List disabledFlushes = new java.util.ArrayList<>(regionizedWorldData.getLocalPlayers().size()); // Folia - region threading + for (ServerPlayer player : regionizedWorldData.getLocalPlayers()) { // Folia - region threading net.minecraft.server.network.ServerGamePacketListenerImpl connection = player.connection; if (connection != null) { connection.connection.disableAutomaticFlush(); @@ -880,14 +832,19 @@ public class ServerChunkCache extends ChunkSource { @Override public void onLightUpdate(LightLayer type, SectionPos pos) { - this.mainThreadProcessor.execute(() -> { + Runnable run = () -> { // Folia - region threading ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.chunk().toLong()); if (playerchunk != null) { playerchunk.sectionLightChanged(type, pos.y()); } - }); + }; // Folia - region threading + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( + this.level, pos.getX(), pos.getZ(), run + ); + // Folia end - region threading } public void addRegionTicket(TicketType ticketType, ChunkPos pos, int radius, T argument) { @@ -959,7 +916,8 @@ public class ServerChunkCache extends ChunkSource { @Nullable @VisibleForDebug public NaturalSpawner.SpawnState getLastSpawnState() { - return this.lastSpawnState; + io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading + return worldData == null ? null : worldData.lastSpawnState; // Folia - region threading } public void removeTicketsOnClosing() { @@ -992,8 +950,43 @@ public class ServerChunkCache extends ChunkSource { return ServerChunkCache.this.mainThread; } + // Folia start - region threading + @Override + public void tell(Runnable runnable) { + if (true) { + throw new UnsupportedOperationException(); + } + super.tell(runnable); + } + + @Override + public void executeBlocking(Runnable runnable) { + if (true) { + throw new UnsupportedOperationException(); + } + super.executeBlocking(runnable); + } + + @Override + public void execute(Runnable runnable) { + if (true) { + throw new UnsupportedOperationException(); + } + super.execute(runnable); + } + + @Override + public void executeIfPossible(Runnable runnable) { + if (true) { + throw new UnsupportedOperationException(); + } + super.executeIfPossible(runnable); + } + // Folia end - region threading + @Override protected void doRunTask(Runnable task) { + if (true) throw new UnsupportedOperationException(); // Folia - region threading ServerChunkCache.this.level.getProfiler().incrementCounter("runTask"); super.doRunTask(task); } @@ -1001,11 +994,16 @@ public class ServerChunkCache extends ChunkSource { @Override // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task public boolean pollTask() { + // Folia start - region threading + if (ServerChunkCache.this.level != io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().world) { + throw new IllegalStateException("Polling tasks from non-owned region"); + } + // Folia end - region threading ServerChunkCache.this.chunkMap.level.playerChunkLoader.tickMidTick(); // Paper - replace player chunk loader if (ServerChunkCache.this.runDistanceManagerUpdates()) { return true; } - return super.pollTask() | ServerChunkCache.this.level.chunkTaskScheduler.executeMainThreadTask(); // Paper - rewrite chunk system + return io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getData().getTaskQueueData().executeChunkTask(); // Paper - rewrite chunk system // Folia - region threading } } diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index bf1a77cf9bbea4e2104b2a8c61309e740f28d51b..acc8af33ad8534d812908b0feb9a1963ee2c64fb 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -190,35 +190,34 @@ public class ServerLevel extends Level implements WorldGenLevel { public final ServerChunkCache chunkSource; private final MinecraftServer server; public final PrimaryLevelData serverLevelData; // CraftBukkit - type - final EntityTickList entityTickList; + //final EntityTickList entityTickList; // Folia - region threading //public final PersistentEntitySectionManager entityManager; // Paper - rewrite chunk system private final GameEventDispatcher gameEventDispatcher; public boolean noSave; private final SleepStatus sleepStatus; private int emptyTime; private final PortalForcer portalForcer; - private final LevelTicks blockTicks; - private final LevelTicks fluidTicks; + //private final LevelTicks blockTicks; // Folia - region threading + //private final LevelTicks fluidTicks; // Folia - region threading final Set navigatingMobs; volatile boolean isUpdatingNavigations; protected final Raids raids; - private final ObjectLinkedOpenHashSet blockEvents; - private final List blockEventsToReschedule; - private boolean handlingTick; + //private final ObjectLinkedOpenHashSet blockEvents; // Folia - region threading + //private final List blockEventsToReschedule; // Folia - region threading + //private boolean handlingTick; // Folia - region threading private final List customSpawners; @Nullable private final EndDragonFight dragonFight; final Int2ObjectMap dragonParts; private final StructureManager structureManager; private final StructureCheck structureCheck; - private final boolean tickTime; - public long lastMidTickExecuteFailure; // Paper - execute chunk tasks mid tick + public final boolean tickTime; // Folia - region threading + // Folia - region threading // CraftBukkit start public final LevelStorageSource.LevelStorageAccess convertable; public final UUID uuid; - public boolean hasPhysicsEvent = true; // Paper - public boolean hasEntityMoveEvent = false; // Paper + // Folia - region threading private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) public static Throwable getAddToWorldStackTrace(Entity entity) { final Throwable thr = new Throwable(entity + " Added to world at " + new java.util.Date()); @@ -254,6 +253,13 @@ public class ServerLevel extends Level implements WorldGenLevel { ServerChunkCache chunkProvider = this.getChunkSource(); + // Folia start - region threading + // don't let players move into regions not owned + if (!io.papermc.paper.util.TickThread.isTickThreadFor(this, minChunkX, minChunkZ, maxChunkX, maxChunkZ)) { + return false; + } + // Folia end - region threading + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) { @@ -265,50 +271,64 @@ public class ServerLevel extends Level implements WorldGenLevel { 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; - } + // Folia start - region threading - TODO rebase + public final void loadChunksAsync(BlockPos pos, int radiusBlocks, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, + java.util.function.Consumer> onLoad) { + loadChunksAsync( + (pos.getX() - radiusBlocks) >> 4, + (pos.getX() + radiusBlocks) >> 4, + (pos.getZ() - radiusBlocks) >> 4, + (pos.getZ() + radiusBlocks) >> 4, + priority, onLoad + ); + } + + public final void loadChunksAsync(BlockPos pos, int radiusBlocks, + net.minecraft.world.level.chunk.ChunkStatus chunkStatus, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, + java.util.function.Consumer> onLoad) { + loadChunksAsync( + (pos.getX() - radiusBlocks) >> 4, + (pos.getX() + radiusBlocks) >> 4, + (pos.getZ() - radiusBlocks) >> 4, + (pos.getZ() + radiusBlocks) >> 4, + chunkStatus, priority, onLoad + ); + } + + public final void loadChunksAsync(int minChunkX, int maxChunkX, int minChunkZ, int maxChunkZ, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, + java.util.function.Consumer> onLoad) { + this.loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, net.minecraft.world.level.chunk.ChunkStatus.FULL, priority, onLoad); + } + + public final void loadChunksAsync(int minChunkX, int maxChunkX, int minChunkZ, int maxChunkZ, + net.minecraft.world.level.chunk.ChunkStatus chunkStatus, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, + java.util.function.Consumer> onLoad) { 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]; + java.util.concurrent.atomic.AtomicInteger loadedChunks = new java.util.concurrent.atomic.AtomicInteger(); + + Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter.getAndIncrement()); - Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++); + int ticketLevel = 33 + net.minecraft.world.level.chunk.ChunkStatus.getDistance(chunkStatus); 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) { + if (loadedChunks.incrementAndGet() == 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); @@ -320,11 +340,31 @@ public class ServerLevel extends Level implements WorldGenLevel { 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 + this, cx, cz, chunkStatus, true, priority, consumer ); } } } + // Folia end - region threading - TODO rebase + + public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, + java.util.function.Consumer> onLoad) { + // Folia - region threading - TODO MERGE INTO CHUNK SYSTEM PATCH + + 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; + + this.loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, priority, onLoad); // Folia - region threading - move into own function TODO rebase + } // Paper start - rewrite chunk system public final io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler chunkTaskScheduler; @@ -444,81 +484,16 @@ public class ServerLevel extends Level implements WorldGenLevel { // Paper end // Paper start - optimise checkDespawn - public final List playersAffectingSpawning = new java.util.ArrayList<>(); + // Folia - region threading // Paper end - optimise checkDespawn // Paper start - optimise get nearest players for entity AI - @Override - public final ServerPlayer getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions condition, @Nullable LivingEntity source, - double centerX, double centerY, double centerZ) { - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby; - nearby = this.getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(Mth.floor(centerX) >> 4, Mth.floor(centerZ) >> 4); - - if (nearby == null) { - return null; - } - - Object[] backingSet = nearby.getBackingSet(); - - double closestDistanceSquared = Double.MAX_VALUE; - ServerPlayer closest = null; - - for (int i = 0, len = backingSet.length; i < len; ++i) { - Object _player = backingSet[i]; - if (!(_player instanceof ServerPlayer)) { - continue; - } - ServerPlayer player = (ServerPlayer)_player; - - double distanceSquared = player.distanceToSqr(centerX, centerY, centerZ); - if (distanceSquared < closestDistanceSquared && condition.test(source, player)) { - closest = player; - closestDistanceSquared = distanceSquared; - } - } - - return closest; - } - - @Override - public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions pathfindertargetcondition, LivingEntity entityliving) { - return this.getNearestPlayer(pathfindertargetcondition, entityliving, entityliving.getX(), entityliving.getY(), entityliving.getZ()); - } - - @Override - public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions pathfindertargetcondition, - double d0, double d1, double d2) { - return this.getNearestPlayer(pathfindertargetcondition, null, d0, d1, d2); - } - - @Override - public List getNearbyPlayers(net.minecraft.world.entity.ai.targeting.TargetingConditions condition, LivingEntity source, AABB axisalignedbb) { - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby; - double centerX = (axisalignedbb.maxX + axisalignedbb.minX) * 0.5; - double centerZ = (axisalignedbb.maxZ + axisalignedbb.minZ) * 0.5; - nearby = this.getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(Mth.floor(centerX) >> 4, Mth.floor(centerZ) >> 4); + // Folia - region threading - List ret = new java.util.ArrayList<>(); + // Folia - region threading - if (nearby == null) { - return ret; - } - - Object[] backingSet = nearby.getBackingSet(); - - for (int i = 0, len = backingSet.length; i < len; ++i) { - Object _player = backingSet[i]; - if (!(_player instanceof ServerPlayer)) { - continue; - } - ServerPlayer player = (ServerPlayer)_player; - - if (axisalignedbb.contains(player.getX(), player.getY(), player.getZ()) && condition.test(source, player)) { - ret.add(player); - } - } + // Folia - region threading - return ret; - } + // Folia - region threading // Paper end - optimise get nearest players for entity AI public final io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader playerChunkLoader = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader(this); @@ -563,6 +538,59 @@ public class ServerLevel extends Level implements WorldGenLevel { }); } + // Folia start - regionised ticking + public final io.papermc.paper.threadedregions.TickRegions tickRegions = new io.papermc.paper.threadedregions.TickRegions(); + public final io.papermc.paper.threadedregions.ThreadedRegionizer regioniser; + { + this.regioniser = new io.papermc.paper.threadedregions.ThreadedRegionizer<>( + 3*9, + (2.0 / 3.0), + 1, + 1, + io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(), + this, + this.tickRegions + ); + } + public final io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData taskQueueRegionData = new io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData(this); + public static final int WORLD_INIT_NOT_CHECKED = 0; + public static final int WORLD_INIT_CHECKING = 1; + public static final int WORLD_INIT_CHECKED = 2; + public final java.util.concurrent.atomic.AtomicInteger checkInitialised = new java.util.concurrent.atomic.AtomicInteger(WORLD_INIT_NOT_CHECKED); + public ChunkPos randomSpawnSelection; + + public static final record PendingTeleport(Entity.EntityTreeNode rootVehicle, Vec3 to) {} + private final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet pendingTeleports = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); + + public void pushPendingTeleport(final PendingTeleport teleport) { + synchronized (this.pendingTeleports) { + this.pendingTeleports.add(teleport); + } + } + + public boolean removePendingTeleport(final PendingTeleport teleport) { + synchronized (this.pendingTeleports) { + return this.pendingTeleports.remove(teleport); + } + } + + public List removeAllRegionTeleports() { + final List ret = new ArrayList<>(); + + synchronized (this.pendingTeleports) { + for (final Iterator iterator = this.pendingTeleports.iterator(); iterator.hasNext();) { + final PendingTeleport pendingTeleport = iterator.next(); + if (io.papermc.paper.util.TickThread.isTickThreadFor(this, pendingTeleport.to())) { + ret.add(pendingTeleport); + iterator.remove(); + } + } + } + + return ret; + } + // Folia end - regionised ticking + // 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 @@ -574,13 +602,13 @@ public class ServerLevel extends Level implements WorldGenLevel { this.convertable = convertable_conversionsession; this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelDirectory.path().toFile()); // CraftBukkit end - this.players = Lists.newArrayList(); - this.entityTickList = new EntityTickList(); - this.blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); - this.fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); + this.players = new java.util.concurrent.CopyOnWriteArrayList<>(); // Folia - region threading + //this.entityTickList = new EntityTickList(); // Folia - region threading + //this.blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); // Folia - moved to RegioniedWorldData + //this.fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); // Folia - moved to RegioniedWorldData this.navigatingMobs = new ObjectOpenHashSet(); - this.blockEvents = new ObjectLinkedOpenHashSet(); - this.blockEventsToReschedule = new ArrayList(64); + //this.blockEvents = new ObjectLinkedOpenHashSet(); // Folia - moved to RegioniedWorldData + //this.blockEventsToReschedule = new ArrayList(64); // Folia - moved to RegioniedWorldData this.dragonParts = new Int2ObjectOpenHashMap(); this.tickTime = flag1; this.server = minecraftserver; @@ -619,7 +647,7 @@ public class ServerLevel extends Level implements WorldGenLevel { }); this.chunkSource.getGeneratorState().ensureStructuresGenerated(); this.portalForcer = new PortalForcer(this); - this.updateSkyBrightness(); + //this.updateSkyBrightness(); // Folia - region threading - delay until first tick this.prepareWeather(); this.getWorldBorder().setAbsoluteMaxSize(minecraftserver.getAbsoluteMaxWorldSize()); this.raids = (Raids) this.getDataStorage().computeIfAbsent((nbttagcompound) -> { @@ -647,7 +675,14 @@ public class ServerLevel extends Level implements WorldGenLevel { this.chunkTaskScheduler = new io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler(this, io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.workerThreads); // Paper - rewrite chunk system this.entityLookup = new io.papermc.paper.chunk.system.entity.EntityLookup(this, new EntityCallbacks()); // Paper - rewrite chunk system + this.updateTickData(); // Folia - region threading - make sure it is initialised before ticked + } + + // Folia start - region threading + public void updateTickData() { + this.tickData = new io.papermc.paper.threadedregions.RegionizedServer.WorldLevelData(this, this.serverLevelData.getGameTime(), this.serverLevelData.getDayTime()); } + // Folia end - region threading public void setWeatherParameters(int clearDuration, int rainDuration, boolean raining, boolean thundering) { this.serverLevelData.setClearWeatherTime(clearDuration); @@ -666,55 +701,31 @@ public class ServerLevel extends Level implements WorldGenLevel { return this.structureManager; } - public void tick(BooleanSupplier shouldKeepTicking) { - // Paper start - optimise checkDespawn - this.playersAffectingSpawning.clear(); - for (ServerPlayer player : this.players) { - if (net.minecraft.world.entity.EntitySelector.PLAYER_AFFECTS_SPAWNING.test(player)) { - this.playersAffectingSpawning.add(player); - } - } - // Paper end - optimise checkDespawn + public void tick(BooleanSupplier shouldKeepTicking, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - regionised ticking + final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking + // Folia - region threading ProfilerFiller gameprofilerfiller = this.getProfiler(); - this.handlingTick = true; + regionizedWorldData.setHandlingTick(true); // Folia - regionised ticking gameprofilerfiller.push("world border"); - this.getWorldBorder().tick(); + if (region == null) this.getWorldBorder().tick(); // Folia - regionised ticking - moved into global tick gameprofilerfiller.popPush("weather"); - this.advanceWeatherCycle(); - int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); + if (region == null) this.advanceWeatherCycle(); + //int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); // Folia - region threading - move intotickSleep long j; - if (this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { - // CraftBukkit start - j = this.levelData.getDayTime() + 24000L; - TimeSkipEvent event = new TimeSkipEvent(this.getWorld(), TimeSkipEvent.SkipReason.NIGHT_SKIP, (j - j % 24000L) - this.getDayTime()); - if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { - getCraftServer().getPluginManager().callEvent(event); - if (!event.isCancelled()) { - this.setDayTime(this.getDayTime() + event.getSkipAmount()); - } - } - - if (!event.isCancelled()) { - this.wakeUpAllPlayers(); - } - // CraftBukkit end - if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) { - this.resetWeatherCycle(); - } - } + if (region == null) this.tickSleep(); // Folia - region threading - this.updateSkyBrightness(); + if (region == null) this.updateSkyBrightness(); // Folia - region threading this.tickTime(); gameprofilerfiller.popPush("tickPending"); timings.scheduledBlocks.startTiming(); // Paper if (!this.isDebug()) { - j = this.getGameTime(); + j = regionizedWorldData.getRedstoneGameTime(); // Folia - region threading gameprofilerfiller.push("blockTicks"); - this.blockTicks.tick(j, 65536, this::tickBlock); + regionizedWorldData.getBlockLevelTicks().tick(j, 65536, this::tickBlock); // Folia - region ticking gameprofilerfiller.popPush("fluidTicks"); - this.fluidTicks.tick(j, 65536, this::tickFluid); + regionizedWorldData.getFluidLevelTicks().tick(j, 65536, this::tickFluid); // Folia - region ticking gameprofilerfiller.pop(); } timings.scheduledBlocks.stopTiming(); // Paper @@ -731,7 +742,7 @@ public class ServerLevel extends Level implements WorldGenLevel { timings.doSounds.startTiming(); // Spigot this.runBlockEvents(); timings.doSounds.stopTiming(); // Spigot - this.handlingTick = false; + regionizedWorldData.setHandlingTick(false); // Folia - regionised ticking gameprofilerfiller.pop(); boolean flag = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players @@ -743,20 +754,30 @@ public class ServerLevel extends Level implements WorldGenLevel { gameprofilerfiller.push("entities"); timings.tickEntities.startTiming(); // Spigot if (this.dragonFight != null) { + if (io.papermc.paper.util.TickThread.isTickThreadFor(this, 0, 0)) { // Folia - region threading gameprofilerfiller.push("dragonFight"); this.dragonFight.tick(); gameprofilerfiller.pop(); + } else { // Folia start - region threading + // try to load dragon fight + ChunkPos fightCenter = new ChunkPos(0, 0); + this.chunkSource.addTicketAtLevel( + TicketType.UNKNOWN, fightCenter, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + fightCenter + ); + } // Folia end - region threading } org.spigotmc.ActivationRange.activateEntities(this); // Spigot timings.entityTick.startTiming(); // Spigot - this.entityTickList.forEach((entity) -> { + regionizedWorldData.forEachTickingEntity((entity) -> { // Folia - regionised ticking if (!entity.isRemoved()) { if (false && this.shouldDiscardEntity(entity)) { // CraftBukkit - We prevent spawning in general, so this butchering is not needed entity.discard(); } else { gameprofilerfiller.push("checkDespawn"); entity.checkDespawn(); + if (entity.isRemoved()) return; // Folia - region threading - if we despawned, DON'T TICK IT! gameprofilerfiller.pop(); if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - now always true if in the ticking list Entity entity1 = entity.getVehicle(); @@ -787,6 +808,31 @@ public class ServerLevel extends Level implements WorldGenLevel { gameprofilerfiller.pop(); } + // Folia start - region threading + public void tickSleep() { + int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); long j; // Folia moved from tick loop + if (this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { // Folia - region threading - moved to global tick + // CraftBukkit start + j = this.levelData.getDayTime() + 24000L; + TimeSkipEvent event = new TimeSkipEvent(this.getWorld(), TimeSkipEvent.SkipReason.NIGHT_SKIP, (j - j % 24000L) - this.getDayTime()); + if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { + getCraftServer().getPluginManager().callEvent(event); + if (!event.isCancelled()) { + this.setDayTime(this.getDayTime() + event.getSkipAmount()); + } + } + + if (!event.isCancelled()) { + this.wakeUpAllPlayers(); + } + // CraftBukkit end + if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) { + this.resetWeatherCycle(); + } + } + } + // Folia end - region threading + @Override public boolean shouldTickBlocksAt(long chunkPos) { // Paper start - replace player chunk loader system @@ -797,11 +843,12 @@ public class ServerLevel extends Level implements WorldGenLevel { protected void tickTime() { if (this.tickTime) { - long i = this.levelData.getGameTime() + 1L; + io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - region threading + long i = regionizedWorldData.getRedstoneGameTime() + 1L; // Folia - region threading - this.serverLevelData.setGameTime(i); - this.serverLevelData.getScheduledEvents().tick(this.server, i); - if (this.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { + regionizedWorldData.setRedstoneGameTime(i); // Folia - region threading + if (false) this.serverLevelData.getScheduledEvents().tick(this.server, i); // Folia - region threading - TODO any way to bring this in? + if (false && this.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { // Folia - region threading this.setDayTime(this.levelData.getDayTime() + 1L); } @@ -830,15 +877,23 @@ public class ServerLevel extends Level implements WorldGenLevel { private void wakeUpAllPlayers() { this.sleepStatus.removeAllSleepers(); (this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList())).forEach((entityplayer) -> { // CraftBukkit - decompile error - entityplayer.stopSleepInBed(false, false); + // Folia start - region threading + entityplayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { + if (player.level != ServerLevel.this || !player.isSleeping()) { + return; + } + player.stopSleepInBed(false, false); + }, null, 1L); + // Folia end - region threading }); } // Paper start - optimise random block ticking - private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos(); - private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong()); + private final ThreadLocal chunkTickMutablePosition = ThreadLocal.withInitial(() -> new BlockPos.MutableBlockPos()); // Folia - region threading + private final ThreadLocal randomTickRandom = ThreadLocal.withInitial(() -> new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong())); // Folia - region threading // Paper end public void tickChunk(LevelChunk chunk, int randomTickSpeed) { + io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = this.randomTickRandom.get(); // Folia - region threading ChunkPos chunkcoordintpair = chunk.getPos(); boolean flag = this.isRaining(); int j = chunkcoordintpair.getMinBlockX(); @@ -846,7 +901,7 @@ public class ServerLevel extends Level implements WorldGenLevel { ProfilerFiller gameprofilerfiller = this.getProfiler(); gameprofilerfiller.push("thunder"); - final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change + final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition.get(); // Paper - use mutable to reduce allocation rate, final to force compile fail on change // Folia - region threading if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - disable thunder blockposition.set(this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15))); // Paper @@ -940,7 +995,7 @@ public class ServerLevel extends Level implements WorldGenLevel { int yPos = (sectionIndex + minSection) << 4; for (int a = 0; a < randomTickSpeed; ++a) { int tickingBlocks = section.tickingList.size(); - int index = this.randomTickRandom.nextInt(16 * 16 * 16); + int index = randomTickRandom.nextInt(16 * 16 * 16); // Folia - region threading if (index >= tickingBlocks) { continue; } @@ -954,7 +1009,7 @@ public class ServerLevel extends Level implements WorldGenLevel { BlockPos blockposition2 = blockposition.set(j + randomX, randomY, k + randomZ); BlockState iblockdata = com.destroystokyo.paper.util.maplist.IBlockDataList.getBlockDataFromRaw(raw); - iblockdata.randomTick(this, blockposition2, this.randomTickRandom); + iblockdata.randomTick(this, blockposition2, randomTickRandom); // Folia - region threading // We drop the fluid tick since LAVA is ALREADY TICKED by the above method (See LiquidBlock). // TODO CHECK ON UPDATE (ping the Canadian) } @@ -1008,7 +1063,7 @@ public class ServerLevel extends Level implements WorldGenLevel { } public boolean isHandlingTick() { - return this.handlingTick; + return this.getCurrentWorldData().isHandlingTick(); // Folia - regionised ticking } public boolean canSleepThroughNights() { @@ -1040,6 +1095,14 @@ public class ServerLevel extends Level implements WorldGenLevel { } public void updateSleepingPlayerList() { + // Folia start - region threading + if (!io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + ServerLevel.this.updateSleepingPlayerList(); + }); + return; + } + // Folia end - region threading if (!this.players.isEmpty() && this.sleepStatus.update(this.players)) { this.announceSleepStatus(); } @@ -1051,7 +1114,7 @@ public class ServerLevel extends Level implements WorldGenLevel { return this.server.getScoreboard(); } - private void advanceWeatherCycle() { + public void advanceWeatherCycle() { // Folia - region threading - public boolean flag = this.isRaining(); if (this.dimensionType().hasSkyLight()) { @@ -1137,23 +1200,24 @@ public class ServerLevel extends Level implements WorldGenLevel { this.server.getPlayerList().broadcastAll(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.THUNDER_LEVEL_CHANGE, this.thunderLevel)); } // */ - for (int idx = 0; idx < this.players.size(); ++idx) { - if (((ServerPlayer) this.players.get(idx)).level == this) { - ((ServerPlayer) this.players.get(idx)).tickWeather(); + ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Folia - region threading + for (ServerPlayer player : players) { // Folia - region threading + if (player.level == this) { // Folia - region threading + player.tickWeather(); // Folia - region threading } } if (flag != this.isRaining()) { // Only send weather packets to those affected - for (int idx = 0; idx < this.players.size(); ++idx) { - if (((ServerPlayer) this.players.get(idx)).level == this) { - ((ServerPlayer) this.players.get(idx)).setPlayerWeather((!flag ? WeatherType.DOWNFALL : WeatherType.CLEAR), false); + for (ServerPlayer player : players) { // Folia - region threading + if (player.level == this) { // Folia - region threading + player.setPlayerWeather((!flag ? WeatherType.DOWNFALL : WeatherType.CLEAR), false); // Folia - region threading } } } - for (int idx = 0; idx < this.players.size(); ++idx) { - if (((ServerPlayer) this.players.get(idx)).level == this) { - ((ServerPlayer) this.players.get(idx)).updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel); + for (ServerPlayer player : players) { // Folia - region threading + if (player.level == this) { // Folia - region threading + player.updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel); // Folia - region threading } } // CraftBukkit end @@ -1217,7 +1281,7 @@ public class ServerLevel extends Level implements WorldGenLevel { public void tickNonPassenger(Entity entity) { // Paper start - log detailed entity tick information - io.papermc.paper.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); + io.papermc.paper.util.TickThread.ensureTickThread(entity, "Cannot tick an entity off-main"); // Folia - region threading try { if (currentlyTickingEntity.get() == null) { currentlyTickingEntity.lazySet(entity); @@ -1250,7 +1314,16 @@ public class ServerLevel extends Level implements WorldGenLevel { if (isActive) { // Paper - EAR 2 TimingHistory.activatedEntityTicks++; entity.tick(); - entity.postTick(); // CraftBukkit + // Folia start - region threading + if (!io.papermc.paper.util.TickThread.isTickThreadFor(entity)) { + // removed from region while ticking + return; + } + if (entity.doPortalLogic()) { + // portalled + return; + } + // Folia end - region threading } else { entity.inactiveTick(); } // Paper - EAR 2 this.getProfiler().pop(); } finally { timer.stopTiming(); } // Paper - timings @@ -1273,7 +1346,7 @@ public class ServerLevel extends Level implements WorldGenLevel { private void tickPassenger(Entity vehicle, Entity passenger) { if (!passenger.isRemoved() && passenger.getVehicle() == vehicle) { - if (passenger instanceof Player || this.entityTickList.contains(passenger)) { + if (passenger instanceof Player || this.getCurrentWorldData().hasEntityTickingEntity(passenger)) { // Folia - region threading // Paper - EAR 2 final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(passenger); co.aikar.timings.Timing timer = isActive ? passenger.getType().passengerTickTimer.startTiming() : passenger.getType().passengerInactiveTickTimer.startTiming(); // Paper @@ -1290,7 +1363,16 @@ public class ServerLevel extends Level implements WorldGenLevel { // Paper start - EAR 2 if (isActive) { passenger.rideTick(); - passenger.postTick(); // CraftBukkit + // Folia start - region threading + if (!io.papermc.paper.util.TickThread.isTickThreadFor(passenger)) { + // removed from region while ticking + return; + } + if (passenger.doPortalLogic()) { + // portalled + return; + } + // Folia end - region threading } else { passenger.setDeltaMovement(Vec3.ZERO); passenger.inactiveTick(); @@ -1378,7 +1460,15 @@ public class ServerLevel extends Level implements WorldGenLevel { // Paper - rewrite chunk system - entity saving moved into ChunkHolder } else if (close) { chunkproviderserver.close(false); } // Paper - rewrite chunk system + // Folia - move into saveLevelData() + } + public void saveLevelData() { // Folia - region threading + if (this.dragonFight != null) { + this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit + } + // Folia start - region threading + // moved from save // CraftBukkit start - moved from MinecraftServer.saveChunks ServerLevel worldserver1 = this; @@ -1386,12 +1476,7 @@ public class ServerLevel extends Level implements WorldGenLevel { this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save()); this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); // CraftBukkit end - } - - private void saveLevelData() { - if (this.dragonFight != null) { - this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit - } + // Folia end - region threading this.getChunkSource().getDataStorage().save(); } @@ -1446,6 +1531,19 @@ public class ServerLevel extends Level implements WorldGenLevel { return list; } + // Folia start - region threading + @Nullable + public ServerPlayer getRandomLocalPlayer() { + List list = this.getLocalPlayers(); + list = new java.util.ArrayList<>(list); + list.removeIf((ServerPlayer player) -> { + return !player.isAlive(); + }); + + return list.isEmpty() ? null : (ServerPlayer) list.get(this.random.nextInt(list.size())); + } + // Folia end - region threading + @Nullable public ServerPlayer getRandomPlayer() { List list = this.getPlayers(LivingEntity::isAlive); @@ -1547,8 +1645,8 @@ public class ServerLevel extends Level implements WorldGenLevel { } else { if (entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity && itemEntity.getItem().isEmpty()) return false; // Paper - Prevent empty items from being added // Paper start - capture all item additions to the world - if (captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) { - captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity); + if (this.getCurrentWorldData().captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) { // Folia - region threading + this.getCurrentWorldData().captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity); // Folia - region threading return true; } // Paper end @@ -1687,7 +1785,7 @@ public class ServerLevel extends Level implements WorldGenLevel { @Override public void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags) { - if (this.isUpdatingNavigations) { + if (false && this.isUpdatingNavigations) { // Folia - region threading String s = "recursive call to sendBlockUpdated"; Util.logAndPauseIfInIde("recursive call to sendBlockUpdated", new IllegalStateException("recursive call to sendBlockUpdated")); @@ -1700,7 +1798,7 @@ public class ServerLevel extends Level implements WorldGenLevel { if (Shapes.joinIsNotEmpty(voxelshape, voxelshape1, BooleanOp.NOT_SAME)) { List list = new ObjectArrayList(); - Iterator iterator = this.navigatingMobs.iterator(); + Iterator iterator = this.getCurrentWorldData().getNavigatingMobs(); // Folia - region threading while (iterator.hasNext()) { // CraftBukkit start - fix SPIGOT-6362 @@ -1723,7 +1821,7 @@ public class ServerLevel extends Level implements WorldGenLevel { } try { - this.isUpdatingNavigations = true; + //this.isUpdatingNavigations = true; // Folia - region threading iterator = list.iterator(); while (iterator.hasNext()) { @@ -1732,7 +1830,7 @@ public class ServerLevel extends Level implements WorldGenLevel { navigationabstract1.recomputePath(); } } finally { - this.isUpdatingNavigations = false; + //this.isUpdatingNavigations = false; // Folia - region threading } } @@ -1741,23 +1839,23 @@ public class ServerLevel extends Level implements WorldGenLevel { @Override public void updateNeighborsAt(BlockPos pos, Block sourceBlock) { - if (captureBlockStates) { return; } // Paper - Cancel all physics during placement - this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, (Direction) null); + if (this.getCurrentWorldData().captureBlockStates) { return; } // Paper - Cancel all physics during placement // Folia - region threading + this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, (Direction) null); // Folia - region threading } @Override public void updateNeighborsAtExceptFromFacing(BlockPos pos, Block sourceBlock, Direction direction) { - this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, direction); + this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, direction); // Folia - region threading } @Override public void neighborChanged(BlockPos pos, Block sourceBlock, BlockPos sourcePos) { - this.neighborUpdater.neighborChanged(pos, sourceBlock, sourcePos); + this.getCurrentWorldData().neighborUpdater.neighborChanged(pos, sourceBlock, sourcePos); // Folia - region threading } @Override public void neighborChanged(BlockState state, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) { - this.neighborUpdater.neighborChanged(state, pos, sourceBlock, sourcePos, notify); + this.getCurrentWorldData().neighborUpdater.neighborChanged(state, pos, sourceBlock, sourcePos, notify); // Folia - region threading } @Override @@ -1788,7 +1886,7 @@ public class ServerLevel extends Level implements WorldGenLevel { explosion.clearToBlow(); } - Iterator iterator = this.players.iterator(); + Iterator iterator = this.getLocalPlayers().iterator(); // Folia - region thraeding while (iterator.hasNext()) { ServerPlayer entityplayer = (ServerPlayer) iterator.next(); @@ -1803,25 +1901,28 @@ public class ServerLevel extends Level implements WorldGenLevel { @Override public void blockEvent(BlockPos pos, Block block, int type, int data) { - this.blockEvents.add(new BlockEventData(pos, block, type, data)); + this.getCurrentWorldData().pushBlockEvent(new BlockEventData(pos, block, type, data)); // Folia - regionised ticking } private void runBlockEvents() { - this.blockEventsToReschedule.clear(); + List blockEventsToReschedule = new ArrayList<>(64); // Folia - regionised ticking - while (!this.blockEvents.isEmpty()) { - BlockEventData blockactiondata = (BlockEventData) this.blockEvents.removeFirst(); + // Folia start - regionised ticking + io.papermc.paper.threadedregions.RegionizedWorldData worldRegionData = this.getCurrentWorldData(); + BlockEventData blockactiondata; + while ((blockactiondata = worldRegionData.removeFirstBlockEvent()) != null) { + // Folia end - regionised ticking if (this.shouldTickBlocksAt(blockactiondata.pos())) { if (this.doBlockEvent(blockactiondata)) { this.server.getPlayerList().broadcast((Player) null, (double) blockactiondata.pos().getX(), (double) blockactiondata.pos().getY(), (double) blockactiondata.pos().getZ(), 64.0D, this.dimension(), new ClientboundBlockEventPacket(blockactiondata.pos(), blockactiondata.block(), blockactiondata.paramA(), blockactiondata.paramB())); } } else { - this.blockEventsToReschedule.add(blockactiondata); + blockEventsToReschedule.add(blockactiondata); // Folia - regionised ticking } } - this.blockEvents.addAll(this.blockEventsToReschedule); + worldRegionData.pushBlockEvents(blockEventsToReschedule); // Folia - regionised ticking } private boolean doBlockEvent(BlockEventData event) { @@ -1832,12 +1933,12 @@ public class ServerLevel extends Level implements WorldGenLevel { @Override public LevelTicks getBlockTicks() { - return this.blockTicks; + return this.getCurrentWorldData().getBlockLevelTicks(); // Folia - region ticking } @Override public LevelTicks getFluidTicks() { - return this.fluidTicks; + return this.getCurrentWorldData().getFluidLevelTicks(); // Folia - region ticking } @Nonnull @@ -1861,7 +1962,7 @@ public class ServerLevel extends Level implements WorldGenLevel { public int sendParticles(ServerPlayer sender, T t0, double d0, double d1, double d2, int i, double d3, double d4, double d5, double d6, boolean force) { // Paper start - Particle API Expansion - return sendParticles(players, sender, t0, d0, d1, d2, i, d3, d4, d5, d6, force); + return sendParticles(this.getLocalPlayers(), sender, t0, d0, d1, d2, i, d3, d4, d5, d6, force); // Folia - region threading } public int sendParticles(List receivers, ServerPlayer sender, T t0, double d0, double d1, double d2, int i, double d3, double d4, double d5, double d6, boolean force) { // Paper end @@ -1914,7 +2015,14 @@ public class ServerLevel extends Level implements WorldGenLevel { public Entity getEntityOrPart(int id) { Entity entity = (Entity) this.getEntities().get(id); - return entity != null ? entity : (Entity) this.dragonParts.get(id); + // Folia start - region threading + if (entity != null) { + return entity; + } + synchronized (this.dragonParts) { + return this.dragonParts.get(id); + } + // Folia end - region threading } @Nullable @@ -1922,6 +2030,61 @@ public class ServerLevel extends Level implements WorldGenLevel { return (Entity) this.getEntities().get(uuid); } + // Folia start - region threading + private final java.util.concurrent.atomic.AtomicLong nonFullSyncLoadIdGenerator = new java.util.concurrent.atomic.AtomicLong(); + + private ChunkAccess getIfAboveStatus(int chunkX, int chunkZ, net.minecraft.world.level.chunk.ChunkStatus status) { + io.papermc.paper.chunk.system.scheduling.NewChunkHolder loaded = + this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkX, chunkZ); + io.papermc.paper.chunk.system.scheduling.NewChunkHolder.ChunkCompletion loadedCompletion; + if (loaded != null && (loadedCompletion = loaded.getLastChunkCompletion()) != null && loadedCompletion.genStatus().isOrAfter(status)) { + return loadedCompletion.chunk(); + } + + return null; + } + + @Override + public ChunkAccess syncLoadNonFull(int chunkX, int chunkZ, net.minecraft.world.level.chunk.ChunkStatus status) { + if (status == null || status.isOrAfter(net.minecraft.world.level.chunk.ChunkStatus.FULL)) { + throw new IllegalArgumentException("Status: " + status.getName()); + } + ChunkAccess loaded = this.getIfAboveStatus(chunkX, chunkZ, status); + if (loaded != null) { + return loaded; + } + + Long ticketId = Long.valueOf(this.nonFullSyncLoadIdGenerator.getAndIncrement()); + int ticketLevel = 33 + net.minecraft.world.level.chunk.ChunkStatus.getDistance(status); + this.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel( + TicketType.NON_FULL_SYNC_LOAD, chunkX, chunkZ, ticketLevel, ticketId + ); + this.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); + + this.chunkTaskScheduler.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING); + + // we could do a simple spinwait here, since we do not need to process tasks while performing this load + // but we process tasks only because it's a better use of the time spent + this.chunkSource.mainThreadProcessor.managedBlock(() -> { + return ServerLevel.this.getIfAboveStatus(chunkX, chunkZ, status) != null; + }); + + loaded = ServerLevel.this.getIfAboveStatus(chunkX, chunkZ, status); + if (loaded == null) { + throw new IllegalStateException("Expected chunk to be loaded for status " + status); + } + + // let the next process ticket updates call pick this up + this.chunkTaskScheduler.chunkHolderManager.pushDelayedTicketUpdate( + io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.TicketOperation.removeOp( + chunkX, chunkZ, TicketType.NON_FULL_SYNC_LOAD, ticketLevel, ticketId + ) + ); + + return loaded; + } + // Folia end - region threading + @Nullable public BlockPos findNearestMapStructure(TagKey structureTag, BlockPos pos, int radius, boolean skipReferencedStructures) { if (!this.serverLevelData.worldGenOptions().generateStructures()) { // CraftBukkit @@ -2074,6 +2237,7 @@ public class ServerLevel extends Level implements WorldGenLevel { } public boolean setChunkForced(int x, int z, boolean forced) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify force loaded chunks off of the global region"); // Folia - region threading ForcedChunksSavedData forcedchunk = (ForcedChunksSavedData) this.getDataStorage().computeIfAbsent(ForcedChunksSavedData::load, ForcedChunksSavedData::new, "chunks"); ChunkPos chunkcoordintpair = new ChunkPos(x, z); long k = chunkcoordintpair.toLong(); @@ -2082,7 +2246,7 @@ public class ServerLevel extends Level implements WorldGenLevel { if (forced) { flag1 = forcedchunk.getChunks().add(k); if (flag1) { - this.getChunk(x, z); + //this.getChunk(x, z); // Folia - region threading - we must let the chunk load asynchronously } } else { flag1 = forcedchunk.getChunks().remove(k); @@ -2110,13 +2274,18 @@ public class ServerLevel extends Level implements WorldGenLevel { BlockPos blockposition1 = pos.immutable(); optional.ifPresent((holder) -> { - this.getServer().execute(() -> { + Runnable run = () -> { // Folia - region threading this.getPoiManager().remove(blockposition1); DebugPackets.sendPoiRemovedPacket(this, blockposition1); - }); + }; // Folia - region threading + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( + this, blockposition1.getX() >> 4, blockposition1.getZ() >> 4, run + ); + // Folia end - region threading }); optional1.ifPresent((holder) -> { - this.getServer().execute(() -> { + Runnable run = () -> { // Folia - region threading // Paper start if (optional.isEmpty() && this.getPoiManager().exists(blockposition1, poiType -> true)) { this.getPoiManager().remove(blockposition1); @@ -2124,7 +2293,12 @@ public class ServerLevel extends Level implements WorldGenLevel { // Paper end this.getPoiManager().add(blockposition1, holder); DebugPackets.sendPoiAddedPacket(this, blockposition1); - }); + }; // Folia - region threading + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( + this, blockposition1.getX() >> 4, blockposition1.getZ() >> 4, run + ); + // Folia end - region threading }); } } @@ -2171,7 +2345,7 @@ public class ServerLevel extends Level implements WorldGenLevel { BufferedWriter bufferedwriter = Files.newBufferedWriter(path.resolve("stats.txt")); try { - bufferedwriter.write(String.format(Locale.ROOT, "spawning_chunks: %d\n", playerchunkmap.getDistanceManager().getNaturalSpawnChunkCount())); + //bufferedwriter.write(String.format(Locale.ROOT, "spawning_chunks: %d\n", playerchunkmap.getDistanceManager().getNaturalSpawnChunkCount())); // Folia - region threading NaturalSpawner.SpawnState spawnercreature_d = this.getChunkSource().getLastSpawnState(); if (spawnercreature_d != null) { @@ -2185,7 +2359,7 @@ public class ServerLevel extends Level implements WorldGenLevel { } bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityLookup.getDebugInfo())); // Paper - rewrite chunk system - bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); + //bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); // Folia - region threading bufferedwriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count())); bufferedwriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count())); bufferedwriter.write("distance_manager: " + playerchunkmap.getDistanceManager().getDebugStatus() + "\n"); @@ -2331,7 +2505,7 @@ public class ServerLevel extends Level implements WorldGenLevel { private void dumpBlockEntityTickers(Writer writer) throws IOException { CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("y").addColumn("z").addColumn("type").build(writer); - Iterator iterator = this.blockEntityTickers.iterator(); + Iterator iterator = null; // Folia - region threading while (iterator.hasNext()) { TickingBlockEntity tickingblockentity = (TickingBlockEntity) iterator.next(); @@ -2344,7 +2518,7 @@ public class ServerLevel extends Level implements WorldGenLevel { @VisibleForTesting public void clearBlockEvents(BoundingBox box) { - this.blockEvents.removeIf((blockactiondata) -> { + this.getCurrentWorldData().removeIfBlockEvents((blockactiondata) -> { // Folia - regionised ticking return box.isInside(blockactiondata.pos()); }); } @@ -2353,7 +2527,7 @@ public class ServerLevel extends Level implements WorldGenLevel { public void blockUpdated(BlockPos pos, Block block) { if (!this.isDebug()) { // CraftBukkit start - if (populating) { + if (this.getCurrentWorldData().populating) { // Folia - region threading return; } // CraftBukkit end @@ -2396,9 +2570,7 @@ public class ServerLevel extends Level implements WorldGenLevel { @VisibleForTesting public String getWatchdogStats() { - return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.entityLookup.getDebugInfo(), ServerLevel.getTypeCount(this.entityLookup.getAll(), (entity) -> { // Paper - rewrite chunk system - return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); - }), this.blockEntityTickers.size(), ServerLevel.getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), this.getBlockTicks().count(), this.getFluidTicks().count(), this.gatherChunkSourceStats()); + return "region threading"; // Folia - region threading } private static String getTypeCount(Iterable items, Function classifier) { @@ -2431,6 +2603,12 @@ public class ServerLevel extends Level implements WorldGenLevel { public static void makeObsidianPlatform(ServerLevel worldserver, Entity entity) { // CraftBukkit end BlockPos blockposition = ServerLevel.END_SPAWN_POINT; + // Folia start - region threading + makeObsidianPlatform(worldserver, entity, blockposition); + } + + public static void makeObsidianPlatform(ServerLevel worldserver, Entity entity, BlockPos blockposition) { + // Folia end - region threading int i = blockposition.getX(); int j = blockposition.getY() - 2; int k = blockposition.getZ(); @@ -2443,11 +2621,7 @@ public class ServerLevel extends Level implements WorldGenLevel { BlockPos.betweenClosed(i - 2, j, k - 2, i + 2, j, k + 2).forEach((blockposition1) -> { blockList.setBlock(blockposition1, Blocks.OBSIDIAN.defaultBlockState(), 3); }); - org.bukkit.World bworld = worldserver.getWorld(); - org.bukkit.event.world.PortalCreateEvent portalEvent = new org.bukkit.event.world.PortalCreateEvent((List) (List) blockList.getList(), bworld, (entity == null) ? null : entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.END_PLATFORM); - - worldserver.getCraftServer().getPluginManager().callEvent(portalEvent); - if (!portalEvent.isCancelled()) { + if (true) { // Folia - region threading blockList.updateList(); } // CraftBukkit end @@ -2468,13 +2642,14 @@ public class ServerLevel extends Level implements WorldGenLevel { } public void startTickingChunk(LevelChunk chunk) { - chunk.unpackTicks(this.getLevelData().getGameTime()); + chunk.unpackTicks(this.getRedstoneGameTime()); // Folia - region threading } public void onStructureStartsAvailable(ChunkAccess chunk) { - this.server.execute(() -> { - this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts()); - }); + // Folia start - region threading + // no longer needs to be on main + this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts()); + // Folia end - region threading } @Override @@ -2496,7 +2671,7 @@ public class ServerLevel extends Level implements WorldGenLevel { // Paper end - rewrite chunk system } - private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { + public boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { // Folia - region threaded - make public // Paper start - optimize is ticking ready type functions io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkPos); // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded @@ -2544,16 +2719,16 @@ public class ServerLevel extends Level implements WorldGenLevel { public void onCreated(Entity entity) {} public void onDestroyed(Entity entity) { - ServerLevel.this.getScoreboard().entityRemoved(entity); + // ServerLevel.this.getScoreboard().entityRemoved(entity); // Folia - region threading } public void onTickingStart(Entity entity) { if (entity instanceof net.minecraft.world.entity.Marker) return; // Paper - Don't tick markers - ServerLevel.this.entityTickList.add(entity); + ServerLevel.this.getCurrentWorldData().addEntityTickingEntity(entity); // Folia - region threading } public void onTickingEnd(Entity entity) { - ServerLevel.this.entityTickList.remove(entity); + ServerLevel.this.getCurrentWorldData().removeEntityTickingEntity(entity); // Folia - region threading // Paper start - Reset pearls when they stop being ticked if (paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && entity instanceof net.minecraft.world.entity.projectile.ThrownEnderpearl pearl) { pearl.cachedOwner = null; @@ -2581,7 +2756,7 @@ public class ServerLevel extends Level implements WorldGenLevel { Util.logAndPauseIfInIde("onTrackingStart called during navigation iteration", new IllegalStateException("onTrackingStart called during navigation iteration")); } - ServerLevel.this.navigatingMobs.add(entityinsentient); + ServerLevel.this.getCurrentWorldData().addNavigatingMob(entityinsentient); // Folia - region threading } if (entity instanceof EnderDragon) { @@ -2592,7 +2767,9 @@ public class ServerLevel extends Level implements WorldGenLevel { for (int j = 0; j < i; ++j) { EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; + synchronized (ServerLevel.this.dragonParts) { // Folia - region threading ServerLevel.this.dragonParts.put(entitycomplexpart.getId(), entitycomplexpart); + } // Folia - region threading } } @@ -2618,11 +2795,18 @@ public class ServerLevel extends Level implements WorldGenLevel { { com.google.common.collect.Streams.stream( ServerLevel.this.getServer().getAllLevels() ).map( ServerLevel::getDataStorage ).forEach( (worldData) -> { - for (Object o : worldData.cache.values() ) + // Folia start - make map data thread-safe + List worldDataCache; + synchronized (worldData.cache) { + worldDataCache = new java.util.ArrayList<>(worldData.cache.values()); + } + for (Object o : worldDataCache ) + // Folia end - make map data thread-safe { if ( o instanceof MapItemSavedData ) { MapItemSavedData map = (MapItemSavedData) o; + synchronized (map) { // Folia - make map data thread-safe map.carriedByPlayers.remove( (Player) entity ); for ( Iterator iter = (Iterator) map.carriedBy.iterator(); iter.hasNext(); ) { @@ -2632,6 +2816,7 @@ public class ServerLevel extends Level implements WorldGenLevel { iter.remove(); } } + } // Folia - make map data thread-safe } } } ); @@ -2666,7 +2851,7 @@ public class ServerLevel extends Level implements WorldGenLevel { Util.logAndPauseIfInIde("onTrackingStart called during navigation iteration", new IllegalStateException("onTrackingStart called during navigation iteration")); } - ServerLevel.this.navigatingMobs.remove(entityinsentient); + ServerLevel.this.getCurrentWorldData().removeNavigatingMob(entityinsentient); // Folia - region threading } if (entity instanceof EnderDragon) { @@ -2677,13 +2862,16 @@ public class ServerLevel extends Level implements WorldGenLevel { for (int j = 0; j < i; ++j) { EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; + synchronized (ServerLevel.this.dragonParts) { // Folia - region threading ServerLevel.this.dragonParts.remove(entitycomplexpart.getId()); + } // Folia - region threading } } entity.updateDynamicGameEventListener(DynamicGameEventListener::remove); // CraftBukkit start entity.valid = false; + // Folia - region threading - TODO THIS SHIT if (!(entity instanceof ServerPlayer)) { for (ServerPlayer player : ServerLevel.this.players) { player.getBukkitEntity().onEntityRemove(entity); diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java index 4231c3fe14a621d237bdc3d56c593f0adc0a7bc6..5f53c9ad5d64053a2e29c58ddb62e7d2bd176efe 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -188,7 +188,7 @@ import org.bukkit.inventory.MainHand; public class ServerPlayer extends Player { private static final Logger LOGGER = LogUtils.getLogger(); - public long lastSave = MinecraftServer.currentTick; // Paper + public long lastSave = Long.MIN_VALUE; // Paper // Folia - threaded regions private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32; private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10; public ServerGamePacketListenerImpl connection; @@ -249,11 +249,7 @@ public class ServerPlayer extends Player { public boolean queueHealthUpdatePacket = false; public net.minecraft.network.protocol.game.ClientboundSetHealthPacket queuedHealthUpdatePacket; // Paper end - // Paper start - mob spawning rework - public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length; - public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper - public final com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet cachedSingleMobDistanceMap; - // Paper end + // Folia - region threading - revert per player mob caps // CraftBukkit start public String displayName; @@ -415,7 +411,7 @@ public class ServerPlayer extends Player { this.adventure$displayName = net.kyori.adventure.text.Component.text(this.getScoreboardName()); // Paper this.bukkitPickUpLoot = true; this.maxHealthCache = this.getMaxHealth(); - this.cachedSingleMobDistanceMap = new com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper + // Folia - region threading - revert per player mob caps } // Yes, this doesn't match Vanilla, but it's the best we can do for now. @@ -457,11 +453,11 @@ public class ServerPlayer extends Player { } // CraftBukkit end - public void fudgeSpawnLocation(ServerLevel world) { - BlockPos blockposition = world.getSharedSpawnPos(); + public static void fudgeSpawnLocation(ServerLevel world, ServerPlayer player, ca.spottedleaf.concurrentutil.completable.Completable toComplete) { // Folia - region threading + BlockPos blockposition = world.getSharedSpawnPos(); final BlockPos spawnPos = blockposition; // Folia - region threading if (world.dimensionType().hasSkyLight() && world.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit - int i = Math.max(0, this.server.getSpawnRadius(world)); + int i = Math.max(0, MinecraftServer.getServer().getSpawnRadius(world)); // Folia - region threading int j = Mth.floor(world.getWorldBorder().getDistanceToBorder((double) blockposition.getX(), (double) blockposition.getZ())); if (j < i) { @@ -475,33 +471,76 @@ public class ServerPlayer extends Player { long k = (long) (i * 2 + 1); long l = k * k; int i1 = l > 2147483647L ? Integer.MAX_VALUE : (int) l; - int j1 = this.getCoprime(i1); + int j1 = getCoprime(i1); // Folia - region threading int k1 = RandomSource.create().nextInt(i1); - for (int l1 = 0; l1 < i1; ++l1) { - int i2 = (k1 + j1 * l1) % i1; - int j2 = i2 % (i * 2 + 1); - int k2 = i2 / (i * 2 + 1); - BlockPos blockposition1 = PlayerRespawnLogic.getOverworldRespawnPos(world, blockposition.getX() + j2 - i, blockposition.getZ() + k2 - i); - - if (blockposition1 != null) { - this.moveTo(blockposition1, 0.0F, 0.0F); - if (world.noCollision(this, this.getBoundingBox(), true)) { // Paper - make sure this loads chunks, we default to NOT loading now - break; - } + // Folia start - region threading + int[] l1 = new int[1]; + final int finalI = i; + Runnable attempt = new Runnable() { + @Override + public void run() { + int i2 = (k1 + j1 * l1[0]) % i1; + int j2 = i2 % (finalI * 2 + 1); + int k2 = i2 / (finalI * 2 + 1); + int x = blockposition.getX() + j2 - finalI; + int z = blockposition.getZ() + k2 - finalI; + + world.loadChunksForMoveAsync(player.getBoundingBoxAt(x + 0.5, 0, z + 0.5), + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, + (c) -> { + BlockPos blockposition1 = PlayerRespawnLogic.getOverworldRespawnPos(world, x, z); + if (blockposition1 != null) { + AABB aabb = player.getBoundingBoxAt(blockposition1.getX() + 0.5, blockposition1.getY(), blockposition1.getZ() + 0.5); + if (world.noCollision(player, aabb, true)) { // Paper - make sure this loads chunks, we default to NOT loading now + toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(blockposition1), world.levelData.getSpawnAngle(), 0.0f)); + return; + } + } + if (++l1[0] >= i1) { + LOGGER.warn("Found no spawn in radius for player " + player.getName() + ", selecting set spawn point " + spawnPos + " in world '" + world.getWorld().getKey() + "'"); + // if we return null, then no chunks may be loaded. but this call requires to return a location with + // loaded chunks, so we need to return something (vanilla does not do this logic, it assumes + // something is returned always) + // we can just return the set spawn position + world.loadChunksForMoveAsync(player.getBoundingBoxAt(spawnPos.getX() + 0.5, spawnPos.getY(), spawnPos.getZ() + 0.5), + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, + (c0) -> { + toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(spawnPos), world.levelData.getSpawnAngle(), 0.0f)); + } + ); + return; + } else { + this.run(); + } + } + ); } - } + }; + attempt.run(); + // Folia end - region threading } else { - this.moveTo(blockposition, 0.0F, 0.0F); - - while (!world.noCollision(this, this.getBoundingBox(), true) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { // Paper - make sure this loads chunks, we default to NOT loading now - this.setPos(this.getX(), this.getY() + 1.0D, this.getZ()); - } + // Folia start - region threading + world.loadChunksForMoveAsync(player.getBoundingBoxAt(blockposition.getX() + 0.5, blockposition.getY(), blockposition.getZ() + 0.5), + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, + (c) -> { + BlockPos ret = blockposition; + while (!world.noCollision(player, player.getBoundingBoxAt(ret.getX() + 0.5, ret.getY(), ret.getZ() + 0.5), true) && ret.getY() < (double) (world.getMaxBuildHeight() - 1)) { // Paper - make sure this loads chunks, we default to NOT loading now + ret = ret.above(); + } + toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(ret), world.levelData.getSpawnAngle(), 0.0f)); + } + ); + // Folia end - region threading } } - private int getCoprime(int horizontalSpawnArea) { + // Folia start - region threading + public final java.util.Set pendingTpas = java.util.concurrent.ConcurrentHashMap.newKeySet(); + // Folia end - region threading + + private static int getCoprime(int horizontalSpawnArea) { // Folia - region threading - not static return horizontalSpawnArea <= 16 ? horizontalSpawnArea - 1 : 17; } @@ -1160,6 +1199,343 @@ public class ServerPlayer extends Player { } } + // Folia start - region threading + /** + * Teleport flag indicating that the player is to be respawned, expected to only be used + * internally for {@link #respawn(java.util.function.Consumer)}. + */ + public static final long TELEPORT_FLAGS_PLAYER_RESPAWN = Long.MIN_VALUE >>> 0; + /** + * Teleport flag indicating the player should be placed at the highest y-value that + * provides no collisions for the player's bounding box. Note that this setting + * does not imply {@link Entity#TELEPORT_FLAG_LOAD_CHUNK}, so it may + * sync load chunks unless the load chunk flag is provided. + */ + public static final long TELEPORT_FLAGS_AVOID_SUFFOCATION = Long.MIN_VALUE >>> 1; + + private void avoidSuffocation() { + while (!this.getLevel().noCollision(this, this.getBoundingBox(), true) && this.getY() < (double)this.getLevel().getMaxBuildHeight()) { // Folia - make sure this loads chunks, we default to NOT loading now + this.setPos(this.getX(), this.getY() + 1.0D, this.getZ()); + } + } + + public void exitEndCredits() { + if (!this.wonGame) { + // not in the end credits anymore + return; + } + this.wonGame = false; + + this.respawn((player) -> { + CriteriaTriggers.CHANGED_DIMENSION.trigger(player, Level.END, Level.OVERWORLD); + }, true); + } + + public void respawn(java.util.function.Consumer respawnComplete) { + this.respawn(respawnComplete, false); + } + + private void respawn(java.util.function.Consumer respawnComplete, boolean alive) { + io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot respawn entity async"); + + this.getBukkitEntity(); // force bukkit entity to be created before TPing + + if (alive != this.isAlive()) { + throw new IllegalStateException("isAlive expected = " + alive); + } + + if (!this.hasNullCallback()) { + this.unRide(); + } + + if (this.isVehicle() || this.isPassenger()) { + throw new IllegalStateException("Dead player should not be a vehicle or passenger"); + } + + ServerLevel origin = this.getLevel(); + ServerLevel respawnWorld = this.server.getLevel(this.getRespawnDimension()); + + // modified based off PlayerList#respawn + + EntityTreeNode passengerTree = this.makePassengerTree(); + + this.isChangingDimension = true; + // must be manually removed from connections + this.getLevel().getCurrentWorldData().connections.remove(this.connection.connection); + origin.removePlayerImmediately(this, RemovalReason.CHANGED_DIMENSION); + + BlockPos respawnPos = this.getRespawnPosition(); + float respawnAngle = this.getRespawnAngle(); + boolean isRespawnForced = this.isRespawnForced(); + + ca.spottedleaf.concurrentutil.completable.Completable spawnPosComplete = + new ca.spottedleaf.concurrentutil.completable.Completable<>(); + boolean[] usedRespawnAnchor = new boolean[1]; + + // set up post spawn location logic + spawnPosComplete.addWaiter((spawnLoc, throwable) -> { + // update pos and velocity + ServerPlayer.this.setPosRaw(spawnLoc.getX(), spawnLoc.getY(), spawnLoc.getZ()); + ServerPlayer.this.setYRot(spawnLoc.getYaw()); + ServerPlayer.this.setYHeadRot(spawnLoc.getYaw()); + ServerPlayer.this.setXRot(spawnLoc.getPitch()); + ServerPlayer.this.setDeltaMovement(Vec3.ZERO); + // placeInAsync will update the world + + this.placeInAsync( + origin, + // use the load chunk flag just in case the spawn loc isn't loaded, and to ensure the chunks + // stay loaded for a bit with the teleport ticket + ((CraftWorld)spawnLoc.getWorld()).getHandle(), + TELEPORT_FLAG_LOAD_CHUNK | TELEPORT_FLAGS_PLAYER_RESPAWN | TELEPORT_FLAGS_AVOID_SUFFOCATION, + passengerTree, // note: we expect this to just be the player, no passengers + (entity) -> { + // reset player if needed + // only after placing, so that we don't trip thread checks + if (!alive) { + ServerPlayer.this.reset(); + } + // now the player is in the world, and can receive sound + if (usedRespawnAnchor[0]) { + ServerPlayer.this.connection.send( + new ClientboundSoundPacket( + net.minecraft.sounds.SoundEvents.RESPAWN_ANCHOR_DEPLETE, SoundSource.BLOCKS, + ServerPlayer.this.getX(), ServerPlayer.this.getY(), ServerPlayer.this.getZ(), + 1.0F, 1.0F, ServerPlayer.this.getLevel().getRandom().nextLong() + ) + ); + } + // now the respawn logic is complete + + // last, call the function callback + if (respawnComplete != null) { + respawnComplete.accept(ServerPlayer.this); + } + } + ); + }); + + // find and modify respawn block state + if (respawnWorld == null || respawnPos == null) { + // default to regular spawn + fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete); + } else { + // load chunk for block + // give at least 1 radius of loaded chunks so that we do not sync load anything + int radiusBlocks = 16; + respawnWorld.loadChunksAsync(respawnPos, radiusBlocks, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, + (chunks) -> { + Vec3 spawnPos = findRespawnPositionAndUseSpawnBlock( + respawnWorld, respawnPos, respawnAngle, isRespawnForced, alive + ).orElse(null); + if (spawnPos == null) { + // no spawn + ServerPlayer.this.connection.send( + new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F) + ); + ServerPlayer.this.setRespawnPosition( + null, null, 0f, false, false, + com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN + ); + // default to regular spawn + fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete); + return; + } + + boolean isRespawnAnchor = respawnWorld.getBlockState(respawnPos).is(Blocks.RESPAWN_ANCHOR); + boolean isBed = respawnWorld.getBlockState(respawnPos).is(net.minecraft.tags.BlockTags.BEDS); + usedRespawnAnchor[0] = !alive && isRespawnAnchor; + + // determine angle + float locAngle; + if (!isBed && !isRespawnAnchor) { + // something else + locAngle = respawnAngle; + } else { + // select angle in direction of the difference applied to respawn pos? + Vec3 vec3d1 = Vec3.atBottomCenterOf(respawnPos).subtract(spawnPos).normalize(); + + locAngle = (float) Mth.wrapDegrees(Mth.atan2(vec3d1.z, vec3d1.x) * 57.2957763671875D - 90.0D); + } + ServerPlayer.this.setRespawnPosition( + respawnWorld.dimension(), respawnPos, respawnAngle, isRespawnForced, false, + com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN + ); + + // finished now, pass the location on + spawnPosComplete.complete( + io.papermc.paper.util.MCUtil.toLocation(respawnWorld, spawnPos, locAngle, 0.0f) + ); + return; + } + ); + } + } + + @Override + protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) { + if (yaw != null) { + this.setYRot(yaw.floatValue()); + this.setYHeadRot(yaw.floatValue()); + } + if (pitch != null) { + this.setXRot(pitch.floatValue()); + } + if (speedDirectionUpdate != null) { + this.setDeltaMovement(speedDirectionUpdate.normalize().scale(this.getDeltaMovement().length())); + } + this.connection.internalTeleport(pos.x, pos.y, pos.z, this.getYRot(), this.getXRot(), java.util.Collections.emptySet()); + this.connection.resetPosition(); + } + + @Override + protected ServerPlayer transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) { + // must be manually removed from connections + this.getLevel().getCurrentWorldData().connections.remove(this.connection.connection); + this.getLevel().removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION); + + this.spawnIn(destination); + this.transform(pos, yaw, pitch, speedDirectionUpdate); + + return this; + } + + @Override + public void preChangeDimension() { + super.preChangeDimension(); + this.stopUsingItem(); + } + + @Override + protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) { + if (destination == originWorld && (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L) { + this.unsetRemoved(); + destination.addDuringTeleport(this); + + // must be manually added to connections + this.getLevel().getCurrentWorldData().connections.add(this.connection.connection); + + if ((teleportFlags & TELEPORT_FLAGS_AVOID_SUFFOCATION) != 0L && treeNode.passengers == null && treeNode.parent == null) { + this.avoidSuffocation(); + } + + // required to set up the pending teleport stuff to the client, and to actually update + // the player's position clientside + this.connection.internalTeleport( + this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot(), + java.util.Collections.emptySet() + ); + this.connection.resetPosition(); + + this.postChangeDimension(); + } else { + // Modelled after PlayerList#respawn + + // We avoid checking for disconnection here, which means we do not have to add/remove from + // the player list here. We can let this be properly handled by the connection handler + + // pre-add logic + PlayerList playerlist = this.server.getPlayerList(); + net.minecraft.world.level.storage.LevelData worlddata = destination.getLevelData(); + this.connection.send( + new ClientboundRespawnPacket( + destination.dimensionTypeId(), destination.dimension(), + BiomeManager.obfuscateSeed(destination.getSeed()), + this.gameMode.getGameModeForPlayer(), + this.gameMode.getPreviousGameModeForPlayer(), + destination.isDebug(), destination.isFlat(), + // if we do not want to respawn, we aren't dead + (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L ? (byte)1 : (byte)0, + this.getLastDeathLocation() + ) + ); + // don't bother with the chunk cache radius and simulation distance packets, they are handled + // by the chunk loader + this.spawnIn(destination); // important that destination != null + // we can delay teleport until later, the player position is already set up at the target + this.setShiftKeyDown(false); + + this.connection.send(new net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket( + destination.getSharedSpawnPos(), destination.getSharedSpawnAngle() + )); + this.connection.send(new ClientboundChangeDifficultyPacket( + worlddata.getDifficulty(), worlddata.isDifficultyLocked() + )); + this.connection.send(new ClientboundSetExperiencePacket( + this.experienceProgress, this.totalExperience, this.experienceLevel + )); + + playerlist.sendLevelInfo(this, destination); + playerlist.sendPlayerPermissionLevel(this); + + // regular world add logic + this.unsetRemoved(); + destination.addDuringTeleport(this); + + // must be manually added to connections + this.getLevel().getCurrentWorldData().connections.add(this.connection.connection); + + if ((teleportFlags & TELEPORT_FLAGS_AVOID_SUFFOCATION) != 0L && treeNode.passengers == null && treeNode.parent == null) { + this.avoidSuffocation(); + } + + // required to set up the pending teleport stuff to the client, and to actually update + // the player's position clientside + this.connection.internalTeleport( + this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot(), + java.util.Collections.emptySet() + ); + this.connection.resetPosition(); + + // delay callback until after post add logic + + // post add logic + + // "Added from changeDimension" + this.setHealth(this.getHealth()); + playerlist.sendAllPlayerInfo(this); + this.onUpdateAbilities(); + for (MobEffectInstance mobEffect : this.getActiveEffects()) { + this.connection.send(new ClientboundUpdateMobEffectPacket(this.getId(), mobEffect)); + } + + this.triggerDimensionChangeTriggers(originWorld); + + // finished + + this.postChangeDimension(); + } + } + + @Override + public boolean endPortalLogicAsync() { + io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); + + if (this.getLevel().getTypeKey() == LevelStem.END) { + if (!this.canPortalAsync(false)) { + return false; + } + this.wonGame = true; + // TODO is there a better solution to this that DOESN'T skip the credits? + this.seenCredits = true; + this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.WIN_GAME, this.seenCredits ? 0.0F : 1.0F)); + this.exitEndCredits(); + return true; + } else { + return super.endPortalLogicAsync(); + } + } + + @Override + protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) { + super.prePortalLogic(origin, destination, type); + if (origin.getTypeKey() == LevelStem.OVERWORLD && destination.getTypeKey() == LevelStem.NETHER) { + this.enteredNetherPosition = this.position(); + } + } + // Folia end - region threading + @Nullable @Override public Entity changeDimension(ServerLevel destination) { @@ -1169,6 +1545,11 @@ public class ServerPlayer extends Player { @Nullable public Entity changeDimension(ServerLevel worldserver, PlayerTeleportEvent.TeleportCause cause) { + // Folia start - region threading + if (true) { + throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); + } + // Folia end - region threading // CraftBukkit end if (this.isSleeping()) return this; // CraftBukkit - SPIGOT-3154 // this.isChangingDimension = true; // CraftBukkit - Moved down and into PlayerList#changeDimension @@ -2109,6 +2490,12 @@ public class ServerPlayer extends Player { public void setCamera(@Nullable Entity entity) { Entity entity1 = this.getCamera(); + // Folia start - region threading + if (entity != null && !io.papermc.paper.util.TickThread.isTickThreadFor(entity)) { + return; + } + // Folia end - region threading + this.camera = (Entity) (entity == null ? this : entity); if (entity1 != this.camera) { // Paper start - Add PlayerStartSpectatingEntityEvent and PlayerStopSpectatingEntity Event @@ -2291,7 +2678,7 @@ public class ServerPlayer extends Player { } public void untrackChunk(ChunkPos chunkPos) { - if (this.isAlive()) { + if (true || this.isAlive()) { // Folia - region threading - always untrack chunk now that the player is retained in the world after death this.connection.send(new ClientboundForgetLevelChunkPacket(chunkPos.x, chunkPos.z)); // Paper start if(io.papermc.paper.event.packet.PlayerChunkUnloadEvent.getHandlerList().getRegisteredListeners().length > 0){ @@ -2570,7 +2957,7 @@ public class ServerPlayer extends Player { this.experienceLevel = this.newLevel; this.totalExperience = this.newTotalExp; this.experienceProgress = 0; - this.deathTime = 0; + this.deathTime = 0; this.broadcastedDeath = false; // Folia - region threading this.setArrowCount(0, true); // CraftBukkit - ArrowBodyCountChangeEvent this.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.DEATH); this.effectsDirty = true; diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java index 1d33c02088c150189d7f4b0aa27f6a1de96b11cf..59a9a9633ac924c606564b75298fae22f1ffd4ec 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java @@ -123,11 +123,11 @@ public class ServerPlayerGameMode { } public void tick() { - this.gameTicks = MinecraftServer.currentTick; // CraftBukkit; + ++this.gameTicks; // CraftBukkit; // Folia - region threading BlockState iblockdata; if (this.hasDelayedDestroy) { - iblockdata = this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper + iblockdata = !io.papermc.paper.util.TickThread.isTickThreadFor(this.level, this.delayedDestroyPos) ? null : this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper // Folia - region threading - don't destroy blocks not owned if (iblockdata == null || iblockdata.isAir()) { // Paper this.hasDelayedDestroy = false; } else { @@ -140,7 +140,7 @@ public class ServerPlayerGameMode { } } else if (this.isDestroyingBlock) { // Paper start - don't want to do same logic as above, return instead - iblockdata = this.level.getBlockStateIfLoaded(this.destroyPos); + iblockdata = !io.papermc.paper.util.TickThread.isTickThreadFor(this.level, this.destroyPos) ? null : this.level.getBlockStateIfLoaded(this.destroyPos); // Folia - region threading - don't destroy blocks not owned if (iblockdata == null) { this.isDestroyingBlock = false; return; @@ -416,7 +416,7 @@ public class ServerPlayerGameMode { } else { // CraftBukkit start org.bukkit.block.BlockState state = bblock.getState(); - level.captureDrops = new ArrayList<>(); + level.getCurrentWorldData().captureDrops = new ArrayList<>(); // Folia - region threading // CraftBukkit end block.playerWillDestroy(this.level, pos, iblockdata, this.player); boolean flag = this.level.removeBlock(pos, false); @@ -444,8 +444,8 @@ public class ServerPlayerGameMode { // return true; // CraftBukkit } // CraftBukkit start - java.util.List itemsToDrop = level.captureDrops; // Paper - store current list - level.captureDrops = null; // Paper - Remove this earlier so that we can actually drop stuff + java.util.List itemsToDrop = level.getCurrentWorldData().captureDrops; // Paper - store current list // Folia - region threading + level.getCurrentWorldData().captureDrops = null; // Paper - Remove this earlier so that we can actually drop stuff // Folia - region threading if (event.isDropItems()) { org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDropItemEvent(bblock, state, this.player, itemsToDrop); // Paper - use stored ref } diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java index 660693c6dc0ef86f4013df980b6d0c11c03e46cd..b64fc5adda3a9ab793a074d08df822754bd8d186 100644 --- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java @@ -98,10 +98,15 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl this.chunkMap.level.chunkTaskScheduler.lightExecutor.queueRunnable(() -> { // Paper - rewrite chunk system this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> { chunkLightCallback.accept(chunkPos); - ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> { + Runnable run = () -> { // Folia - region threading ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()).broadcast(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(chunkPos, ThreadedLevelLightEngine.this, null, null, true), false); ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, io.papermc.paper.util.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos)); - }); + }; // Folia - region threading + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( + (ServerLevel)this.theLightEngine.getWorld(), chunkPos.x, chunkPos.z, run + ); + // Folia end - region threading }, onComplete); }); this.tryScheduleUpdate(); @@ -109,7 +114,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl return totalChunks; } - private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); + //private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); // Folia - region threading private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, final Supplier> runnable) { final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld(); @@ -128,11 +133,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl return; } - if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) { + if (!io.papermc.paper.util.TickThread.isTickThreadFor(world, chunkX, chunkZ)) { // Folia - region threading // ticket logic is not safe to run off-main, re-schedule - world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> { + Runnable run = () -> { // Folia - region threading this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable); - }); + }; // Folia - region threading + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + world, chunkX, chunkZ, run + ); + // Folia end - region threading return; } @@ -145,22 +155,28 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl return; } - final int references = this.chunksBeingWorkedOn.addTo(key, 1); + final int references = this.chunkMap.level.getCurrentWorldData().chunksBeingWorkedOn.addTo(key, 1); // Folia - region threading if (references == 0) { final ChunkPos pos = new ChunkPos(chunkX, chunkZ); world.getChunkSource().addRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); } - updateFuture.thenAcceptAsync((final Void ignore) -> { - final int newReferences = this.chunksBeingWorkedOn.get(key); - if (newReferences == 1) { - this.chunksBeingWorkedOn.remove(key); - final ChunkPos pos = new ChunkPos(chunkX, chunkZ); - world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); - } else { - this.chunksBeingWorkedOn.put(key, newReferences - 1); - } - }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> { + // Folia start - region threading + updateFuture.thenAccept((final Void ignore) -> { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + this.chunkMap.level, chunkX, chunkZ, () -> { + final int newReferences = this.chunkMap.level.getCurrentWorldData().chunksBeingWorkedOn.get(key); + if (newReferences == 1) { + this.chunkMap.level.getCurrentWorldData().chunksBeingWorkedOn.remove(key); + final ChunkPos pos = new ChunkPos(chunkX, chunkZ); + world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); + } else { + this.chunkMap.level.getCurrentWorldData().chunksBeingWorkedOn.put(key, newReferences - 1); + } + } + ); + }).whenComplete((final Void ignore, final Throwable thr) -> { + // Folia end - region threading if (thr != null) { LOGGER.error("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr); } diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java index 97d1ff2af23bac14e67bca5896843325aaa5bfc1..8bede935166cfe0defbc7bb1e959bd91f2cd1c6e 100644 --- a/src/main/java/net/minecraft/server/level/TicketType.java +++ b/src/main/java/net/minecraft/server/level/TicketType.java @@ -35,6 +35,15 @@ public class TicketType { public static final TicketType POI_LOAD = create("poi_load", Long::compareTo); public static final TicketType UNLOAD_COOLDOWN = create("unload_cooldown", (u1, u2) -> 0, 5 * 20); // Paper end - rewrite chunk system + // Folia start - region threading + public static final TicketType LOGIN = create("login", (u1, u2) -> 0, 20); + public static final TicketType DELAYED = create("delay", (u1, u2) -> 0, 5); + public static final TicketType END_GATEWAY_EXIT_SEARCH = create("end_gateway_exit_search", Long::compareTo); + public static final TicketType NON_FULL_SYNC_LOAD = create("non_full_sync_load", Long::compareTo); + public static final TicketType NETHER_PORTAL_DOUBLE_CHECK = create("nether_portal_double_check", Long::compareTo); + public static final TicketType TELEPORT_HOLD_TICKET = create("teleport_hold_ticket", Long::compareTo); + public static final TicketType REGION_SCHEDULER_API_HOLD = create("region_scheduler_api_hold", (a, b) -> 0); + // Folia end - region threading public static TicketType create(String name, Comparator argumentComparator) { return new TicketType<>(name, argumentComparator, 0L); diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java index 877498729c66de9aa6a27c9148f7494d7895615c..d8af2d59fb1f112f2f1a9fdbb3517fc72a2e572d 100644 --- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java +++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java @@ -84,6 +84,13 @@ public class WorldGenRegion implements WorldGenLevel { private final AtomicLong subTickCount = new AtomicLong(); private static final ResourceLocation WORLDGEN_REGION_RANDOM = new ResourceLocation("worldgen_region_random"); + // Folia start - region threading + @Override + public StructureManager structureManager() { + return this.structureManager; + } + // Folia end - region threading + public WorldGenRegion(ServerLevel world, List chunks, ChunkStatus status, int placementRadius) { this.generatingStatus = status; this.writeRadiusCutoff = placementRadius; diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java index 44d99e89226adb6234b9405f25ac9dab9bd84297..072634e26d32ca0b3438a5d3a03be3670d0f846e 100644 --- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java +++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java @@ -154,10 +154,13 @@ public class ServerConnectionListener { // Paper end //ServerConnectionListener.this.connections.add(object); - pending.add(object); // Paper + // Folia - connection fixes - move down channelpipeline.addLast("packet_handler", (ChannelHandler) object); ((Connection) object).setListener(new ServerHandshakePacketListenerImpl(ServerConnectionListener.this.server, (Connection) object)); io.papermc.paper.network.ChannelInitializeListenerHolder.callListeners(channel); // Paper + // Folia start - regionised threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addConnection((Connection)object); + // Folia end - regionised threading } }).group((EventLoopGroup) lazyinitvar.get()).localAddress(address)).option(ChannelOption.AUTO_READ, false).bind().syncUninterruptibly()); // CraftBukkit // Paper } @@ -218,7 +221,7 @@ public class ServerConnectionListener { // Spigot Start this.addPending(); // Paper // This prevents players from 'gaming' the server, and strategically relogging to increase their position in the tick order - if ( org.spigotmc.SpigotConfig.playerShuffle > 0 && MinecraftServer.currentTick % org.spigotmc.SpigotConfig.playerShuffle == 0 ) + if ( org.spigotmc.SpigotConfig.playerShuffle > 0 && 0 % org.spigotmc.SpigotConfig.playerShuffle == 0 ) // Folia - region threading { Collections.shuffle( this.connections ); } diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java index d587b2c4e39bce7e098aa9fab361230f72770658..870a4a6c2a1fa0a3ae438c5ae1360cf312de6277 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -322,10 +322,10 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic private final org.bukkit.craftbukkit.CraftServer cserver; public boolean processedDisconnect; - private int lastTick = MinecraftServer.currentTick; + private long lastTick = Util.getMillis() / 50L; // Folia - region threading private int allowedPlayerTicks = 1; - private int lastDropTick = MinecraftServer.currentTick; - private int lastBookTick = MinecraftServer.currentTick; + private long lastDropTick = Util.getMillis() / 50L; // Folia - region threading + private long lastBookTick = Util.getMillis() / 50L; // Folia - region threading private int dropCount = 0; // Get position of last block hit for BlockDamageLevel.STOPPED @@ -342,8 +342,40 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic } // CraftBukkit end + // Folia start - region threading + public net.minecraft.world.level.ChunkPos disconnectPos; + private static final java.util.concurrent.atomic.AtomicLong DISCONNECT_TICKET_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); + public static final net.minecraft.server.level.TicketType DISCONNECT_TICKET = net.minecraft.server.level.TicketType.create("disconnect_ticket", Long::compareTo); + public final Long disconnectTicketId = Long.valueOf(DISCONNECT_TICKET_ID_GENERATOR.getAndIncrement()); + + private void checkKeepAlive() { + long currentTime = Util.getMillis(); + long elapsedTime = currentTime - this.keepAliveTime; + + if (this.keepAlivePending) { + if (!this.processedDisconnect && elapsedTime >= KEEPALIVE_LIMIT) { // check keepalive limit, don't fire if already disconnected + ServerGamePacketListenerImpl.LOGGER.warn("{} was kicked due to keepalive timeout!", this.player.getScoreboardName()); // more info + this.disconnect(Component.translatable("disconnect.timeout", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause + } + } else { + if (elapsedTime >= 15000L) { // 15 seconds + this.keepAlivePending = true; + this.keepAliveTime = currentTime; + this.keepAliveChallenge = currentTime; + this.send(new ClientboundKeepAlivePacket(this.keepAliveChallenge)); + } + } + } + // Folia end - region threading + @Override public void tick() { + // Folia start - region threading + this.checkKeepAlive(); + if (this.player.wonGame) { + return; + } + // Folia end - region threading if (this.ackBlockChangesUpTo > -1) { this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo)); this.ackBlockChangesUpTo = -1; @@ -395,22 +427,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic this.server.getProfiler().push("keepAlive"); // Paper Start - give clients a longer time to respond to pings as per pre 1.12.2 timings // This should effectively place the keepalive handling back to "as it was" before 1.12.2 - long currentTime = Util.getMillis(); - long elapsedTime = currentTime - this.keepAliveTime; - - if (this.keepAlivePending) { - if (!this.processedDisconnect && elapsedTime >= KEEPALIVE_LIMIT) { // check keepalive limit, don't fire if already disconnected - ServerGamePacketListenerImpl.LOGGER.warn("{} was kicked due to keepalive timeout!", this.player.getScoreboardName()); // more info - this.disconnect(Component.translatable("disconnect.timeout", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause - } - } else { - if (elapsedTime >= 15000L) { // 15 seconds - this.keepAlivePending = true; - this.keepAliveTime = currentTime; - this.keepAliveChallenge = currentTime; - this.send(new ClientboundKeepAlivePacket(this.keepAliveChallenge)); - } - } + // Folia - region threading - move to own method above // Paper end this.server.getProfiler().pop(); @@ -443,6 +460,19 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic this.lastGoodX = this.player.getX(); this.lastGoodY = this.player.getY(); this.lastGoodZ = this.player.getZ(); + // Folia start - support vehicle teleportations + this.lastVehicle = this.player.getRootVehicle(); + if (this.lastVehicle != this.player && this.lastVehicle.getControllingPassenger() == this.player) { + this.vehicleFirstGoodX = this.lastVehicle.getX(); + this.vehicleFirstGoodY = this.lastVehicle.getY(); + this.vehicleFirstGoodZ = this.lastVehicle.getZ(); + this.vehicleLastGoodX = this.lastVehicle.getX(); + this.vehicleLastGoodY = this.lastVehicle.getY(); + this.vehicleLastGoodZ = this.lastVehicle.getZ(); + } else { + this.lastVehicle = null; + } + // Folia end - support vehicle teleportations } @Override @@ -479,24 +509,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic if (this.processedDisconnect) { return; } - if (!this.cserver.isPrimaryThread()) { - Waitable waitable = new Waitable() { - @Override - protected Object evaluate() { - ServerGamePacketListenerImpl.this.disconnect(reason, cause); // Paper - adventure, kick event cause - return null; - } - }; - - this.server.processQueue.add(waitable); - - try { - waitable.get(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (ExecutionException e) { - throw new RuntimeException(e); - } + if (!io.papermc.paper.util.TickThread.isTickThreadFor(this.player)) { // Folia - region threading + this.connection.disconnectSafely(PaperAdventure.asVanilla(reason), cause); // Folia - region threading - it HAS to be delayed/async to avoid deadlock if we try to wait for another region return; } @@ -527,7 +541,6 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic Objects.requireNonNull(this.connection); // CraftBukkit - Don't wait - minecraftserver.scheduleOnMain(networkmanager::handleDisconnection); // Paper } private CompletableFuture filterTextPacket(T text, BiFunction> filterer) { @@ -610,9 +623,10 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic // Paper end - fix large move vectors killing the server // CraftBukkit start - handle custom speeds and skipped ticks - this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick; + int currTick = (int)(Util.getMillis() / 50); // Folia - region threading + this.allowedPlayerTicks += currTick - this.lastTick; // Folia - region threading this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1); - this.lastTick = (int) (System.currentTimeMillis() / 50); + this.lastTick = (int) currTick; // Folia - region threading ++this.receivedMovePacketCount; int i = this.receivedMovePacketCount - this.knownMovePacketCount; @@ -750,7 +764,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic // there to avoid any 'Moved wrongly' or 'Moved too quickly' errors. // We only do this if the Event was not cancelled. if (!oldTo.equals(event.getTo()) && !event.isCancelled()) { - this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); + this.player.getBukkitEntity().teleportAsync(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading return; } @@ -866,13 +880,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic // PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); // Paper - run this async // CraftBukkit start if (this.chatSpamTickCount.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamLimit && !this.server.getPlayerList().isOp(this.player.getGameProfile())) { // Paper start - split and make configurable - server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause + this.disconnect(Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause // Folia - region threading return; } // Paper start String str = packet.getCommand(); int index = -1; if (str.length() > 64 && ((index = str.indexOf(' ')) == -1 || index >= 64)) { - server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause + this.disconnect(Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause // Folia - region threading return; } // Paper end @@ -897,7 +911,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic if (!event.isHandled()) { if (!event.isCancelled()) { - this.server.scheduleOnMain(() -> { // This needs to be on main + this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading ParseResults parseresults = this.server.getCommands().getDispatcher().parse(stringreader, this.player.createCommandSourceStack()); this.server.getCommands().getDispatcher().getCompletionSuggestions(parseresults).thenAccept((suggestions) -> { @@ -908,7 +922,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic this.connection.send(new ClientboundCommandSuggestionsPacket(packet.getId(), suggestEvent.getSuggestions())); // Paper end - Brigadier API }); - }); + }, null, 1L); // Folia - region threading } } else if (!completions.isEmpty()) { final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(command, stringreader.getTotalLength()); @@ -1217,7 +1231,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic int byteLength = testString.getBytes(java.nio.charset.StandardCharsets.UTF_8).length; if (byteLength > 256 * 4) { ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send a book with with a page too large!"); - server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause + this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Folia - region threading return; } byteTotal += byteLength; @@ -1240,17 +1254,17 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic if (byteTotal > byteAllowed) { ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send too large of a book. Book Size: " + byteTotal + " - Allowed: "+ byteAllowed + " - Pages: " + pageList.size()); - server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause + this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Folia - region threading return; } } // Paper end // CraftBukkit start - if (this.lastBookTick + 20 > MinecraftServer.currentTick) { - server.scheduleOnMain(() -> this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause // Paper - Also ensure this is called on main + if (this.lastBookTick + 20 > this.lastTick) { + this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Paper - Also ensure this is called on main // Folia - region threading return; } - this.lastBookTick = MinecraftServer.currentTick; + this.lastBookTick = this.lastTick; // CraftBukkit end int i = packet.getSlot(); @@ -1270,7 +1284,19 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic this.updateBookContents(list1, i); }; - this.filterTextPacket((List) list).thenAcceptAsync(consumer, this.server); + this.filterTextPacket(list).thenAcceptAsync(consumer, // Folia start - region threading + (Runnable run) -> { + this.player.getBukkitEntity().taskScheduler.schedule( + (player) -> { + run.run(); + }, + null, 1L); + }).whenComplete((Object res, Throwable thr) -> { + if (thr != null) { + LOGGER.error("Failed to handle book update packet", thr); + } + }); + // Folia end - region threading } } @@ -1437,9 +1463,10 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic int i = this.receivedMovePacketCount - this.knownMovePacketCount; // CraftBukkit start - handle custom speeds and skipped ticks - this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick; + int currTick = (int)(Util.getMillis() / 50); // Folia - region threading + this.allowedPlayerTicks += currTick - this.lastTick; // Folia - region threading this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1); - this.lastTick = (int) (System.currentTimeMillis() / 50); + this.lastTick = (int) currTick; // Folia - region threading if (i > Math.max(this.allowedPlayerTicks, 5)) { ServerGamePacketListenerImpl.LOGGER.debug("{} is sending move packets too frequently ({} packets since last tick)", this.player.getName().getString(), i); @@ -1604,7 +1631,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic // there to avoid any 'Moved wrongly' or 'Moved too quickly' errors. // We only do this if the Event was not cancelled. if (!oldTo.equals(event.getTo()) && !event.isCancelled()) { - this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); + this.player.getBukkitEntity().teleportAsync(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading return; } @@ -1819,9 +1846,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic if (!this.player.isSpectator()) { // limit how quickly items can be dropped // If the ticks aren't the same then the count starts from 0 and we update the lastDropTick. - if (this.lastDropTick != MinecraftServer.currentTick) { + if (this.lastDropTick != io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) { this.dropCount = 0; - this.lastDropTick = MinecraftServer.currentTick; + this.lastDropTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); } else { // Else we increment the drop count and check the amount. this.dropCount++; @@ -1849,7 +1876,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic case ABORT_DESTROY_BLOCK: case STOP_DESTROY_BLOCK: // Paper start - Don't allow digging in unloaded chunks - if (this.player.level.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4) == null) { + if (!io.papermc.paper.util.TickThread.isTickThreadFor(this.player.getLevel(), blockposition.getX() >> 4, blockposition.getZ() >> 4, 8) || this.player.level.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4) == null) { // Folia - region threading - don't destroy blocks not owned this.player.connection.ackBlockChangesUpTo(packet.getSequence()); return; } @@ -1933,7 +1960,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic BlockPos blockposition = movingobjectpositionblock.getBlockPos(); Vec3 vec3d1 = Vec3.atCenterOf(blockposition); - if (this.player.getEyePosition().distanceToSqr(vec3d1) <= ServerGamePacketListenerImpl.MAX_INTERACTION_DISTANCE) { + if (io.papermc.paper.util.TickThread.isTickThreadFor(this.player.getLevel(), blockposition.getX() >> 4, blockposition.getZ() >> 4, 8) && this.player.getEyePosition().distanceToSqr(vec3d1) <= ServerGamePacketListenerImpl.MAX_INTERACTION_DISTANCE) { // Folia - do not allow players to interact with blocks outside the current region Vec3 vec3d2 = vec3d.subtract(vec3d1); double d0 = 1.0000001D; @@ -2046,7 +2073,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic Entity entity = packet.getEntity(worldserver); if (entity != null) { - this.player.teleportTo(worldserver, entity.getX(), entity.getY(), entity.getZ(), entity.getYRot(), entity.getXRot(), org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit + io.papermc.paper.threadedregions.TeleportUtils.teleport(this.player, false, entity, null, null, Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE, null); // Folia - region threading return; } } @@ -2109,6 +2136,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic this.player.disconnect(); // Paper start - Adventure quitMessage = quitMessage == null ? this.server.getPlayerList().remove(this.player) : this.server.getPlayerList().remove(this.player, quitMessage); // Paper - pass in quitMessage to fix kick message not being used + this.disconnectPos = this.player.chunkPosition(); // Folia - region threading - note: only set after removing, since it can tick the player + this.player.getLevel().chunkSource.addTicketAtLevel(DISCONNECT_TICKET, this.disconnectPos, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, this.disconnectTicketId); // Folia - region threading - force chunk to be loaded so that the region is not lost if ((quitMessage != null) && !quitMessage.equals(net.kyori.adventure.text.Component.empty())) { this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(quitMessage), false); // Paper end @@ -2194,9 +2223,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic } // CraftBukkit end if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.message())) { - this.server.scheduleOnMain(() -> { // Paper - push to main for event firing + // Folia - region threading this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - add cause - }); // Paper - push to main for event firing + // Folia - region threading } else { Optional optional = this.tryHandleChat(packet.message(), packet.timeStamp(), packet.lastSeenMessages()); @@ -2230,17 +2259,17 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic @Override public void handleChatCommand(ServerboundChatCommandPacket packet) { if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.command())) { - this.server.scheduleOnMain(() -> { // Paper - push to main for event firing + // Folia - region threading this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - }); // Paper - push to main for event firing + // Folia - region threading } else { Optional optional = this.tryHandleChat(packet.command(), packet.timeStamp(), packet.lastSeenMessages()); if (optional.isPresent()) { - this.server.submit(() -> { + this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading this.performChatCommand(packet, (LastSeenMessages) optional.get()); this.detectRateSpam("/" + packet.command()); // Spigot - }); + }, null, 1L); // Folia - region threading } } @@ -2314,9 +2343,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic private Optional tryHandleChat(String message, Instant timestamp, LastSeenMessages.Update acknowledgment) { if (!this.updateChatOrder(timestamp)) { ServerGamePacketListenerImpl.LOGGER.warn("{} sent out-of-order chat: '{}': {} > {}", this.player.getName().getString(), message, this.lastChatTimeStamp.get().getEpochSecond(), timestamp.getEpochSecond()); // Paper - this.server.scheduleOnMain(() -> { // Paper - push to main + // Folia - region threading this.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat"), org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event ca - }); // Paper - push to main + // Folia - region threading return Optional.empty(); } else { Optional optional = this.unpackAndApplyLastSeen(acknowledgment); @@ -2391,7 +2420,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic String originalFormat = event.getFormat(), originalMessage = event.getMessage(); this.cserver.getPluginManager().callEvent(event); - if (PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) { + if (false && PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) { // Folia - region threading // Evil plugins still listening to deprecated event final PlayerChatEvent queueEvent = new PlayerChatEvent(player, event.getMessage(), event.getFormat(), event.getRecipients()); queueEvent.setCancelled(event.isCancelled()); @@ -2469,6 +2498,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic public void handleCommand(String s) { // Paper - private -> public // Paper Start if (!org.spigotmc.AsyncCatcher.shuttingDown && !org.bukkit.Bukkit.isPrimaryThread()) { + if (true) throw new UnsupportedOperationException(); // Folia - region threading LOGGER.error("Command Dispatched Async: " + s); LOGGER.error("Please notify author of plugin causing this execution to fix this bug! see: http://bit.ly/1oSiM6C", new Throwable()); Waitable wait = new Waitable<>() { @@ -2529,6 +2559,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic if (s.isEmpty()) { ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send an empty message"); } else if (this.getCraftPlayer().isConversing()) { + if (true) throw new UnsupportedOperationException(); // Folia - region threading final String conversationInput = s; this.server.processQueue.add(new Runnable() { @Override @@ -2770,7 +2801,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic this.player.resetLastActionTime(); this.player.setShiftKeyDown(packet.isUsingSecondaryAction()); - if (entity != null) { + if (io.papermc.paper.util.TickThread.isTickThreadFor(entity) && entity != null) { // Folia - region threading - do not allow interaction of entities outside the current region if (!worldserver.getWorldBorder().isWithinBounds(entity.blockPosition())) { return; } @@ -2910,6 +2941,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic switch (packetplayinclientcommand_enumclientcommand) { case PERFORM_RESPAWN: if (this.player.wonGame) { + // Folia start - region threading + if (true) { + this.player.exitEndCredits(); + return; + } + // Folia end - region threading this.player.wonGame = false; this.player = this.server.getPlayerList().respawn(this.player, this.server.getLevel(this.player.getRespawnDimension()), true, null, true, org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag.END_PORTAL); // Paper - add isEndCreditsRespawn argument CriteriaTriggers.CHANGED_DIMENSION.trigger(this.player, Level.END, Level.OVERWORLD); @@ -2918,6 +2955,18 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic return; } + // Folia start - region threading + if (true) { + this.player.respawn((ServerPlayer player) -> { + if (ServerGamePacketListenerImpl.this.server.isHardcore()) { + player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper + ((GameRules.BooleanValue) player.getLevel().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS)).set(false, player.getLevel()); // Paper + } + }); + return; + } + // Folia end - region threading + this.player = this.server.getPlayerList().respawn(this.player, false); if (this.server.isHardcore()) { this.player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper @@ -3270,7 +3319,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic // Paper start if (!org.bukkit.Bukkit.isPrimaryThread()) { if (recipeSpamPackets.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamLimit) { - server.scheduleOnMain(() -> this.disconnect(net.minecraft.network.chat.Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause + this.disconnect(net.minecraft.network.chat.Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause // Folia - region threading return; } } @@ -3412,7 +3461,18 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic this.filterTextPacket(list).thenAcceptAsync((list1) -> { this.updateSignText(packet, list1); - }, this.server); + }, (Runnable run) -> { // Folia start - region threading + this.player.getBukkitEntity().taskScheduler.schedule( + (player) -> { + run.run(); + }, + null, 1L); + }).whenComplete((Object res, Throwable thr) -> { + if (thr != null) { + LOGGER.error("Failed to handle sign update packet", thr); + } + }); + // Folia end - region threading } private void updateSignText(ServerboundSignUpdatePacket packet, List signText) { @@ -3482,9 +3542,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic this.keepAlivePending = false; } else if (!this.isSingleplayerOwner()) { // Paper start - This needs to be handled on the main thread for plugins - server.submit(() -> { + // Folia - region threading - do not schedule to main anymore, there is no main this.disconnect(Component.translatable("disconnect.timeout"), org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause - }); + // Folia - region threading - do not schedule to main anymore, there is no main // Paper end } diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java index 2ff578e4a953ffcf5176815ba8e3f06f73499989..2e96377d628b3a07fb565020074d665f594f32e8 100644 --- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java @@ -53,7 +53,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, private final byte[] challenge; final MinecraftServer server; public final Connection connection; - public ServerLoginPacketListenerImpl.State state; + public volatile ServerLoginPacketListenerImpl.State state; // Folia - region threading private int tick; public @Nullable GameProfile gameProfile; @@ -80,20 +80,14 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, } // Paper end if (this.state == ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT) { - // Paper start - prevent logins to be processed even though disconnect was called - if (connection.isConnected()) { + // Folia start - region threading - rewrite login process + String name = this.gameProfile.getName(); + UUID uniqueId = UUIDUtil.getOrCreatePlayerUUID(this.gameProfile); + if (this.server.getPlayerList().pushPendingJoin(name, uniqueId, this.connection)) { this.handleAcceptedLogin(); } - // Paper end - } else if (this.state == ServerLoginPacketListenerImpl.State.DELAY_ACCEPT) { - ServerPlayer entityplayer = this.server.getPlayerList().getPlayer(this.gameProfile.getId()); - - if (entityplayer == null) { - this.state = ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT; - this.placeNewPlayer(this.delayedAcceptPlayer); - this.delayedAcceptPlayer = null; - } - } + // Folia end - region threading - rewrite login process + } // Folia - region threading - remove delayed accept if (this.tick++ == 600) { this.disconnect(Component.translatable("multiplayer.disconnect.slow_login")); @@ -163,7 +157,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, // this.disconnect(ichatbasecomponent); // CraftBukkit end } else { - this.state = ServerLoginPacketListenerImpl.State.ACCEPTED; + this.state = ServerLoginPacketListenerImpl.State.HANDING_OFF; // Folia - region threading - rewrite login process if (this.server.getCompressionThreshold() >= 0 && !this.connection.isMemoryConnection()) { this.connection.send(new ClientboundLoginCompressionPacket(this.server.getCompressionThreshold()), PacketSendListener.thenRun(() -> { this.connection.setupCompression(this.server.getCompressionThreshold(), true); @@ -171,17 +165,55 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, } this.connection.send(new ClientboundGameProfilePacket(this.gameProfile)); - ServerPlayer entityplayer = this.server.getPlayerList().getPlayer(this.gameProfile.getId()); + // Folia - region threading - rewrite login process try { - ServerPlayer entityplayer1 = this.server.getPlayerList().getPlayerForLogin(this.gameProfile, s); // CraftBukkit - add player reference - - if (entityplayer != null) { - this.state = ServerLoginPacketListenerImpl.State.DELAY_ACCEPT; - this.delayedAcceptPlayer = entityplayer1; - } else { - this.placeNewPlayer(entityplayer1); - } + // Folia start - region threading - rewrite login process + org.apache.commons.lang3.mutable.MutableObject data = new org.apache.commons.lang3.mutable.MutableObject<>(); + org.apache.commons.lang3.mutable.MutableObject lastKnownName = new org.apache.commons.lang3.mutable.MutableObject<>(); + ca.spottedleaf.concurrentutil.completable.Completable toComplete = new ca.spottedleaf.concurrentutil.completable.Completable<>(); + // note: need to call addWaiter before completion to ensure the callback is invoked synchronously + // the loadSpawnForNewPlayer function always completes the completable once the chunks were loaded, + // on the load callback for those chunks (so on the same region) + // this guarantees the chunk cannot unload under our feet + toComplete.addWaiter((org.bukkit.Location loc, Throwable t) -> { + int chunkX = net.minecraft.util.Mth.floor(loc.getX()) >> 4; + int chunkZ = net.minecraft.util.Mth.floor(loc.getZ()) >> 4; + + net.minecraft.server.level.ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)loc.getWorld()).getHandle(); + // we just need to hold the chunks at loaded until the next tick + // so we do not need to care about unique IDs for the ticket + world.getChunkSource().addTicketAtLevel( + net.minecraft.server.level.TicketType.LOGIN, + new net.minecraft.world.level.ChunkPos(chunkX, chunkZ), + io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, + net.minecraft.util.Unit.INSTANCE + ); + + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + world, chunkX, chunkZ, + () -> { + // now at this point the connection is held by the region, so we have to check isConnected() + // this would have been handled in connection ticking, but we are in a state between + // being owned by the global tick thread and the region so we have to do it + if (t != null || !ServerLoginPacketListenerImpl.this.connection.isConnected()) { + ServerLoginPacketListenerImpl.this.connection.handleDisconnection(); + return; + } + ServerLoginPacketListenerImpl.this.state = State.ACCEPTED; + ServerLoginPacketListenerImpl.this.server.getPlayerList().placeNewPlayer( + ServerLoginPacketListenerImpl.this.connection, + s, + data.getValue(), + lastKnownName.getValue(), + loc + ); + }, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER + ); + }); + this.server.getPlayerList().loadSpawnForNewPlayer(this.connection, s, data, lastKnownName, toComplete); + // Folia end - region threading - rewrite login process } catch (Exception exception) { ServerLoginPacketListenerImpl.LOGGER.error("Couldn't place player in world", exception); MutableComponent ichatmutablecomponent = Component.translatable("multiplayer.disconnect.invalid_player_data"); @@ -198,9 +230,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, } - private void placeNewPlayer(ServerPlayer player) { - this.server.getPlayerList().placeNewPlayer(this.connection, player); - } + // Folia end - region threading - rewrite login process @Override public void onDisconnect(Component reason) { @@ -397,7 +427,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, uniqueId = gameProfile.getId(); // Paper end - if (PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { + if (false && PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { // Folia - region threading final PlayerPreLoginEvent event = new PlayerPreLoginEvent(playerName, address, uniqueId); if (asyncEvent.getResult() != PlayerPreLoginEvent.Result.ALLOWED) { event.disallow(asyncEvent.getResult(), asyncEvent.kickMessage()); // Paper - Adventure @@ -480,7 +510,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, public static enum State { - HELLO, KEY, AUTHENTICATING, NEGOTIATING, READY_TO_ACCEPT, DELAY_ACCEPT, ACCEPTED; + HELLO, KEY, AUTHENTICATING, NEGOTIATING, READY_TO_ACCEPT, DELAY_ACCEPT, HANDING_OFF, ACCEPTED; // Folia - region threading private State() {} } diff --git a/src/main/java/net/minecraft/server/players/BanListEntry.java b/src/main/java/net/minecraft/server/players/BanListEntry.java index 8f19876c20ce9cce574ebbec386a26ab5c807e18..4627c7bd326ca42c9476ec17867dba2ad4191b86 100644 --- a/src/main/java/net/minecraft/server/players/BanListEntry.java +++ b/src/main/java/net/minecraft/server/players/BanListEntry.java @@ -10,7 +10,7 @@ import net.minecraft.network.chat.Component; public abstract class BanListEntry extends StoredUserEntry { - public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT); + public static final ThreadLocal DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT)); // Folia - region threading - SDF is not thread-safe public static final String EXPIRES_NEVER = "forever"; protected final Date created; protected final String source; @@ -32,7 +32,7 @@ public abstract class BanListEntry extends StoredUserEntry { Date date; try { - date = json.has("created") ? BanListEntry.DATE_FORMAT.parse(json.get("created").getAsString()) : new Date(); + date = json.has("created") ? BanListEntry.DATE_FORMAT.get().parse(json.get("created").getAsString()) : new Date(); // Folia - region threading - SDF is not thread-safe } catch (ParseException parseexception) { date = new Date(); } @@ -43,7 +43,7 @@ public abstract class BanListEntry extends StoredUserEntry { Date date1; try { - date1 = json.has("expires") ? BanListEntry.DATE_FORMAT.parse(json.get("expires").getAsString()) : null; + date1 = json.has("expires") ? BanListEntry.DATE_FORMAT.get().parse(json.get("expires").getAsString()) : null; // Folia - region threading - SDF is not thread-safe } catch (ParseException parseexception1) { date1 = null; } @@ -78,9 +78,9 @@ public abstract class BanListEntry extends StoredUserEntry { @Override protected void serialize(JsonObject json) { - json.addProperty("created", BanListEntry.DATE_FORMAT.format(this.created)); + json.addProperty("created", BanListEntry.DATE_FORMAT.get().format(this.created)); // Folia - region threading - SDF is not thread-safe json.addProperty("source", this.source); - json.addProperty("expires", this.expires == null ? "forever" : BanListEntry.DATE_FORMAT.format(this.expires)); + json.addProperty("expires", this.expires == null ? "forever" : BanListEntry.DATE_FORMAT.get().format(this.expires)); // Folia - region threading - SDF is not thread-safe json.addProperty("reason", this.reason); } @@ -89,7 +89,7 @@ public abstract class BanListEntry extends StoredUserEntry { Date expires = null; try { - expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.parse(jsonobject.get("expires").getAsString()) : null; + expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.get().parse(jsonobject.get("expires").getAsString()) : null; // Folia - region threading - SDF is not thread-safe } catch (ParseException ex) { // Guess we don't have a date } diff --git a/src/main/java/net/minecraft/server/players/OldUsersConverter.java b/src/main/java/net/minecraft/server/players/OldUsersConverter.java index 7edd4b88eb0476f0630630bc4681e859bd145b2b..f3586a5c5b5d4cae817aa7c15fc0c2fc4cd6dc09 100644 --- a/src/main/java/net/minecraft/server/players/OldUsersConverter.java +++ b/src/main/java/net/minecraft/server/players/OldUsersConverter.java @@ -517,7 +517,7 @@ public class OldUsersConverter { Date date1; try { - date1 = BanListEntry.DATE_FORMAT.parse(dateString); + date1 = BanListEntry.DATE_FORMAT.get().parse(dateString); // Folia - region threading - SDF is not thread-safe } catch (ParseException parseexception) { date1 = fallback; } diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java index 0dbe182fbae5ce5ba182176eb5d5e3f1897e77f2..b9bd88fa6536e17cccec8a4f482b9b272be9568f 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java @@ -136,10 +136,10 @@ public abstract class PlayerList { public static final Component CHAT_FILTERED_FULL = Component.translatable("chat.filtered_full"); private static final Logger LOGGER = LogUtils.getLogger(); private static final int SEND_PLAYER_INFO_INTERVAL = 600; - private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); + private static final ThreadLocal BAN_DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z")); // Folia - region threading - SDF is not thread-safe private final MinecraftServer server; public final List players = new java.util.concurrent.CopyOnWriteArrayList(); // CraftBukkit - ArrayList -> CopyOnWriteArrayList: Iterator safety - private final Map playersByUUID = Maps.newHashMap(); + private final Map playersByUUID = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading - change to CHM - Note: we do NOT expect concurrency PER KEY! private final UserBanList bans; private final IpBanList ipBans; private final ServerOpList ops; @@ -161,9 +161,56 @@ public abstract class PlayerList { // CraftBukkit start private CraftServer cserver; - private final Map playersByName = new java.util.HashMap<>(); + private final Map playersByName = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading - change to CHM - Note: we do NOT expect concurrency PER KEY! public @Nullable String collideRuleTeamName; // Paper - Team name used for collideRule + // Folia start - region threading + private final Object stateLock = new Object(); + private final Map connectionByName = new java.util.HashMap<>(); + private final Map connectionById = new java.util.HashMap<>(); + + public boolean pushPendingJoin(String userName, UUID byId, Connection conn) { + userName = userName.toLowerCase(java.util.Locale.ROOT); + Connection conflictingName, conflictingId; + synchronized (this.stateLock) { + conflictingName = this.connectionByName.get(userName); + conflictingId = this.connectionById.get(byId); + + if (conflictingName == null && conflictingId == null) { + this.connectionByName.put(userName, conn); + this.connectionById.put(byId, conn); + } + } + + Component message = Component.translatable("multiplayer.disconnect.duplicate_login", new Object[0]); + + if (conflictingId != null || conflictingName != null) { + if (conflictingName != null && conflictingName.isPlayerConnected()) { + conflictingName.disconnectSafely(message, org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); + } + if (conflictingName != conflictingId && conflictingId != null && conflictingId.isPlayerConnected()) { + conflictingId.disconnectSafely(message, org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); + } + } + + return conflictingName == null && conflictingId == null; + } + + public void removeConnection(String userName, UUID byId, Connection conn) { + userName = userName.toLowerCase(java.util.Locale.ROOT); + synchronized (this.stateLock) { + this.connectionByName.remove(userName, conn); + this.connectionById.remove(byId, conn); + } + } + + private int getTotalConnections() { + synchronized (this.stateLock) { + return this.connectionById.size(); + } + } + // Folia end - region threading + public PlayerList(MinecraftServer server, LayeredRegistryAccess registryManager, PlayerDataStorage saveHandler, int maxPlayers) { this.cserver = server.server = new CraftServer((DedicatedServer) server, this); server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper @@ -185,7 +232,7 @@ public abstract class PlayerList { } abstract public void loadAndSaveFiles(); // Paper - moved from DedicatedPlayerList constructor - public void placeNewPlayer(Connection connection, ServerPlayer player) { + public void loadSpawnForNewPlayer(Connection connection, ServerPlayer player, org.apache.commons.lang3.mutable.MutableObject data, org.apache.commons.lang3.mutable.MutableObject lastKnownName, ca.spottedleaf.concurrentutil.completable.Completable toComplete) { player.isRealPlayer = true; // Paper player.loginTime = System.currentTimeMillis(); // Paper GameProfile gameprofile = player.getGameProfile(); @@ -235,8 +282,28 @@ public abstract class PlayerList { worldserver1 = worldserver; } - if (nbttagcompound == null) player.fudgeSpawnLocation(worldserver1); // Paper - only move to spawn on first login, otherwise, stay where you are.... - + // Folia start - region threading - rewrite login process + if (nbttagcompound == null) ServerPlayer.fudgeSpawnLocation(worldserver1, player, toComplete); // Folia - only move to spawn on first login, otherwise, stay where you are.... + if (nbttagcompound != null) { + worldserver1.loadChunksForMoveAsync( + player.getBoundingBox(), + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, + (c) -> { + toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(worldserver1, player.position())); + } + ); + } + data.setValue(nbttagcompound); + lastKnownName.setValue(s); + return; + } + // nbttagcomound -> player data + // s -> last known name + public void placeNewPlayer(Connection connection, ServerPlayer player, CompoundTag nbttagcompound, String s, Location selectedSpawn) { + ServerLevel worldserver1 = ((CraftWorld)selectedSpawn.getWorld()).getHandle(); + player.setPosRaw(selectedSpawn.getX(), selectedSpawn.getY(), selectedSpawn.getZ()); + player.lastSave = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); + // Folia end - region threading - rewrite login process player.setLevel(worldserver1); String s1 = "local"; @@ -247,7 +314,7 @@ public abstract class PlayerList { // Spigot start - spawn location event Player spawnPlayer = player.getBukkitEntity(); org.spigotmc.event.player.PlayerSpawnLocationEvent ev = new com.destroystokyo.paper.event.player.PlayerInitialSpawnEvent(spawnPlayer, spawnPlayer.getLocation()); // Paper use our duplicate event - this.cserver.getPluginManager().callEvent(ev); + //this.cserver.getPluginManager().callEvent(ev); // Folia - region threading - TODO WTF TO DO WITH THIS EVENT? Location loc = ev.getSpawnLocation(); worldserver1 = ((CraftWorld) loc.getWorld()).getHandle(); @@ -266,6 +333,7 @@ public abstract class PlayerList { player.loadGameTypes(nbttagcompound); ServerGamePacketListenerImpl playerconnection = new ServerGamePacketListenerImpl(this.server, connection, player); + worldserver1.getCurrentWorldData().connections.add(connection); // Folia - region threading - only AFTER updating listener to game GameRules gamerules = worldserver1.getGameRules(); boolean flag = gamerules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN); boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO); @@ -283,7 +351,7 @@ public abstract class PlayerList { this.sendPlayerPermissionLevel(player); player.getStats().markAllDirty(); player.getRecipeBook().sendInitialRecipeBook(player); - this.updateEntireScoreboard(worldserver1.getScoreboard(), player); + if (false) this.updateEntireScoreboard(worldserver1.getScoreboard(), player); // Folia - region threading this.server.invalidateStatus(); MutableComponent ichatmutablecomponent; @@ -325,7 +393,7 @@ public abstract class PlayerList { this.cserver.getPluginManager().callEvent(playerJoinEvent); if (!player.connection.isAcceptingMessages()) { - return; + //return; // Folia - region threading - must still allow the player to connect, as we must add to chunk map before handling disconnect } final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); @@ -340,8 +408,7 @@ public abstract class PlayerList { ClientboundPlayerInfoUpdatePacket packet = ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)); final List onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1); // Paper - use single player info update packet - for (int i = 0; i < this.players.size(); ++i) { - ServerPlayer entityplayer1 = (ServerPlayer) this.players.get(i); + for (ServerPlayer entityplayer1 : this.players) { // Folia - region threadingv if (entityplayer1.getBukkitEntity().canSee(bukkitPlayer)) { entityplayer1.connection.send(packet); @@ -458,7 +525,7 @@ public abstract class PlayerList { // Paper start - Add to collideRule team if needed final Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); final PlayerTeam collideRuleTeam = scoreboard.getPlayerTeam(this.collideRuleTeamName); - if (this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { + if (false && this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { // Folia - region threading scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam); } // Paper end @@ -549,7 +616,7 @@ public abstract class PlayerList { protected void save(ServerPlayer player) { if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit - player.lastSave = MinecraftServer.currentTick; // Paper + player.lastSave = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading this.playerIo.save(player); ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit @@ -589,7 +656,7 @@ public abstract class PlayerList { // CraftBukkit end // Paper start - Remove from collideRule team if needed - if (this.collideRuleTeamName != null) { + if (false && this.collideRuleTeamName != null) { // Folia - region threading final Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard(); final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName); if (entityplayer.getTeam() == team && team != null) { @@ -629,6 +696,7 @@ public abstract class PlayerList { entityplayer.unRide(); worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER); + entityplayer.retireScheduler(); // Folia - region threading entityplayer.getAdvancements().stopListening(); this.players.remove(entityplayer); this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot @@ -647,8 +715,7 @@ public abstract class PlayerList { // CraftBukkit start // this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(entityplayer.getUUID()))); ClientboundPlayerInfoRemovePacket packet = new ClientboundPlayerInfoRemovePacket(List.of(entityplayer.getUUID())); - for (int i = 0; i < this.players.size(); i++) { - ServerPlayer entityplayer2 = (ServerPlayer) this.players.get(i); + for (ServerPlayer entityplayer2 : this.players) { // Folia - region threading if (entityplayer2.getBukkitEntity().canSee(entityplayer.getBukkitEntity())) { entityplayer2.connection.send(packet); @@ -673,19 +740,13 @@ public abstract class PlayerList { ServerPlayer entityplayer; - for (int i = 0; i < this.players.size(); ++i) { - entityplayer = (ServerPlayer) this.players.get(i); - if (entityplayer.getUUID().equals(uuid) || (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && entityplayer.getGameProfile().getName().equalsIgnoreCase(gameprofile.getName()))) { // Paper - validate usernames - list.add(entityplayer); - } - } + // Folia - region threading - rewrite login process - moved to pushPendingJoin Iterator iterator = list.iterator(); while (iterator.hasNext()) { entityplayer = (ServerPlayer) iterator.next(); - this.save(entityplayer); // CraftBukkit - Force the player's inventory to be saved - entityplayer.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause + // Folia - moved to pushPendingJoin } // Instead of kicking then returning, we need to store the kick reason @@ -704,7 +765,7 @@ public abstract class PlayerList { ichatmutablecomponent = Component.translatable("multiplayer.disconnect.banned.reason", gameprofilebanentry.getReason()); if (gameprofilebanentry.getExpires() != null) { - ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned.expiration", PlayerList.BAN_DATE_FORMAT.format(gameprofilebanentry.getExpires()))); + ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned.expiration", PlayerList.BAN_DATE_FORMAT.get().format(gameprofilebanentry.getExpires()))); // Folia - region threading - SDF is not thread-safe } // return chatmessage; @@ -717,14 +778,14 @@ public abstract class PlayerList { ichatmutablecomponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipbanentry.getReason()); if (ipbanentry.getExpires() != null) { - ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned_ip.expiration", PlayerList.BAN_DATE_FORMAT.format(ipbanentry.getExpires()))); + ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned_ip.expiration", PlayerList.BAN_DATE_FORMAT.get().format(ipbanentry.getExpires()))); // Folia - region threading - SDF is not thread-safe } // return chatmessage; event.disallow(PlayerLoginEvent.Result.KICK_BANNED, PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure } else { // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile) ? IChatBaseComponent.translatable("multiplayer.disconnect.server_full") : null; - if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile)) { + if (this.getTotalConnections() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile)) { // Folia - region threading - we control connection state here now async, not player list size event.disallow(PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure } } @@ -782,6 +843,11 @@ public abstract class PlayerList { public ServerPlayer respawn(ServerPlayer entityplayer, ServerLevel worldserver, boolean flag, Location location, boolean avoidSuffocation, org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag...respawnFlags) { // Paper end + // Folia start - region threading + if (true) { + throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); + } + // Folia end - region threading entityplayer.stopRiding(); // CraftBukkit this.players.remove(entityplayer); this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot @@ -976,10 +1042,10 @@ public abstract class PlayerList { public void tick() { if (++this.sendAllPlayerInfoIn > 600) { // CraftBukkit start - for (int i = 0; i < this.players.size(); ++i) { - final ServerPlayer target = (ServerPlayer) this.players.get(i); + ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Folia - region threading + for (final ServerPlayer target : players) { // Folia - region threading - target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), this.players.stream().filter(new Predicate() { + target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), java.util.Arrays.stream(players).filter(new Predicate() { // Folia - region threading @Override public boolean test(ServerPlayer input) { return target.getBukkitEntity().canSee(input.getBukkitEntity()); @@ -1005,18 +1071,17 @@ public abstract class PlayerList { // CraftBukkit start - add a world/entity limited version public void broadcastAll(Packet packet, net.minecraft.world.entity.player.Player entityhuman) { - for (int i = 0; i < this.players.size(); ++i) { - ServerPlayer entityplayer = this.players.get(i); + for (ServerPlayer entityplayer : this.players) { // Folia - region threading if (entityhuman != null && !entityplayer.getBukkitEntity().canSee(entityhuman.getBukkitEntity())) { continue; } - ((ServerPlayer) this.players.get(i)).connection.send(packet); + entityplayer.connection.send(packet); // Folia - region threading } } public void broadcastAll(Packet packet, Level world) { - for (int i = 0; i < world.players().size(); ++i) { - ((ServerPlayer) world.players().get(i)).connection.send(packet); + for (net.minecraft.world.entity.player.Player player : world.players()) { // Folia - region threading + ((ServerPlayer) player).connection.send(packet); // Folia - region threading } } @@ -1060,8 +1125,7 @@ public abstract class PlayerList { if (scoreboardteambase == null) { this.broadcastSystemMessage(message, false); } else { - for (int i = 0; i < this.players.size(); ++i) { - ServerPlayer entityplayer = (ServerPlayer) this.players.get(i); + for (ServerPlayer entityplayer : this.players) { // Folia - region threading if (entityplayer.getTeam() != scoreboardteambase) { entityplayer.sendSystemMessage(message); @@ -1072,10 +1136,12 @@ public abstract class PlayerList { } public String[] getPlayerNamesArray() { - String[] astring = new String[this.players.size()]; + List players = new java.util.ArrayList<>(this.players); // Folia start - region threading + String[] astring = new String[players.size()]; - for (int i = 0; i < this.players.size(); ++i) { - astring[i] = ((ServerPlayer) this.players.get(i)).getGameProfile().getName(); + for (int i = 0; i < players.size(); ++i) { + astring[i] = ((ServerPlayer) players.get(i)).getGameProfile().getName(); + // Folia end - region threading } return astring; @@ -1094,7 +1160,9 @@ public abstract class PlayerList { ServerPlayer entityplayer = this.getPlayer(profile.getId()); if (entityplayer != null) { + entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading this.sendPlayerPermissionLevel(entityplayer); + }, null, 1L); // Folia - region threading } } @@ -1104,7 +1172,10 @@ public abstract class PlayerList { ServerPlayer entityplayer = this.getPlayer(profile.getId()); if (entityplayer != null) { + entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading this.sendPlayerPermissionLevel(entityplayer); + }, null, 1L); // Folia - region threading + } } @@ -1165,8 +1236,7 @@ public abstract class PlayerList { } public void broadcast(@Nullable net.minecraft.world.entity.player.Player player, double x, double y, double z, double distance, ResourceKey worldKey, Packet packet) { - for (int i = 0; i < this.players.size(); ++i) { - ServerPlayer entityplayer = (ServerPlayer) this.players.get(i); + for (ServerPlayer entityplayer : this.players) { // Folia - region threading // CraftBukkit start - Test if player receiving packet can see the source of the packet if (player != null && !entityplayer.getBukkitEntity().canSee(player.getBukkitEntity())) { @@ -1196,9 +1266,12 @@ public abstract class PlayerList { io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main MinecraftTimings.savePlayers.startTiming(); // Paper int numSaved = 0; - long now = MinecraftServer.currentTick; - for (int i = 0; i < this.players.size(); ++i) { - ServerPlayer entityplayer = this.players.get(i); + long now = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading + for (ServerPlayer entityplayer : this.players) { // Folia start - region threading + if (!io.papermc.paper.util.TickThread.isTickThreadFor(entityplayer)) { + continue; + } + // Folia end - region threading if (interval == -1 || now - entityplayer.lastSave >= interval) { this.save(entityplayer); if (interval != -1 && ++numSaved <= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { break; } @@ -1318,6 +1391,20 @@ public abstract class PlayerList { } public void removeAll(boolean isRestarting) { + // Folia start - region threading + // just send disconnect packet, don't modify state + for (ServerPlayer player : this.players) { + final Component shutdownMessage = PaperAdventure.asVanilla(this.server.server.shutdownMessage()); // Paper - Adventure + // CraftBukkit end + + player.connection.send(new net.minecraft.network.protocol.game.ClientboundDisconnectPacket(shutdownMessage), net.minecraft.network.PacketSendListener.thenRun(() -> { + player.connection.connection.disconnect(shutdownMessage); + })); + } + if (true) { + return; + } + // Folia end - region threading // Paper end // CraftBukkit start - disconnect safely for (ServerPlayer player : this.players) { @@ -1327,7 +1414,7 @@ public abstract class PlayerList { // CraftBukkit end // Paper start - Remove collideRule team if it exists - if (this.collideRuleTeamName != null) { + if (false && this.collideRuleTeamName != null) { // Folia - region threading final Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); final PlayerTeam team = scoreboard.getPlayersTeam(this.collideRuleTeamName); if (team != null) scoreboard.removePlayerTeam(team); diff --git a/src/main/java/net/minecraft/server/players/StoredUserList.java b/src/main/java/net/minecraft/server/players/StoredUserList.java index 4fd709a550bf8da1e996894a1ca6b91206c31e9e..e07eddbfbe3fa5e5915580a0f4d753ce54b33248 100644 --- a/src/main/java/net/minecraft/server/players/StoredUserList.java +++ b/src/main/java/net/minecraft/server/players/StoredUserList.java @@ -148,6 +148,7 @@ public abstract class StoredUserList> { } public void save() throws IOException { + synchronized (this) { // Folia - region threading this.removeExpired(); // Paper - remove expired values before saving JsonArray jsonarray = new JsonArray(); Stream stream = this.map.values().stream().map((jsonlistentry) -> { // CraftBukkit - decompile error @@ -178,10 +179,12 @@ public abstract class StoredUserList> { if (bufferedwriter != null) { bufferedwriter.close(); } + } // Folia - region threading } public void load() throws IOException { + synchronized (this) { // Folia - region threading if (this.file.exists()) { BufferedReader bufferedreader = Files.newReader(this.file, StandardCharsets.UTF_8); @@ -226,5 +229,6 @@ public abstract class StoredUserList> { } } + } // Folia - region threading } } diff --git a/src/main/java/net/minecraft/util/SortedArraySet.java b/src/main/java/net/minecraft/util/SortedArraySet.java index 4f5f2c25e12ee6d977bc98d9118650cfe91e6c0e..d227b91defc3992f1a003a19264bc3aa29718795 100644 --- a/src/main/java/net/minecraft/util/SortedArraySet.java +++ b/src/main/java/net/minecraft/util/SortedArraySet.java @@ -82,7 +82,7 @@ public class SortedArraySet extends AbstractSet { return Arrays.binarySearch(this.contents, 0, this.size, object, this.comparator); } - private static int getInsertionPosition(int binarySearchResult) { + public static int getInsertionPosition(int binarySearchResult) { // Folia - region threading - public return -binarySearchResult - 1; } @@ -169,6 +169,40 @@ public class SortedArraySet extends AbstractSet { } } // Paper end - rewrite chunk system + // Folia start - region threading + public int binarySearch(final T search) { + return this.findIndex(search); + } + + public int insertAndGetIdx(final T value) { + final int idx = this.findIndex(value); + if (idx >= 0) { + // exists already + return idx; + } + + this.addInternal(value, getInsertionPosition(idx)); + return idx; + } + + public T removeFirst() { + final T ret = this.contents[0]; + + this.removeInternal(0); + + return ret; + } + + public T removeLast() { + final int index = --this.size; + + final T ret = this.contents[index]; + + this.contents[index] = null; + + return ret; + } + // Folia end - region threading @Override public boolean remove(Object object) { diff --git a/src/main/java/net/minecraft/util/SpawnUtil.java b/src/main/java/net/minecraft/util/SpawnUtil.java index 83ef8cb27db685cceb5c2b7c9674e17b93ba081c..2d87c16420a97b9142d4ea76ceb6013deed22a1f 100644 --- a/src/main/java/net/minecraft/util/SpawnUtil.java +++ b/src/main/java/net/minecraft/util/SpawnUtil.java @@ -59,7 +59,7 @@ public class SpawnUtil { return Optional.of(t0); } - t0.discard(); + //t0.discard(); // Folia - region threading } } } diff --git a/src/main/java/net/minecraft/world/damagesource/CombatTracker.java b/src/main/java/net/minecraft/world/damagesource/CombatTracker.java index a454aa44acddd668c96a17da44ccccb1bbac4546..0e6112a7dbeb17b088e3585b0d284ec36e48ca80 100644 --- a/src/main/java/net/minecraft/world/damagesource/CombatTracker.java +++ b/src/main/java/net/minecraft/world/damagesource/CombatTracker.java @@ -40,7 +40,7 @@ public class CombatTracker { public void prepareForDamage() { this.resetPreparedStatus(); Optional optional = this.mob.getLastClimbablePos(); - if (optional.isPresent()) { + if (optional.isPresent() && io.papermc.paper.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.mob.level, optional.get())) { // Folia - region threading - may sync load the block, which would crash BlockState blockState = this.mob.level.getBlockState(optional.get()); if (!blockState.is(Blocks.LADDER) && !blockState.is(BlockTags.TRAPDOORS)) { if (blockState.is(Blocks.VINE)) { @@ -92,6 +92,7 @@ public class CombatTracker { Component component = combatEntry2.getAttackerName(); DamageSource damageSource = combatEntry2.getSource(); Entity entity = damageSource.getEntity(); + if (!io.papermc.paper.util.TickThread.isTickThreadFor(entity)) entity = null; // Folia - region threading - not safe to access other entity data DeathMessageType deathMessageType = damageSource.type().deathMessageType(); Component component4; if (combatEntry != null && deathMessageType == DeathMessageType.FALL_VARIANTS) { @@ -158,6 +159,7 @@ public class CombatTracker { for(CombatEntry combatEntry : this.entries) { Entity var8 = combatEntry.getSource().getEntity(); + if (!io.papermc.paper.util.TickThread.isTickThreadFor(var8)) { continue; } // Folia - region threading - skip entities we do not own if (var8 instanceof Player player2) { if (player == null || combatEntry.getDamage() > g) { g = combatEntry.getDamage(); diff --git a/src/main/java/net/minecraft/world/damagesource/DamageSource.java b/src/main/java/net/minecraft/world/damagesource/DamageSource.java index 93a1e990b0a6caae4143c2f9d09bfb368fa1d6db..ad3166481dd37f4b5380f8bf28653bb4fd5c4933 100644 --- a/src/main/java/net/minecraft/world/damagesource/DamageSource.java +++ b/src/main/java/net/minecraft/world/damagesource/DamageSource.java @@ -112,7 +112,7 @@ public class DamageSource { Entity entity = this.causingEntity; ItemStack itemstack; - if (entity instanceof LivingEntity) { + if (entity instanceof LivingEntity livingEntity && io.papermc.paper.util.TickThread.isTickThreadFor(livingEntity)) { // Folia - region threading LivingEntity entityliving2 = (LivingEntity) entity; itemstack = entityliving2.getMainHandItem(); diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java index 4705d7066207250c03a5f98eef61554c901f2e35..c659a574195f465ec3316f4c014621569e099cfc 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -165,7 +165,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { // Paper start public static RandomSource SHARED_RANDOM = new RandomRandomSource(); - private static final class RandomRandomSource extends java.util.Random implements net.minecraft.world.level.levelgen.BitRandomSource { + public static final class RandomRandomSource extends java.util.Random implements net.minecraft.world.level.levelgen.BitRandomSource { // Folia - region threading private boolean locked = false; @Override @@ -239,17 +239,29 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData; // Paper public boolean collisionLoadChunks = false; // Paper - private CraftEntity bukkitEntity; + private volatile CraftEntity bukkitEntity; // Folia - region threading public @org.jetbrains.annotations.Nullable net.minecraft.server.level.ChunkMap.TrackedEntity tracker; // Paper public @Nullable Throwable addedToWorldStack; // Paper - entity debug public CraftEntity getBukkitEntity() { if (this.bukkitEntity == null) { - this.bukkitEntity = CraftEntity.getEntity(this.level.getCraftServer(), this); + // Folia start - region threading + synchronized (this) { + if (this.bukkitEntity == null) { + return this.bukkitEntity = CraftEntity.getEntity(this.level.getCraftServer(), this); + } + } + // Folia end - region threading } return this.bukkitEntity; } + // Folia start - region threading + public CraftEntity getBukkitEntityRaw() { + return this.bukkitEntity; + } + // Folia end - region threading + @Override public CommandSender getBukkitSender(CommandSourceStack wrapper) { return this.getBukkitEntity(); @@ -489,28 +501,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { this.isLegacyTrackingEntity = isLegacyTrackingEntity; } - public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getPlayersInTrackRange() { - // determine highest range of passengers - if (this.passengers.isEmpty()) { - return ((ServerLevel)this.level).getChunkSource().chunkMap.playerEntityTrackerTrackMaps[this.trackingRangeType.ordinal()] - .getObjectsInRange(MCUtil.getCoordinateKey(this)); - } - Iterable passengers = this.getIndirectPassengers(); - net.minecraft.server.level.ChunkMap chunkMap = ((ServerLevel)this.level).getChunkSource().chunkMap; - org.spigotmc.TrackingRange.TrackingRangeType type = this.trackingRangeType; - int range = chunkMap.getEntityTrackerRange(type.ordinal()); - - for (Entity passenger : passengers) { - org.spigotmc.TrackingRange.TrackingRangeType passengerType = passenger.trackingRangeType; - int passengerRange = chunkMap.getEntityTrackerRange(passengerType.ordinal()); - if (passengerRange > range) { - type = passengerType; - range = passengerRange; - } - } - - return chunkMap.playerEntityTrackerTrackMaps[type.ordinal()].getObjectsInRange(MCUtil.getCoordinateKey(this)); - } + // Folia - region threading // Paper end - optimise entity tracking // Paper start - make end portalling safe public BlockPos portalBlock; @@ -542,6 +533,25 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { this.teleportTo(worldserver, null); } // Paper end - make end portalling safe + // Folia start + private static final java.util.concurrent.ConcurrentHashMap, Integer> CLASS_ID_MAP = new java.util.concurrent.ConcurrentHashMap<>(); + private static final AtomicInteger CLASS_ID_GENERATOR = new AtomicInteger(); + public final int classId = CLASS_ID_MAP.computeIfAbsent(this.getClass(), (Class c) -> { + return CLASS_ID_GENERATOR.getAndIncrement(); + }); + private static final java.util.concurrent.atomic.AtomicLong REFERENCE_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); + public final long referenceId = REFERENCE_ID_GENERATOR.getAndIncrement(); + // Folia end + // Folia start - region ticking + public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) { + if (this.activatedTick != Integer.MIN_VALUE) { + this.activatedTick += fromTickOffset; + } + if (this.activatedImmunityTick != Integer.MIN_VALUE) { + this.activatedImmunityTick += fromTickOffset; + } + } + // Folia end - region ticking public Entity(EntityType type, Level world) { this.id = Entity.ENTITY_COUNTER.incrementAndGet(); @@ -657,6 +667,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { } public final void discard() { + // Folia start - region threading + if (this.isRemoved()) { + return; + } + // Folia end - region threading this.remove(Entity.RemovalReason.DISCARDED); } @@ -781,6 +796,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { // CraftBukkit start public void postTick() { + // Folia start - region threading + // moved to doPortalLogic + if (true) { + return; + } + // Folia end - region threading // No clean way to break out of ticking once the entity has been copied to a new world, so instead we move the portalling later in the tick cycle if (!(this instanceof ServerPlayer) && this.isAlive()) { // Paper - don't attempt to teleport dead entities this.handleNetherPortal(); @@ -803,7 +824,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { this.walkDistO = this.walkDist; this.xRotO = this.getXRot(); this.yRotO = this.getYRot(); - if (this instanceof ServerPlayer) this.handleNetherPortal(); // CraftBukkit - // Moved up to postTick + //if (this instanceof ServerPlayer) this.handleNetherPortal(); // CraftBukkit - // Moved up to postTick // Folia - region threading - ONLY allow in postTick() if (this.canSpawnSprintParticle()) { this.spawnSprintParticle(); } @@ -904,11 +925,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { // This will be called every single tick the entity is in lava, so don't throw an event this.setSecondsOnFire(15, false); } - CraftEventFactory.blockDamage = (this.lastLavaContact) == null ? null : org.bukkit.craftbukkit.block.CraftBlock.at(level, lastLavaContact); + CraftEventFactory.blockDamageRT.set((this.lastLavaContact) == null ? null : org.bukkit.craftbukkit.block.CraftBlock.at(level, lastLavaContact)); // Folia - region threading if (this.hurt(this.damageSources().lava(), 4.0F)) { this.playSound(SoundEvents.GENERIC_BURN, 0.4F, 2.0F + this.random.nextFloat() * 0.4F); } - CraftEventFactory.blockDamage = null; + CraftEventFactory.blockDamageRT.set(null); // Folia - region threading // CraftBukkit end - we also don't throw an event unless the object in lava is living, to save on some event calls } @@ -1016,8 +1037,8 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { } else { this.wasOnFire = this.isOnFire(); if (movementType == MoverType.PISTON) { - this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20); // Paper - this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20); // Paper + this.activatedTick = Math.max(this.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper + this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper movement = this.limitPistonMovement(movement); if (movement.equals(Vec3.ZERO)) { return; @@ -3090,6 +3111,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { @Nullable public Team getTeam() { + // Folia start - region threading + if (true) { + return null; + } + // Folia end - region threading if (!this.level.paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof Player)) { return null; } // Paper return this.level.getScoreboard().getPlayersTeam(this.getScoreboardName()); } @@ -3205,9 +3231,9 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { if (this.fireImmune()) { return; } - CraftEventFactory.entityDamage = lightning; + CraftEventFactory.entityDamageRT.set(lightning); // Folia - region threading if (!this.hurt(this.damageSources().lightningBolt(), 5.0F)) { - CraftEventFactory.entityDamage = null; + CraftEventFactory.entityDamageRT.set(null); // Folia - region threading return; } // CraftBukkit end @@ -3380,6 +3406,763 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { this.portalEntrancePos = original.portalEntrancePos; } + // Folia start - region threading + public static class EntityTreeNode { + @Nullable + public EntityTreeNode parent; + public Entity root; + @Nullable + public EntityTreeNode[] passengers; + + public EntityTreeNode(EntityTreeNode parent, Entity root) { + this.parent = parent; + this.root = root; + } + + public EntityTreeNode(EntityTreeNode parent, Entity root, EntityTreeNode[] passengers) { + this.parent = parent; + this.root = root; + this.passengers = passengers; + } + + public List getFullTree() { + List ret = new java.util.ArrayList<>(); + ret.add(this); + + // this is just a BFS except we don't remove from head, we just advance down the list + for (int i = 0; i < ret.size(); ++i) { + EntityTreeNode node = ret.get(i); + + EntityTreeNode[] passengers = node.passengers; + if (passengers == null) { + continue; + } + for (EntityTreeNode passenger : passengers) { + ret.add(passenger); + } + } + + return ret; + } + + public void restore() { + java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); + queue.add(this); + + EntityTreeNode curr; + while ((curr = queue.pollFirst()) != null) { + EntityTreeNode[] passengers = curr.passengers; + if (passengers == null) { + continue; + } + + List newPassengers = new java.util.ArrayList<>(); + for (EntityTreeNode passenger : passengers) { + newPassengers.add(passenger.root); + passenger.root.vehicle = curr.root; + } + + curr.root.passengers = ImmutableList.copyOf(newPassengers); + } + } + + public void addTracker() { + for (final EntityTreeNode node : this.getFullTree()) { + if (node.root.tracker != null) { + for (final ServerPlayer player : node.root.level.getLocalPlayers()) { + node.root.tracker.updatePlayer(player); + } + } + } + } + + public void clearTracker() { + for (final EntityTreeNode node : this.getFullTree()) { + if (node.root.tracker != null) { + node.root.tracker.removeNonTickThreadPlayers(); + for (final ServerPlayer player : node.root.level.getLocalPlayers()) { + node.root.tracker.removePlayer(player); + } + } + } + } + + public void adjustRiders() { + java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); + queue.add(this); + + EntityTreeNode curr; + while ((curr = queue.pollFirst()) != null) { + EntityTreeNode[] passengers = curr.passengers; + if (passengers == null) { + continue; + } + + for (EntityTreeNode passenger : passengers) { + curr.root.positionRider(passenger.root, Entity::moveTo); + } + } + } + } + + protected EntityTreeNode makePassengerTree() { + io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot read passengers off of the main thread"); + + EntityTreeNode root = new EntityTreeNode(null, this); + java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); + queue.add(root); + EntityTreeNode curr; + while ((curr = queue.pollFirst()) != null) { + Entity vehicle = curr.root; + List passengers = vehicle.passengers; + if (passengers.isEmpty()) { + continue; + } + + EntityTreeNode[] treePassengers = new EntityTreeNode[passengers.size()]; + curr.passengers = treePassengers; + + for (int i = 0; i < passengers.size(); ++i) { + Entity passenger = passengers.get(i); + queue.addLast(treePassengers[i] = new EntityTreeNode(curr, passenger)); + } + } + + return root; + } + + protected EntityTreeNode detachPassengers() { + io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot adjust passengers/vehicle off of the main thread"); + + EntityTreeNode root = new EntityTreeNode(null, this); + java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); + queue.add(root); + EntityTreeNode curr; + while ((curr = queue.pollFirst()) != null) { + Entity vehicle = curr.root; + List passengers = vehicle.passengers; + if (passengers.isEmpty()) { + continue; + } + + vehicle.passengers = ImmutableList.of(); + + EntityTreeNode[] treePassengers = new EntityTreeNode[passengers.size()]; + curr.passengers = treePassengers; + + for (int i = 0; i < passengers.size(); ++i) { + Entity passenger = passengers.get(i); + passenger.vehicle = null; + queue.addLast(treePassengers[i] = new EntityTreeNode(curr, passenger)); + } + } + + return root; + } + + /** + * This flag will perform an async load on the chunks determined by + * the entity's bounding box before teleporting the entity. + */ + public static final long TELEPORT_FLAG_LOAD_CHUNK = 1L << 0; + /** + * This flag requires the entity being teleported to be a root vehicle. + * Thus, if you want to teleport a non-root vehicle, you must dismount + * the target entity before calling teleport, otherwise the + * teleport will be refused. + */ + public static final long TELEPORT_FLAG_TELEPORT_PASSENGERS = 1L << 1; + /** + * The flag will dismount any passengers and dismout from the current vehicle + * to teleport if and only if dismounting would result in the teleport being allowed. + */ + public static final long TELEPORT_FLAG_UNMOUNT = 1L << 2; + + protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) { + destination.addDuringTeleport(this); + } + + protected final void placeInAsync(ServerLevel originWorld, ServerLevel destination, long teleportFlags, + EntityTreeNode passengerTree, java.util.function.Consumer teleportComplete) { + Vec3 pos = this.position(); + ChunkPos posChunk = new ChunkPos( + io.papermc.paper.util.CoordinateUtils.getChunkX(pos), + io.papermc.paper.util.CoordinateUtils.getChunkZ(pos) + ); + + // ensure the region is always ticking in case of a shutdown + // otherwise, the shutdown will not be able to complete the shutdown as it requires a ticking region + Long teleportHoldId = Long.valueOf(TELEPORT_HOLD_TICKET_GEN.getAndIncrement()); + originWorld.chunkSource.addTicketAtLevel( + TicketType.TELEPORT_HOLD_TICKET, posChunk, + io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + teleportHoldId + ); + final ServerLevel.PendingTeleport pendingTeleport = new ServerLevel.PendingTeleport(passengerTree, pos); + destination.pushPendingTeleport(pendingTeleport); + + Runnable scheduleEntityJoin = () -> { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + destination, + io.papermc.paper.util.CoordinateUtils.getChunkX(pos), io.papermc.paper.util.CoordinateUtils.getChunkZ(pos), + () -> { + if (!destination.removePendingTeleport(pendingTeleport)) { + // shutdown logic placed the entity already, and we are shutting down - do nothing to ensure + // we do not produce any errors here + return; + } + originWorld.chunkSource.removeTicketAtLevel( + TicketType.TELEPORT_HOLD_TICKET, posChunk, + io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + teleportHoldId + ); + List fullTree = passengerTree.getFullTree(); + for (EntityTreeNode node : fullTree) { + node.root.placeSingleSync(originWorld, destination, node, teleportFlags); + } + + // restore passenger tree + passengerTree.restore(); + passengerTree.adjustRiders(); + + // invoke post dimension change now + for (EntityTreeNode node : fullTree) { + node.root.postChangeDimension(); + } + + if (teleportComplete != null) { + teleportComplete.accept(Entity.this); + } + } + ); + }; + + if ((teleportFlags & TELEPORT_FLAG_LOAD_CHUNK) != 0L) { + destination.loadChunksForMoveAsync( + Entity.this.getBoundingBox(), ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, + (chunkList) -> { + for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunkList) { + destination.chunkSource.addTicketAtLevel( + TicketType.POST_TELEPORT, chunk.getPos(), + io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, + Integer.valueOf(Entity.this.getId()) + ); + } + scheduleEntityJoin.run(); + } + ); + } else { + scheduleEntityJoin.run(); + } + } + + protected boolean canTeleportAsync() { + return !this.hasNullCallback() && !this.isRemoved() && this.isAlive() && (!(this instanceof net.minecraft.world.entity.LivingEntity livingEntity) || !livingEntity.isSleeping()); + } + + protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) { + if (yaw != null) { + this.setYRot(yaw.floatValue()); + this.setYHeadRot(yaw.floatValue()); + } + if (pitch != null) { + this.setXRot(pitch.floatValue()); + } + if (speedDirectionUpdate != null) { + this.setDeltaMovement(speedDirectionUpdate.normalize().scale(this.getDeltaMovement().length())); + } + this.moveTo(pos.x, pos.y, pos.z); + } + + protected void transform(Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) { + if (yaw != null) { + this.setYRot(yaw.floatValue()); + this.setYHeadRot(yaw.floatValue()); + } + if (pitch != null) { + this.setXRot(pitch.floatValue()); + } + if (speedDirectionUpdate != null) { + this.setDeltaMovement(speedDirectionUpdate); + } + if (pos != null) { + this.setPosRaw(pos.x, pos.y, pos.z); + } + } + + protected Entity transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) { + this.removeAfterChangingDimensions(); // remove before so that any CBEntity#getHandle call affects this entity before copying + + Entity copy = this.getType().create(destination); + copy.restoreFrom(this); + copy.transform(pos, yaw, pitch, speedDirectionUpdate); + // vanilla code used to call remove _after_ copying, and some stuff is required to be after copy - so add hook here + // for example, clearing of inventory after switching dimensions + this.postRemoveAfterChangingDimensions(); + + return copy; + } + + public final boolean teleportAsync(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate, + org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause, long teleportFlags, + java.util.function.Consumer teleportComplete) { + io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot teleport entity async"); + + if (!ServerLevel.isInSpawnableBounds(new BlockPos(io.papermc.paper.util.CoordinateUtils.getBlockX(pos), io.papermc.paper.util.CoordinateUtils.getBlockY(pos), io.papermc.paper.util.CoordinateUtils.getBlockZ(pos)))) { + return false; + } + + if (!this.canTeleportAsync()) { + return false; + } + this.getBukkitEntity(); // force bukkit entity to be created before TPing + if ((teleportFlags & TELEPORT_FLAG_UNMOUNT) == 0L) { + for (Entity entity : this.getIndirectPassengers()) { + if (!entity.canTeleportAsync()) { + return false; + } + entity.getBukkitEntity(); // force bukkit entity to be created before TPing + } + } else { + this.unRide(); + } + + if ((teleportFlags & TELEPORT_FLAG_TELEPORT_PASSENGERS) != 0L) { + if (this.isPassenger()) { + return false; + } + } else { + if (this.isVehicle() || this.isPassenger()) { + return false; + } + } + + // TODO any events that can modify go HERE + + // check for same region + if (destination == this.getLevel()) { + Vec3 currPos = this.position(); + if ( + destination.regioniser.getRegionAtUnsynchronised( + io.papermc.paper.util.CoordinateUtils.getChunkX(currPos), io.papermc.paper.util.CoordinateUtils.getChunkZ(currPos) + ) == destination.regioniser.getRegionAtUnsynchronised( + io.papermc.paper.util.CoordinateUtils.getChunkX(pos), io.papermc.paper.util.CoordinateUtils.getChunkZ(pos) + ) + ) { + EntityTreeNode passengerTree = this.detachPassengers(); + // Note: The client does not accept position updates for controlled entities. So, we must + // perform a lot of tracker updates here to make it all work out. + + // first, clear the tracker + passengerTree.clearTracker(); + for (EntityTreeNode entity : passengerTree.getFullTree()) { + entity.root.teleportSyncSameRegion(pos, yaw, pitch, speedDirectionUpdate); + } + + passengerTree.restore(); + // re-add to the tracker once the tree is restored + passengerTree.addTracker(); + + // adjust entities to final position + passengerTree.adjustRiders(); + + // the tracker clear/add logic is only used in the same region, as the other logic + // performs add/remove from world logic which will also perform add/remove tracker logic + + if (teleportComplete != null) { + teleportComplete.accept(this); + } + return true; + } + } + + EntityTreeNode passengerTree = this.detachPassengers(); + List fullPassengerTree = passengerTree.getFullTree(); + ServerLevel originWorld = (ServerLevel)this.level; + + for (EntityTreeNode node : fullPassengerTree) { + node.root.preChangeDimension(); + } + + for (EntityTreeNode node : fullPassengerTree) { + node.root = node.root.transformForAsyncTeleport(destination, pos, yaw, pitch, speedDirectionUpdate); + } + + passengerTree.root.placeInAsync(originWorld, destination, teleportFlags, passengerTree, teleportComplete); + + return true; + } + + public void preChangeDimension() { + + } + + public void postChangeDimension() { + + } + + protected static enum PortalType { + NETHER, END; + } + + public boolean doPortalLogic() { + if (this.tryNetherPortal()) { + return true; + } + if (this.tryEndPortal()) { + return true; + } + return false; + } + + protected boolean tryEndPortal() { + io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); + BlockPos pos = this.portalBlock; + ServerLevel world = this.portalWorld; + this.portalBlock = null; + this.portalWorld = null; + + if (pos == null || world == null || world != this.level) { + return false; + } + + if (this.isPassenger() || this.isVehicle() || !this.canChangeDimensions() || this.isRemoved() || !this.valid || !this.isAlive()) { + return false; + } + + return this.endPortalLogicAsync(); + } + + protected boolean tryNetherPortal() { + io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); + + int portalWaitTime = this.getPortalWaitTime(); + + if (this.isInsidePortal) { + // if we are in a nether portal still, this flag will be set next tick. + this.isInsidePortal = false; + if (this.portalTime++ >= portalWaitTime) { + this.portalTime = portalWaitTime; + if (this.netherPortalLogicAsync()) { + return true; + } + } + } else { + // rapidly decrease portal time + this.portalTime = Math.max(0, this.portalTime - 4); + } + + this.processPortalCooldown(); + return false; + } + + public boolean endPortalLogicAsync() { + io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); + + ServerLevel destination = this.getServer().getLevel(this.getLevel().getTypeKey() == LevelStem.END ? Level.OVERWORLD : Level.END); + if (destination == null) { + // wat + return false; + } + + return this.portalToAsync(destination, false, PortalType.END, null); + } + + public boolean netherPortalLogicAsync() { + io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); + + ServerLevel destination = this.getServer().getLevel(this.getLevel().getTypeKey() == LevelStem.NETHER ? Level.OVERWORLD : Level.NETHER); + if (destination == null) { + // wat + return false; + } + + return this.portalToAsync(destination, false, PortalType.NETHER, null); + } + + private static final java.util.concurrent.atomic.AtomicLong CREATE_PORTAL_DOUBLE_CHECK = new java.util.concurrent.atomic.AtomicLong(); + private static final java.util.concurrent.atomic.AtomicLong TELEPORT_HOLD_TICKET_GEN = new java.util.concurrent.atomic.AtomicLong(); + + // To simplify portal logic, in region threading both players + // and non-player entities will create portals. By guaranteeing + // that the teleportation can take place, we can simply + // remove the entity, find/create the portal, and place async. + // If we have to worry about whether the entity may not teleport, + // we need to first search, then report back, ... + protected void findOrCreatePortalAsync(ServerLevel origin, ServerLevel destination, PortalType type, + ca.spottedleaf.concurrentutil.completable.Completable portalInfoCompletable) { + switch (type) { + // end portal logic is quite simple, the spawn in the end is fixed and when returning to the overworld + // we just select the spawn position + case END: { + if (destination.getTypeKey() == LevelStem.END) { + BlockPos targetPos = ServerLevel.END_SPAWN_POINT; + // need to load chunks so we can create the platform + destination.loadChunksAsync( + targetPos, 16, // load 16 blocks to be safe from block physics + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH, + (chunks) -> { + ServerLevel.makeObsidianPlatform(destination, null, targetPos); + + // the portal obsidian is placed at targetPos.y - 2, so if we want to place the entity + // on the obsidian, we need to spawn at targetPos.y - 1 + portalInfoCompletable.complete( + new PortalInfo(Vec3.atBottomCenterOf(targetPos.below()), Vec3.ZERO, 90.0f, 0.0f, destination, null) + ); + } + ); + } else { + BlockPos spawnPos = destination.getSharedSpawnPos(); + // need to load chunk for heightmap + destination.loadChunksAsync( + spawnPos, 0, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH, + (chunks) -> { + BlockPos adjustedSpawn = destination.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, spawnPos); + + // done + portalInfoCompletable.complete( + new PortalInfo(Vec3.atBottomCenterOf(adjustedSpawn), Vec3.ZERO, 90.0f, 0.0f, destination, null) + ); + } + ); + } + + break; + } + // for the nether logic, we need to first load the chunks in radius to empty (so that POI is created) + // then we can search for an existing portal using the POI routines + // if we don't find a portal, then we bring the chunks in the create radius to full and + // create it + case NETHER: { + // hoisted from the create fallback, so that we can avoid the sync load later if we need it + BlockState originalPortalBlock = this.portalEntrancePos == null ? null : origin.getBlockStateIfLoaded(this.portalEntrancePos); + Direction.Axis originalPortalDirection = originalPortalBlock == null ? Direction.Axis.X : + originalPortalBlock.getOptionalValue(net.minecraft.world.level.block.NetherPortalBlock.AXIS).orElse(Direction.Axis.X); + BlockUtil.FoundRectangle originalPortalRectangle = + originalPortalBlock == null || !originalPortalBlock.hasProperty(BlockStateProperties.HORIZONTAL_AXIS) + ? null + : BlockUtil.getLargestRectangleAround( + this.portalEntrancePos, originalPortalDirection, 21, Direction.Axis.Y, 21, + (blockpos) -> { + return origin.getBlockStateFromEmptyChunkIfLoaded(blockpos) == originalPortalBlock; + } + ); + + boolean destinationIsNether = destination.getTypeKey() == LevelStem.NETHER; + + int portalSearchRadius = origin.paperConfig().environment.portalSearchVanillaDimensionScaling && destinationIsNether ? + (int)(destination.paperConfig().environment.portalSearchRadius / destination.dimensionType().coordinateScale()) : + destination.paperConfig().environment.portalSearchRadius; + int portalCreateRadius = destination.paperConfig().environment.portalCreateRadius; + + WorldBorder destinationBorder = destination.getWorldBorder(); + double dimensionScale = DimensionType.getTeleportationScale(origin.dimensionType(), destination.dimensionType()); + BlockPos targetPos = destination.getWorldBorder().clampToBounds(this.getX() * dimensionScale, this.getY(), this.getZ() * dimensionScale); + + ca.spottedleaf.concurrentutil.completable.Completable portalFound + = new ca.spottedleaf.concurrentutil.completable.Completable<>(); + + // post portal find/create logic + portalFound.addWaiter( + (BlockUtil.FoundRectangle portal, Throwable thr) -> { + // no portal could be created + if (portal == null) { + portalInfoCompletable.complete( + new PortalInfo(Vec3.atCenterOf(targetPos), Vec3.ZERO, 90.0f, 0.0f, destination, null) + ); + return; + } + + Vec3 relativePos = originalPortalRectangle == null ? + new Vec3(0.5, 0.0, 0.0) : + Entity.this.getRelativePortalPosition(originalPortalDirection, originalPortalRectangle); + + portalInfoCompletable.complete( + PortalShape.createPortalInfo( + destination, portal, originalPortalDirection, relativePos, + Entity.this, Entity.this.getDeltaMovement(), Entity.this.getYRot(), Entity.this.getXRot(), null + ) + ); + } + ); + + // kick off search for existing portal or creation + destination.loadChunksAsync( + // add 32 so that the final search for a portal frame doesn't load any chunks + targetPos, portalSearchRadius + 32, + net.minecraft.world.level.chunk.ChunkStatus.EMPTY, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH, + (chunks) -> { + BlockUtil.FoundRectangle portal = + destination.getPortalForcer().findPortalAround(targetPos, destinationBorder, portalSearchRadius).orElse(null); + if (portal != null) { + portalFound.complete(portal); + return; + } + + // add tickets so that we can re-search for a portal once the chunks are loaded + Long ticketId = Long.valueOf(CREATE_PORTAL_DOUBLE_CHECK.getAndIncrement()); + for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunks) { + destination.chunkSource.addTicketAtLevel( + TicketType.NETHER_PORTAL_DOUBLE_CHECK, chunk.getPos(), + io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + ticketId + ); + } + + // no portal found - create one + destination.loadChunksAsync( + targetPos, portalCreateRadius + 32, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH, + (chunks2) -> { + // don't need the tickets anymore + // note: we expect removeTicketsAtLevel to add an unknown ticket for us automatically + // if the ticket level were to decrease + for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunks) { + destination.chunkSource.removeTicketAtLevel( + TicketType.NETHER_PORTAL_DOUBLE_CHECK, chunk.getPos(), + io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + ticketId + ); + } + + // when two entities portal at the same time, it is possible that both entities reach this + // part of the code - and create a double portal + // to fix this, we just issue another search to try and see if another entity created + // a portal nearby + BlockUtil.FoundRectangle existingTryAgain = + destination.getPortalForcer().findPortalAround(targetPos, destinationBorder, portalSearchRadius).orElse(null); + if (existingTryAgain != null) { + portalFound.complete(existingTryAgain); + return; + } + + // we do not have the correct entity reference here + BlockUtil.FoundRectangle createdPortal = + destination.getPortalForcer().createPortal(targetPos, originalPortalDirection, null, portalCreateRadius).orElse(null); + // if it wasn't created, passing null is expected here + portalFound.complete(createdPortal); + } + ); + } + ); + break; + } + default: { + throw new IllegalStateException("Unknown portal type " + type); + } + } + } + + public boolean canPortalAsync(boolean considerPassengers) { + return this.canPortalAsync(considerPassengers, false); + } + + protected boolean canPortalAsync(boolean considerPassengers, boolean skipPassengerCheck) { + if (considerPassengers) { + if (!skipPassengerCheck && this.isPassenger()) { + return false; + } + } else { + if (this.isVehicle() || (!skipPassengerCheck && this.isPassenger())) { + return false; + } + } + this.getBukkitEntity(); // force bukkit entity to be created before TPing + if (!this.canTeleportAsync() || !this.canChangeDimensions() || this.isOnPortalCooldown()) { + return false; + } + if (considerPassengers) { + for (Entity entity : this.passengers) { + if (!entity.canPortalAsync(true, true)) { + return false; + } + } + } + + return true; + } + + protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) { + + } + + protected boolean portalToAsync(ServerLevel destination, boolean takePassengers, + PortalType type, java.util.function.Consumer teleportComplete) { + io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); + if (!this.canPortalAsync(takePassengers)) { + return false; + } + + Vec3 initialPosition = this.position(); + ChunkPos initialPositionChunk = new ChunkPos( + io.papermc.paper.util.CoordinateUtils.getChunkX(initialPosition), + io.papermc.paper.util.CoordinateUtils.getChunkZ(initialPosition) + ); + + // first, remove entity/passengers from world + EntityTreeNode passengerTree = this.detachPassengers(); + List fullPassengerTree = passengerTree.getFullTree(); + ServerLevel originWorld = (ServerLevel)this.level; + + for (EntityTreeNode node : fullPassengerTree) { + node.root.preChangeDimension(); + node.root.prePortalLogic(originWorld, destination, type); + } + + for (EntityTreeNode node : fullPassengerTree) { + // we will update pos/rot/speed later + node.root = node.root.transformForAsyncTeleport(destination, null, null, null, null); + // set portal cooldown + node.root.setPortalCooldown(); + } + + // ensure the region is always ticking in case of a shutdown + // otherwise, the shutdown will not be able to complete the shutdown as it requires a ticking region + Long teleportHoldId = Long.valueOf(TELEPORT_HOLD_TICKET_GEN.getAndIncrement()); + originWorld.chunkSource.addTicketAtLevel( + TicketType.TELEPORT_HOLD_TICKET, initialPositionChunk, + io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + teleportHoldId + ); + + ServerLevel.PendingTeleport beforeFindDestination = new ServerLevel.PendingTeleport(passengerTree, initialPosition); + originWorld.pushPendingTeleport(beforeFindDestination); + + ca.spottedleaf.concurrentutil.completable.Completable portalInfoCompletable + = new ca.spottedleaf.concurrentutil.completable.Completable<>(); + + portalInfoCompletable.addWaiter((PortalInfo info, Throwable throwable) -> { + if (!originWorld.removePendingTeleport(beforeFindDestination)) { + // the shutdown thread has placed us back into the origin world at the original position + // we just have to abandon this teleport to prevent duplication + return; + } + originWorld.chunkSource.removeTicketAtLevel( + TicketType.TELEPORT_HOLD_TICKET, initialPositionChunk, + io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + teleportHoldId + ); + // adjust passenger tree to final pos + for (EntityTreeNode node : fullPassengerTree) { + node.root.transform(info.pos, Float.valueOf(info.yRot), Float.valueOf(info.xRot), info.speed); + } + + // place + passengerTree.root.placeInAsync( + originWorld, destination, Entity.TELEPORT_FLAG_LOAD_CHUNK | (takePassengers ? Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS : 0L), + passengerTree, teleportComplete + ); + }); + + + passengerTree.root.findOrCreatePortalAsync(originWorld, destination, type, portalInfoCompletable); + + return true; + } + // Folia end - region threading + @Nullable public Entity changeDimension(ServerLevel destination) { // CraftBukkit start @@ -3388,6 +4171,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { @Nullable public Entity teleportTo(ServerLevel worldserver, PositionImpl location) { + // Folia start - region threading + if (true) { + throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); + } + // Folia end - region threading // CraftBukkit end // Paper start - fix bad state entities causing dupes if (!isAlive() || !valid) { @@ -3471,6 +4259,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { } } + // Folia start - region threading - move inventory clearing until after the dimension change + protected void postRemoveAfterChangingDimensions() { + + } + // Folia end - region threading - move inventory clearing until after the dimension change + protected void removeAfterChangingDimensions() { this.setRemoved(Entity.RemovalReason.CHANGED_DIMENSION); } @@ -3910,17 +4704,13 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { // Paper start public void startSeenByPlayer(ServerPlayer player) { - if (io.papermc.paper.event.player.PlayerTrackEntityEvent.getHandlerList().getRegisteredListeners().length > 0) { - new io.papermc.paper.event.player.PlayerTrackEntityEvent(player.getBukkitEntity(), this.getBukkitEntity()).callEvent(); - } + // Folia - region threading - no } // Paper end // Paper start public void stopSeenByPlayer(ServerPlayer player) { - if(io.papermc.paper.event.player.PlayerUntrackEntityEvent.getHandlerList().getRegisteredListeners().length > 0) { - new io.papermc.paper.event.player.PlayerUntrackEntityEvent(player.getBukkitEntity(), this.getBukkitEntity()).callEvent(); - } + // Folia - region threading - no } // Paper end @@ -4411,7 +5201,8 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { } } // Paper end - fix MC-4 - if (this.position.x != x || this.position.y != y || this.position.z != z) { + boolean posChanged = this.position.x != x || this.position.y != y || this.position.z != z; // Folia - region threading + if (posChanged) { // Folia - region threading synchronized (this.posLock) { // Paper this.position = new Vec3(x, y, z); } // Paper @@ -4432,7 +5223,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { // Paper start - never allow AABB to become desynced from position // hanging has its own special logic - if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || this.position.x != x || this.position.y != y || this.position.z != z)) { + if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || posChanged)) { this.setBoundingBox(this.makeBoundingBox()); } // Paper end @@ -4519,6 +5310,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { return this.removalReason != null; } + // Folia start - region threading + public final boolean hasNullCallback() { + return this.levelCallback == EntityInLevelCallback.NULL; + } + // Folia end - region threading + @Nullable public Entity.RemovalReason getRemovalReason() { return this.removalReason; @@ -4543,7 +5340,23 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { if (reason != RemovalReason.UNLOADED_TO_CHUNK) this.getPassengers().forEach(Entity::stopRiding); // Paper - chunk system - don't adjust passenger state when unloading, it's just not safe (and messes with our logic in entity chunk unload) this.levelCallback.onRemove(reason); + // Folia start - region threading + if (!(this instanceof ServerPlayer) && reason != RemovalReason.CHANGED_DIMENSION) { + // Players need to be special cased, because they are regularly removed from the world + this.retireScheduler(); + } + // Folia end - region threading + } + + // Folia start - region threading + /** + * Invoked only when the entity is truly removed from the server, never to be added to any world. + */ + public final void retireScheduler() { + // we need to force create the bukkit entity so that the scheduler can be retired... + this.getBukkitEntity().taskScheduler.retire(); } + // Folia end - region threading public void unsetRemoved() { this.removalReason = null; diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java index dcfb71b5a53df789e366fea2080921d677549a2e..4f8062432cfe6480664e674fde0255f07b15c73a 100644 --- a/src/main/java/net/minecraft/world/entity/LivingEntity.java +++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java @@ -464,7 +464,7 @@ public abstract class LivingEntity extends Entity implements Attackable { if (this.isDeadOrDying() && this.level.shouldTickDeath(this)) { this.tickDeath(); - } + } else { this.broadcastedDeath = false; } // Folia - region threading if (this.lastHurtByPlayerTime > 0) { --this.lastHurtByPlayerTime; @@ -610,11 +610,14 @@ public abstract class LivingEntity extends Entity implements Attackable { return true; } + public boolean broadcastedDeath = false; // Folia - region threading protected void tickDeath() { ++this.deathTime; - if (this.deathTime >= 20 && !this.level.isClientSide() && !this.isRemoved()) { + if (this.deathTime >= 20 && !this.level.isClientSide() && !this.isRemoved() && !this.broadcastedDeath) { // Folia - region threading + this.broadcastedDeath = true; // Folia - region threading this.level.broadcastEntityEvent(this, (byte) 60); - this.remove(Entity.RemovalReason.KILLED); + if (!(this instanceof ServerPlayer)) this.remove(Entity.RemovalReason.KILLED); // Folia - region threading - don't remove, we want the tick scheduler to be running + if ((this instanceof ServerPlayer)) this.unRide(); // Folia - region threading - unmount player when dead } } @@ -835,9 +838,9 @@ public abstract class LivingEntity extends Entity implements Attackable { } this.hurtTime = nbt.getShort("HurtTime"); - this.deathTime = nbt.getShort("DeathTime"); + this.deathTime = nbt.getShort("DeathTime"); this.broadcastedDeath = false; // Folia - region threading this.lastHurtByMobTimestamp = nbt.getInt("HurtByTimestamp"); - if (nbt.contains("Team", 8)) { + if (false && nbt.contains("Team", 8)) { // Folia start - region threading String s = nbt.getString("Team"); PlayerTeam scoreboardteam = this.level.getScoreboard().getPlayerTeam(s); if (!level.paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof net.minecraft.world.entity.player.Player)) { scoreboardteam = null; } // Paper @@ -1117,7 +1120,7 @@ public abstract class LivingEntity extends Entity implements Attackable { } public boolean addEffect(MobEffectInstance mobeffect, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause) { - org.spigotmc.AsyncCatcher.catchOp("effect add"); // Spigot + io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot add effects to entities asynchronously"); // Folia - region threading if (this.isTickingEffects) { this.effectsToProcess.add(new ProcessableEffect(mobeffect, cause)); return true; @@ -2266,7 +2269,7 @@ public abstract class LivingEntity extends Entity implements Attackable { @Nullable public LivingEntity getKillCredit() { - return (LivingEntity) (this.combatTracker.getKiller() != null ? this.combatTracker.getKiller() : (this.lastHurtByPlayer != null ? this.lastHurtByPlayer : (this.lastHurtByMob != null ? this.lastHurtByMob : null))); + return (LivingEntity) (this.combatTracker.getKiller() != null ? this.combatTracker.getKiller() : (this.lastHurtByPlayer != null && io.papermc.paper.util.TickThread.isTickThreadFor(this.lastHurtByPlayer) ? this.lastHurtByPlayer : (this.lastHurtByMob != null && io.papermc.paper.util.TickThread.isTickThreadFor(this.lastHurtByMob) ? this.lastHurtByMob : null))); // Folia - region threading } public final float getMaxHealth() { @@ -3394,7 +3397,7 @@ public abstract class LivingEntity extends Entity implements Attackable { this.pushEntities(); this.level.getProfiler().pop(); // Paper start - if (((ServerLevel) this.level).hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { + if (((ServerLevel) this.level).getCurrentWorldData().hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { if (this.xo != getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) { Location from = new Location(this.level.getWorld(), this.xo, this.yo, this.zo, this.yRotO, this.xRotO); Location to = new Location (this.level.getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); @@ -4054,7 +4057,7 @@ public abstract class LivingEntity extends Entity implements Attackable { BlockPos blockposition = BlockPos.containing(d0, d1, d2); Level world = this.level; - if (world.hasChunkAt(blockposition)) { + if (io.papermc.paper.util.TickThread.isTickThreadFor((ServerLevel)world, blockposition) && world.hasChunkAt(blockposition)) { // Folia - region threading boolean flag2 = false; while (!flag2 && blockposition.getY() > world.getMinBuildHeight()) { diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java index 02cb6b8c1d59855ff4a8aad3024fe12007eca0ee..a719dc69b48ae867ce0b508d3d640e65d2863068 100644 --- a/src/main/java/net/minecraft/world/entity/Mob.java +++ b/src/main/java/net/minecraft/world/entity/Mob.java @@ -135,6 +135,14 @@ public abstract class Mob extends LivingEntity implements Targeting { public boolean aware = true; // CraftBukkit + // Folia start - region threading + @Override + public void preChangeDimension() { + super.preChangeDimension(); + this.dropLeash(true, true); + } + // Folia end - region threading + protected Mob(EntityType type, Level world) { super(type, world); this.handItems = NonNullList.withSize(2, ItemStack.EMPTY); @@ -283,9 +291,21 @@ public abstract class Mob extends LivingEntity implements Targeting { @Nullable @Override public LivingEntity getTarget() { + // Folia start - region threading + if (!io.papermc.paper.util.TickThread.isTickThreadFor(this.target)) { + this.target = null; + return null; + } + // Folia end - region threading return this.target; } + // Folia start - region threading + public LivingEntity getTargetRaw() { + return this.target; + } + // Folia end - region threading + 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 @@ -293,7 +313,7 @@ public abstract class Mob extends LivingEntity implements Targeting { } public boolean setTarget(LivingEntity entityliving, EntityTargetEvent.TargetReason reason, boolean fireEvent) { - if (this.getTarget() == entityliving) return false; + if (this.getTargetRaw() == entityliving) return false; // Folia - region threading if (fireEvent) { if (reason == EntityTargetEvent.TargetReason.UNKNOWN && this.getTarget() != null && entityliving == null) { reason = this.getTarget().isAlive() ? EntityTargetEvent.TargetReason.FORGOT_TARGET : EntityTargetEvent.TargetReason.TARGET_DIED; @@ -855,12 +875,7 @@ public abstract class Mob extends LivingEntity implements Targeting { if (this.level.getDifficulty() == Difficulty.PEACEFUL && this.shouldDespawnInPeaceful()) { this.discard(); } else if (!this.isPersistenceRequired() && !this.requiresCustomPersistence()) { - // Paper start - optimise checkDespawn - Player entityhuman = this.level.findNearbyPlayer(this, level.paperConfig().entities.spawning.despawnRanges.get(this.getType().getCategory()).hard() + 1, EntitySelector.PLAYER_AFFECTS_SPAWNING); // Paper - if (entityhuman == null) { - entityhuman = ((ServerLevel)this.level).playersAffectingSpawning.isEmpty() ? null : ((ServerLevel)this.level).playersAffectingSpawning.get(0); - } - // Paper end - optimise checkDespawn + Player entityhuman = this.level.getNearestPlayer(this, -1.0D); // Folia - region threading if (entityhuman != null) { double d0 = entityhuman.distanceToSqr((Entity) this); @@ -1728,6 +1743,15 @@ public abstract class Mob extends LivingEntity implements Targeting { this.goalSelector.removeAllGoals(predicate); } + // Folia start - region threading - move inventory clearing until after the dimension change + @Override + protected void postRemoveAfterChangingDimensions() { + this.getAllSlots().forEach((itemstack) -> { + if (!itemstack.isEmpty()) itemstack.setCount(0); // CraftBukkit + }); + } + // Folia end - region threading - move inventory clearing until after the dimension change + @Override protected void removeAfterChangingDimensions() { super.removeAfterChangingDimensions(); @@ -1736,9 +1760,7 @@ public abstract class Mob extends LivingEntity implements Targeting { this.level.getCraftServer().getPluginManager().callEvent(event); // CraftBukkit this.dropLeash(true, event.isDropLeash()); // Paper end - this.getAllSlots().forEach((itemstack) -> { - if (!itemstack.isEmpty()) itemstack.setCount(0); // CraftBukkit - }); + // Folia - region threading - move inventory clearing until after the dimension change - move into postRemoveAfterChangingDimensions } @Nullable diff --git a/src/main/java/net/minecraft/world/entity/ai/Brain.java b/src/main/java/net/minecraft/world/entity/ai/Brain.java index 328c3ecd0d35d2cad15173ec80962cee9177eaf8..625e03abc4a62f42774fedee31a5f5f776169674 100644 --- a/src/main/java/net/minecraft/world/entity/ai/Brain.java +++ b/src/main/java/net/minecraft/world/entity/ai/Brain.java @@ -412,9 +412,17 @@ public class Brain { } public void stopAll(ServerLevel world, E entity) { + // Folia start - region threading + List> behaviors = this.getRunningBehaviors(); + if (behaviors.isEmpty()) { + // avoid calling getGameTime, as this may be called while portalling an entity - which will cause + // the world data retrieval to fail + return; + } + // Folia end - region threading long l = entity.level.getGameTime(); - for(BehaviorControl behaviorControl : this.getRunningBehaviors()) { + for(BehaviorControl behaviorControl : behaviors) { // Folia - region threading behaviorControl.doStop(world, entity, l); } diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java b/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java index 8ec07578c1e41997a2e5ef158885ad3f4c2a31b6..6dcacfca6eb4a8a6425f1aaeb57733d29989032a 100644 --- a/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java +++ b/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java @@ -17,6 +17,12 @@ public class PoiCompetitorScan { return context.group(context.present(MemoryModuleType.JOB_SITE), context.present(MemoryModuleType.NEAREST_LIVING_ENTITIES)).apply(context, (jobSite, mobs) -> { return (world, entity, time) -> { GlobalPos globalPos = context.get(jobSite); + // Folia start - region threading + if (globalPos.dimension() != world.dimension() || !io.papermc.paper.util.TickThread.isTickThreadFor(world, globalPos.pos())) { + entity.getBrain().eraseMemory(MemoryModuleType.JOB_SITE); + return true; + } + // Folia end - region threading world.getPoiManager().getType(globalPos.pos()).ifPresent((poiType) -> { context.>get(mobs).stream().filter((mob) -> { return mob instanceof Villager && mob != entity; diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java index 4fe177c950c769f5c6e76b522019b5d5b78259a5..624a4cc8a03880b74fb44bf16e0e35e4398eb1f9 100644 --- a/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java +++ b/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java @@ -70,7 +70,7 @@ public class FollowOwnerGoal extends Goal { @Override public boolean canContinueToUse() { - return this.navigation.isDone() ? false : (this.unableToMove() ? false : this.tamable.distanceToSqr((Entity) this.owner) > (double) (this.stopDistance * this.stopDistance)); + return this.navigation.isDone() ? false : (this.unableToMove() ? false : (this.owner.level == this.level && this.tamable.distanceToSqr((Entity) this.owner) > (double) (this.stopDistance * this.stopDistance))); // Folia - region threading - check level } private boolean unableToMove() { @@ -96,7 +96,7 @@ public class FollowOwnerGoal extends Goal { if (this.tamable.distanceToSqr(this.owner) <= 16 * 16) this.tamable.getLookControl().setLookAt(this.owner, 10.0F, (float) this.tamable.getMaxHeadXRot()); // Paper if (--this.timeToRecalcPath <= 0) { this.timeToRecalcPath = this.adjustedTickDelay(10); - if (this.tamable.distanceToSqr((Entity) this.owner) >= 144.0D) { + if (!io.papermc.paper.util.TickThread.isTickThreadFor(this.owner) || this.tamable.distanceToSqr((Entity) this.owner) >= 144.0D) { // Folia - region threading - required in case the player suddenly moves into another dimension this.teleportToOwner(); } else { this.navigation.moveTo((Entity) this.owner, this.speedModifier); @@ -107,6 +107,11 @@ public class FollowOwnerGoal extends Goal { private void teleportToOwner() { BlockPos blockposition = this.owner.blockPosition(); + // Folia start - region threading + if (this.owner.isRemoved() || this.owner.level != level) { + return; + } + // Folia end - region threading for (int i = 0; i < 10; ++i) { int j = this.randomIntInclusive(-3, 3); @@ -137,7 +142,21 @@ public class FollowOwnerGoal extends Goal { } to = event.getTo(); - this.tamable.moveTo(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch()); + // Folia start - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick + // also, use teleportAsync so that crossing region boundaries will not blow up + Location finalTo = to; + this.tamable.getBukkitEntity().taskScheduler.schedule((TamableAnimal nmsEntity) -> { + if (nmsEntity.level == FollowOwnerGoal.this.level) { + nmsEntity.teleportAsync( + (net.minecraft.server.level.ServerLevel)nmsEntity.level, + new net.minecraft.world.phys.Vec3(finalTo.getX(), finalTo.getY(), finalTo.getZ()), + Float.valueOf(finalTo.getYaw()), Float.valueOf(finalTo.getPitch()), + net.minecraft.world.phys.Vec3.ZERO, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN, Entity.TELEPORT_FLAG_LOAD_CHUNK, + null + ); + } + }, null, 1L); + // Folia start - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick // CraftBukkit end this.navigation.stop(); return true; diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java index 7ffe5bef3778d5971ea4ceadf3102725fd0d08cd..bd6910c279a3a319744d3c7c889a889b892a8f76 100644 --- a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +++ b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java @@ -80,11 +80,11 @@ public abstract class PathNavigation { } public void recomputePath() { - if (this.level.getGameTime() - this.timeLastRecompute > 20L) { + if (this.tick - this.timeLastRecompute > 20L) { // Folia - region threading if (this.targetPos != null) { this.path = null; this.path = this.createPath(this.targetPos, this.reachRange); - this.timeLastRecompute = this.level.getGameTime(); + this.timeLastRecompute = this.tick; // Folia - region threading this.hasDelayedRecomputation = false; } } else { @@ -199,7 +199,7 @@ public abstract class PathNavigation { public boolean moveTo(Entity entity, double speed) { // Paper start - Pathfinding optimizations - if (this.pathfindFailures > 10 && this.path == null && net.minecraft.server.MinecraftServer.currentTick < this.lastFailure + 40) { + if (this.pathfindFailures > 10 && this.path == null && this.tick < this.lastFailure + 40) { // Folia - region threading return false; } // Paper end @@ -211,7 +211,7 @@ public abstract class PathNavigation { return true; } else { this.pathfindFailures++; - this.lastFailure = net.minecraft.server.MinecraftServer.currentTick; + this.lastFailure = this.tick; // Folia - region threading return false; } // Paper end diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java index e3242cf9a6ad51a23c5781142198dec30c8f376d..f32f5982ceb368b240062b9b8ac0141be59e2f1e 100644 --- a/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java +++ b/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java @@ -37,7 +37,7 @@ public class TemptingSensor extends Sensor { protected void doTick(ServerLevel world, PathfinderMob entity) { Brain behaviorcontroller = entity.getBrain(); - Stream stream = world.players().stream().filter(EntitySelector.NO_SPECTATORS).filter((entityplayer) -> { // CraftBukkit - decompile error + Stream stream = world.getLocalPlayers().stream().filter(EntitySelector.NO_SPECTATORS).filter((entityplayer) -> { // CraftBukkit - decompile error // Folia - region threading return TemptingSensor.TEMPT_TARGETING.test(entity, entityplayer); }).filter((entityplayer) -> { return entity.closerThan(entityplayer, 10.0D); diff --git a/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java b/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java index fed09b886f4fa0006d160e5f2abb00dfee45434d..69075026c25ed1ce0f3c769ea0e4a8f3c4841f63 100644 --- a/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java +++ b/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java @@ -22,62 +22,66 @@ import org.slf4j.Logger; public class VillageSiege implements CustomSpawner { private static final Logger LOGGER = LogUtils.getLogger(); - private boolean hasSetupSiege; - private VillageSiege.State siegeState; - private int zombiesToSpawn; - private int nextSpawnTime; - private int spawnX; - private int spawnY; - private int spawnZ; + // Folia - region threading public VillageSiege() { - this.siegeState = VillageSiege.State.SIEGE_DONE; + // Folia - region threading } @Override public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + // Folia start - region threading + // check if the spawn pos is no longer owned by this region + if (worldData.villageSiegeState.siegeState != State.SIEGE_DONE + && !io.papermc.paper.util.TickThread.isTickThreadFor(world, worldData.villageSiegeState.spawnX >> 4, worldData.villageSiegeState.spawnZ >> 4, 8)) { + // can't spawn here, just re-set + worldData.villageSiegeState = new io.papermc.paper.threadedregions.RegionizedWorldData.VillageSiegeState(); + } + // Folia end - region threading if (!world.isDay() && spawnMonsters) { float f = world.getTimeOfDay(0.0F); if ((double) f == 0.5D) { - this.siegeState = world.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; + worldData.villageSiegeState.siegeState = world.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; // Folia - region threading } - if (this.siegeState == VillageSiege.State.SIEGE_DONE) { + if (worldData.villageSiegeState.siegeState == VillageSiege.State.SIEGE_DONE) { // Folia - region threading return 0; } else { - if (!this.hasSetupSiege) { + if (!worldData.villageSiegeState.hasSetupSiege) { // Folia - region threading if (!this.tryToSetupSiege(world)) { return 0; } - this.hasSetupSiege = true; + worldData.villageSiegeState.hasSetupSiege = true; // Folia - region threading } - if (this.nextSpawnTime > 0) { - --this.nextSpawnTime; + if (worldData.villageSiegeState.nextSpawnTime > 0) { // Folia - region threading + --worldData.villageSiegeState.nextSpawnTime; // Folia - region threading return 0; } else { - this.nextSpawnTime = 2; - if (this.zombiesToSpawn > 0) { + worldData.villageSiegeState.nextSpawnTime = 2; // Folia - region threading + if (worldData.villageSiegeState.zombiesToSpawn > 0) { // Folia - region threading this.trySpawn(world); - --this.zombiesToSpawn; + --worldData.villageSiegeState.zombiesToSpawn; // Folia - region threading } else { - this.siegeState = VillageSiege.State.SIEGE_DONE; + worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Folia - region threading } return 1; } } } else { - this.siegeState = VillageSiege.State.SIEGE_DONE; - this.hasSetupSiege = false; + worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Folia - region threading + worldData.villageSiegeState.hasSetupSiege = false; // Folia - region threading return 0; } } private boolean tryToSetupSiege(ServerLevel world) { - Iterator iterator = world.players().iterator(); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + Iterator iterator = world.getLocalPlayers().iterator(); // Folia - region threading while (iterator.hasNext()) { Player entityhuman = (Player) iterator.next(); @@ -89,12 +93,12 @@ public class VillageSiege implements CustomSpawner { for (int i = 0; i < 10; ++i) { float f = world.random.nextFloat() * 6.2831855F; - this.spawnX = blockposition.getX() + Mth.floor(Mth.cos(f) * 32.0F); - this.spawnY = blockposition.getY(); - this.spawnZ = blockposition.getZ() + Mth.floor(Mth.sin(f) * 32.0F); - if (this.findRandomSpawnPos(world, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)) != null) { - this.nextSpawnTime = 0; - this.zombiesToSpawn = 20; + worldData.villageSiegeState.spawnX = blockposition.getX() + Mth.floor(Mth.cos(f) * 32.0F); // Folia - region threading + worldData.villageSiegeState.spawnY = blockposition.getY(); // Folia - region threading + worldData.villageSiegeState.spawnZ = blockposition.getZ() + Mth.floor(Mth.sin(f) * 32.0F); // Folia - region threading + if (this.findRandomSpawnPos(world, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)) != null) { // Folia - region threading + worldData.villageSiegeState.nextSpawnTime = 0; // Folia - region threading + worldData.villageSiegeState.zombiesToSpawn = 20; // Folia - region threading break; } } @@ -108,7 +112,8 @@ public class VillageSiege implements CustomSpawner { } private void trySpawn(ServerLevel world) { - Vec3 vec3d = this.findRandomSpawnPos(world, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + Vec3 vec3d = this.findRandomSpawnPos(world, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)); // Folia - region threading if (vec3d != null) { Zombie entityzombie; @@ -143,7 +148,7 @@ public class VillageSiege implements CustomSpawner { return null; } - private static enum State { + public static enum State { // Folia - region threading SIEGE_CAN_ACTIVATE, SIEGE_TONIGHT, SIEGE_DONE; diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java index 9be85eb0abec02bc0e0eded71c34ab1c565c63e7..9c56304476b4fc841b5d7694232617586ebd8e84 100644 --- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java @@ -48,11 +48,13 @@ public class PoiManager extends SectionStorage { } protected void updateDistanceTracking(long section) { + synchronized (this.villageDistanceTracker) { // Folia - region threading if (this.isVillageCenter(section)) { this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE); } else { this.villageDistanceTracker.removeSource(section); } + } // Folia - region threading } // Paper end - rewrite chunk system @@ -215,8 +217,10 @@ public class PoiManager extends SectionStorage { } public int sectionsToVillage(SectionPos pos) { + synchronized (this.villageDistanceTracker) { // Folia - region threading this.villageDistanceTracker.propagateUpdates(); // Paper - replace distance tracking util return convertBetweenLevels(this.villageDistanceTracker.getLevel(io.papermc.paper.util.CoordinateUtils.getChunkSectionKey(pos))); // Paper - replace distance tracking util + } // Folia - region threading } boolean isVillageCenter(long pos) { @@ -230,7 +234,9 @@ public class PoiManager extends SectionStorage { @Override public void tick(BooleanSupplier shouldKeepTicking) { + synchronized (this.villageDistanceTracker) { // Folia - region threading this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system + } // Folia - region threading } @Override diff --git a/src/main/java/net/minecraft/world/entity/animal/Cat.java b/src/main/java/net/minecraft/world/entity/animal/Cat.java index 72b30a5cdeb8a43702d9ab5f198311929761fad1..3951a1f740839f286a190f3fda66132fc4e7ead5 100644 --- a/src/main/java/net/minecraft/world/entity/animal/Cat.java +++ b/src/main/java/net/minecraft/world/entity/animal/Cat.java @@ -363,7 +363,7 @@ public class Cat extends TamableAnimal implements VariantHolder { }); ServerLevel worldserver = world.getLevel(); - if (worldserver.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK, world).isValid()) { // Paper - fix deadlock + if (world.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK).isValid()) { // Paper - fix deadlock // Folia - region threading - properly fix this this.setVariant((CatVariant) BuiltInRegistries.CAT_VARIANT.getOrThrow(CatVariant.ALL_BLACK)); this.setPersistenceRequired(); } diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java index 81dab77f525ae667614f940c4ff5ec308a9579a2..5fdb9ab7c6c0ae6bb948a44c5578c4304e8d6c3f 100644 --- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java +++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java @@ -337,9 +337,9 @@ public class Turtle extends Animal { @Override public void thunderHit(ServerLevel world, LightningBolt lightning) { - org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = lightning; // CraftBukkit + org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(lightning); // CraftBukkit // Folia - region threading this.hurt(this.damageSources().lightningBolt(), Float.MAX_VALUE); - org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = null; // CraftBukkit + org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Folia - region threading } private static class TurtleMoveControl extends MoveControl { diff --git a/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java b/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java index 30aec9dff249ae629b22318e52902361a9fa4099..b452b566693558fa714eca2db08ca775e73d48a0 100644 --- a/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java +++ b/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java @@ -294,8 +294,10 @@ public class ItemFrame extends HangingEntity { MapItemSavedData worldmap = MapItem.getSavedData(i, this.level); if (worldmap != null) { + synchronized (worldmap) { // Folia - make map data thread-safe worldmap.removedFromFrame(this.pos, this.getId()); worldmap.setDirty(true); + } // Folia - make map data thread-safe } }); diff --git a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java index ce2c3c146ef64400e00084bd2245d2b87a67fbc2..c8d125955754e27da54d95fb5b1cea39ca54b618 100644 --- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java +++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java @@ -293,9 +293,9 @@ public class FallingBlockEntity extends Entity { float f2 = (float) Math.min(Mth.floor((float) i * this.fallDamagePerDistance), this.fallDamageMax); this.level.getEntities((Entity) this, this.getBoundingBox(), predicate).forEach((entity) -> { - CraftEventFactory.entityDamage = this; // CraftBukkit + CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // Folia - region threading entity.hurt(damagesource1, f2); - CraftEventFactory.entityDamage = null; // CraftBukkit + CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Folia - region threading }); boolean flag = this.blockState.is(BlockTags.ANVIL); diff --git a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java index d47b3ac633e7936d30abfda6fc46c2c7412d76fe..4245973677f1e2a3a75dce74c3e010330d0a9a41 100644 --- a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java +++ b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java @@ -51,7 +51,7 @@ public class ItemEntity extends Entity implements TraceableEntity { @Nullable public UUID target; public final float bobOffs; - private int lastTick = MinecraftServer.currentTick - 1; // CraftBukkit + //private int lastTick = MinecraftServer.currentTick - 1; // CraftBukkit // Folia - region threading public boolean canMobPickup = true; // Paper private int despawnRate = -1; // Paper public net.kyori.adventure.util.TriState frictionState = net.kyori.adventure.util.TriState.NOT_SET; // Paper @@ -125,13 +125,11 @@ public class ItemEntity extends Entity implements TraceableEntity { this.discard(); } else { super.tick(); - // CraftBukkit start - Use wall time for pickup and despawn timers - int elapsedTicks = MinecraftServer.currentTick - this.lastTick; - if (this.pickupDelay != 32767) this.pickupDelay -= elapsedTicks; - this.pickupDelay = Math.max(0, this.pickupDelay); // Paper - don't go below 0 - if (this.age != -32768) this.age += elapsedTicks; - this.lastTick = MinecraftServer.currentTick; - // CraftBukkit end + // Folia start - region threading - restore original timers + if (this.pickupDelay > 0 && this.pickupDelay != 32767) { + --this.pickupDelay; + } + // Folia end - region threading - restore original timers this.xo = this.getX(); this.yo = this.getY(); @@ -185,11 +183,11 @@ public class ItemEntity extends Entity implements TraceableEntity { this.mergeWithNeighbours(); } - /* CraftBukkit start - moved up + // Folia - region threading - restore original timers if (this.age != -32768) { ++this.age; } - // CraftBukkit end */ + // Folia - region threading - restore original timers this.hasImpulse |= this.updateInWaterStateAndDoFluidPushing(); if (!this.level.isClientSide) { @@ -216,13 +214,14 @@ public class ItemEntity extends Entity implements TraceableEntity { // Spigot start - copied from above @Override public void inactiveTick() { - // CraftBukkit start - Use wall time for pickup and despawn timers - int elapsedTicks = MinecraftServer.currentTick - this.lastTick; - if (this.pickupDelay != 32767) this.pickupDelay -= elapsedTicks; - this.pickupDelay = Math.max(0, this.pickupDelay); // Paper - don't go below 0 - if (this.age != -32768) this.age += elapsedTicks; - this.lastTick = MinecraftServer.currentTick; - // CraftBukkit end + // Folia start - region threading - restore original timers + if (this.pickupDelay > 0 && this.pickupDelay != 32767) { + --this.pickupDelay; + } + if (this.age != -32768) { + ++this.age; + } + // Folia end - region threading - restore original timers if (!this.level.isClientSide && this.age >= this.despawnRate) { // Spigot // Paper // CraftBukkit start - fire ItemDespawnEvent @@ -520,14 +519,20 @@ public class ItemEntity extends Entity implements TraceableEntity { return false; } + // Folia start - region threading + @Override + public void postChangeDimension() { + super.postChangeDimension(); + this.mergeWithNeighbours(); + } + // Folia end - region threading + @Nullable @Override public Entity changeDimension(ServerLevel destination) { Entity entity = super.changeDimension(destination); - if (!this.level.isClientSide && entity instanceof ItemEntity) { - ((ItemEntity) entity).mergeWithNeighbours(); - } + if (entity != null) entity.postChangeDimension(); // Folia - region threading - move to post change return entity; } diff --git a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java index dfdf7e7fc1070975ec18fd215c724f4fc84d3705..52150e27294fcd51ae23ea3ff06673fb1ca3d113 100644 --- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java @@ -60,7 +60,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { @Override public void tick() { - if (level.spigotConfig.maxTntTicksPerTick > 0 && ++level.spigotConfig.currentPrimedTnt > level.spigotConfig.maxTntTicksPerTick) { return; } // Spigot + if (level.spigotConfig.maxTntTicksPerTick > 0 && ++level.getCurrentWorldData().currentPrimedTnt > level.spigotConfig.maxTntTicksPerTick) { return; } // Spigot // Folia - region threading if (!this.isNoGravity()) { this.setDeltaMovement(this.getDeltaMovement().add(0.0D, -0.04D, 0.0D)); } @@ -102,7 +102,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { */ // Send position and velocity updates to nearby players on every tick while the TNT is in water. // This does pretty well at keeping their clients in sync with the server. - net.minecraft.server.level.ChunkMap.TrackedEntity ete = ((net.minecraft.server.level.ServerLevel)this.level).getChunkSource().chunkMap.entityMap.get(this.getId()); + net.minecraft.server.level.ChunkMap.TrackedEntity ete = this.tracker; // Folia - region threading if (ete != null) { net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket velocityPacket = new net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket(this); net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket positionPacket = new net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket(this); diff --git a/src/main/java/net/minecraft/world/entity/monster/Zombie.java b/src/main/java/net/minecraft/world/entity/monster/Zombie.java index 9976205537cfe228735687f1e9c52c74ac025690..f286abfff186b657db99f28d3592465ccee4498a 100644 --- a/src/main/java/net/minecraft/world/entity/monster/Zombie.java +++ b/src/main/java/net/minecraft/world/entity/monster/Zombie.java @@ -94,7 +94,7 @@ public class Zombie extends Monster { private boolean canBreakDoors; private int inWaterTime; public int conversionTime; - private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field + // private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field // Folia - region threading - restore original timers private boolean shouldBurnInDay = true; // Paper public Zombie(EntityType type, Level world) { @@ -217,10 +217,7 @@ public class Zombie extends Monster { public void tick() { if (!this.level.isClientSide && this.isAlive() && !this.isNoAi()) { if (this.isUnderWaterConverting()) { - // CraftBukkit start - Use wall time instead of ticks for conversion - int elapsedTicks = MinecraftServer.currentTick - this.lastTick; - this.conversionTime -= elapsedTicks; - // CraftBukkit end + --this.conversionTime; // Folia - region threading - restore original timers if (this.conversionTime < 0) { this.doUnderWaterConversion(); } @@ -237,7 +234,7 @@ public class Zombie extends Monster { } super.tick(); - this.lastTick = MinecraftServer.currentTick; // CraftBukkit + //this.lastTick = MinecraftServer.currentTick; // CraftBukkit // Folia - region threading - restore original timers } @Override @@ -276,7 +273,7 @@ public class Zombie extends Monster { } // Paper end public void startUnderWaterConversion(int ticksUntilWaterConversion) { - this.lastTick = MinecraftServer.currentTick; // CraftBukkit + // Folia - region threading - restore original timers this.conversionTime = ticksUntilWaterConversion; this.getEntityData().set(Zombie.DATA_DROWNED_CONVERSION_ID, true); } diff --git a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java index 71a36cf9b976443cca9ab63cd0eb23253f638562..c7a03304d8fb33e2e5c90547cba46665b51eec79 100644 --- a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java +++ b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java @@ -70,7 +70,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { @Nullable private CompoundTag tradeOffers; private int villagerXp; - private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field + // private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field // Folia - region threading - restore original timers public ZombieVillager(EntityType type, Level world) { super(type, world); @@ -145,10 +145,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { public void tick() { if (!this.level.isClientSide && this.isAlive() && this.isConverting()) { int i = this.getConversionProgress(); - // CraftBukkit start - Use wall time instead of ticks for villager conversion - int elapsedTicks = MinecraftServer.currentTick - this.lastTick; - i *= elapsedTicks; - // CraftBukkit end + // Folia - region threading - restore original timers this.villagerConversionTime -= i; if (this.villagerConversionTime <= 0) { @@ -157,7 +154,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { } super.tick(); - this.lastTick = MinecraftServer.currentTick; // CraftBukkit + // Folia - region threading - restore original timers } @Override diff --git a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java index ca96b893e22de3ae7c11d5cded51edf70bdcb6f2..6000e891620850aac303630bf6676085d41f65b5 100644 --- a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java +++ b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java @@ -213,10 +213,18 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa this.readInventoryFromTag(nbt); } + // Folia start - region threading + @Override + public void preChangeDimension() { + super.preChangeDimension(); + this.stopTrading(); + } + // Folia end - region threading + @Nullable @Override public Entity changeDimension(ServerLevel destination) { - this.stopTrading(); + this.preChangeDimension(); // Folia - region threading - move into preChangeDimension return super.changeDimension(destination); } diff --git a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java index 5f407535298a31a34cfe114dd863fd6a9b977707..cb0f75fb32836efa50f0a86dfae7907b0c88780f 100644 --- a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java +++ b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java @@ -21,17 +21,18 @@ import net.minecraft.world.phys.AABB; public class CatSpawner implements CustomSpawner { private static final int TICK_DELAY = 1200; - private int nextTick; + //private int nextTick; // Folia - region threading @Override public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) { if (spawnAnimals && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { - --this.nextTick; - if (this.nextTick > 0) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + --worldData.catSpawnerNextTick; // Folia - region threading + if (worldData.catSpawnerNextTick > 0) { // Folia - region threading return 0; } else { - this.nextTick = 1200; - Player player = world.getRandomPlayer(); + worldData.catSpawnerNextTick = 1200; // Folia - region threading + Player player = world.getRandomLocalPlayer(); // Folia - region threading if (player == null) { return 0; } else { diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java index 6023b9eb3001e1a98ab8b970d853c4e7c7603f4d..0d16b7ea1e2c7d430b09721d50ea199aaa58bc89 100644 --- a/src/main/java/net/minecraft/world/entity/npc/Villager.java +++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java @@ -201,7 +201,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler brain.setCoreActivities(ImmutableSet.of(Activity.CORE)); brain.setDefaultActivity(Activity.IDLE); brain.setActiveActivityIfPossible(Activity.IDLE); - brain.updateActivityFromSchedule(this.level.getDayTime(), this.level.getGameTime()); + brain.updateActivityFromSchedule(this.level.getLevelData().getDayTime(), this.level.getLevelData().getGameTime()); // Folia - region threading } @Override @@ -722,6 +722,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler ServerLevel worldserver = minecraftserver.getLevel(globalpos.dimension()); if (worldserver != null) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( // Folia - region threading + worldserver, globalpos.pos().getX() >> 4, globalpos.pos().getZ() >> 4, () -> { // Folia - region threading PoiManager villageplace = worldserver.getPoiManager(); Optional> optional = villageplace.getType(globalpos.pos()); BiPredicate> bipredicate = (BiPredicate) Villager.POI_MEMORIES.get(pos); @@ -730,6 +732,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler villageplace.release(globalpos.pos()); DebugPackets.sendPoiTicketCountPacket(worldserver, globalpos.pos()); } + }); // Folia - region threading } }); diff --git a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java index 5d199fe497bd852827d3d18fb7566a09e70331a3..db6139c04ce9a1bc17e4305f5644c0e1ff9bf02e 100644 --- a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java +++ b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java @@ -32,16 +32,14 @@ public class WanderingTraderSpawner implements CustomSpawner { private static final int SPAWN_CHANCE_INCREASE = 25; private static final int SPAWN_ONE_IN_X_CHANCE = 10; private static final int NUMBER_OF_SPAWN_ATTEMPTS = 10; - private final RandomSource random = RandomSource.create(); + private final RandomSource random = new net.minecraft.world.entity.Entity.RandomRandomSource(); // Folia - region threading private final ServerLevelData serverLevelData; - private int tickDelay; - private int spawnDelay; - private int spawnChance; + // Folia - region threading public WanderingTraderSpawner(ServerLevelData properties) { this.serverLevelData = properties; // Paper start - this.tickDelay = Integer.MIN_VALUE; + //this.tickDelay = Integer.MIN_VALUE; // Folia - region threading - moved to regionisedworlddata //this.spawnDelay = properties.getWanderingTraderSpawnDelay(); // Paper - This value is read from the world file only for the first spawn, after which vanilla uses a hardcoded value //this.spawnChance = properties.getWanderingTraderSpawnChance(); // Paper - This value is read from the world file only for the first spawn, after which vanilla uses a hardcoded value //if (this.spawnDelay == 0 && this.spawnChance == 0) { @@ -56,36 +54,37 @@ public class WanderingTraderSpawner implements CustomSpawner { @Override public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading // Paper start - if (this.tickDelay == Integer.MIN_VALUE) { - this.tickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; - this.spawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; - this.spawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; + if (worldData.wanderingTraderTickDelay == Integer.MIN_VALUE) { // Folia - region threading + worldData.wanderingTraderTickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading + worldData.wanderingTraderSpawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading + worldData.wanderingTraderSpawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading } if (!world.getGameRules().getBoolean(GameRules.RULE_DO_TRADER_SPAWNING)) { return 0; - } else if (this.tickDelay - 1 > 0) { - this.tickDelay = this.tickDelay - 1; + } else if (worldData.wanderingTraderTickDelay - 1 > 0) { // Folia - region threading + worldData.wanderingTraderTickDelay = worldData.wanderingTraderTickDelay - 1; // Folia - region threading return 0; } else { - this.tickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; - this.spawnDelay = this.spawnDelay - world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; + worldData.wanderingTraderTickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading + worldData.wanderingTraderSpawnDelay = worldData.wanderingTraderSpawnDelay - world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading //this.serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways - if (this.spawnDelay > 0) { + if (worldData.wanderingTraderSpawnDelay > 0) { // Folia - region threading return 0; } else { - this.spawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; + worldData.wanderingTraderSpawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading if (!world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { return 0; } else { - int i = this.spawnChance; + int i = worldData.wanderingTraderSpawnChance; // Folia - region threading // this.serverLevelData.setWanderingTraderSpawnChance(this.spawnChance); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways - this.spawnChance = Mth.clamp(i + world.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); + worldData.wanderingTraderSpawnChance = Mth.clamp(i + world.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); // Folia - region threading if (this.random.nextInt(100) > i) { return 0; } else if (this.spawn(world)) { - this.spawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; + worldData.wanderingTraderSpawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading // Paper end return 1; } else { @@ -97,7 +96,7 @@ public class WanderingTraderSpawner implements CustomSpawner { } private boolean spawn(ServerLevel world) { - ServerPlayer entityplayer = world.getRandomPlayer(); + ServerPlayer entityplayer = world.getRandomLocalPlayer(); // Folia - region threading if (entityplayer == null) { return true; @@ -127,7 +126,7 @@ public class WanderingTraderSpawner implements CustomSpawner { this.tryToSpawnLlamaFor(world, entityvillagertrader, 4); } - this.serverLevelData.setWanderingTraderId(entityvillagertrader.getUUID()); + //this.serverLevelData.setWanderingTraderId(entityvillagertrader.getUUID()); // Folia - region threading - doesn't appear to be used anywhere, so avoid the race condition here... // entityvillagertrader.setDespawnDelay(48000); // CraftBukkit - moved to EntityVillagerTrader constructor. This lets the value be modified by plugins on CreatureSpawnEvent entityvillagertrader.setWanderTarget(blockposition1); entityvillagertrader.restrictTo(blockposition1, 16); diff --git a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java index 5d6d26cfe8f0ab68a3145214b3fc126ca7a71a66..c8be8b54859e39967ede5c53ce940da39a68563e 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java +++ b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java @@ -149,6 +149,11 @@ public abstract class AbstractArrow extends Projectile { @Override public void tick() { super.tick(); + // Folia start - region threading - make sure entities do not move into regions they do not own + if (!io.papermc.paper.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.getLevel(), this.position(), this.getDeltaMovement(), 1)) { + return; + } + // Folia end - region threading - make sure entities do not move into regions they do not own boolean flag = this.isNoPhysics(); Vec3 vec3d = this.getDeltaMovement(); diff --git a/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java b/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java index 2096e8a0bdbcfc865f175f3a01ab688542481531..8e072c1ae653258722fce7b50cd27076988d9666 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java +++ b/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java @@ -77,6 +77,11 @@ public abstract class AbstractHurtingProjectile extends Projectile { this.discard(); } else { super.tick(); + // Folia start - region threading - make sure entities do not move into regions they do not own + if (!io.papermc.paper.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.getLevel(), this.position(), this.getDeltaMovement(), 1)) { + return; + } + // Folia end - region threading - make sure entities do not move into regions they do not own if (this.shouldBurn()) { this.setSecondsOnFire(1); } diff --git a/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java b/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java index d8a2b5e0e0ff640bcc827f1b1310bd19b9154cc2..2a4542a9bcb990e276796fb042d9f780cc7b63dd 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java +++ b/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java @@ -129,9 +129,9 @@ public class EvokerFangs extends Entity implements TraceableEntity { if (target.isAlive() && !target.isInvulnerable() && target != entityliving1) { if (entityliving1 == null) { - org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = this; // CraftBukkit + org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // Folia - region threading target.hurt(this.damageSources().magic(), 6.0F); - org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = null; // CraftBukkit + org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Folia - region threading } else { if (entityliving1.isAlliedTo((Entity) target)) { return; diff --git a/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java b/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java index fca27f98989bf106060ba08196255fe32f850df5..98c9eda4056093d8d230ac56ac407c39556ba2cb 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java +++ b/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java @@ -129,6 +129,11 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier { }); } + // Folia start - region threading + if (this.attachedToEntity != null && !io.papermc.paper.util.TickThread.isTickThreadFor(this.attachedToEntity)) { + this.attachedToEntity = null; + } + // Folia end - region threading if (this.attachedToEntity != null) { if (this.attachedToEntity.isFallFlying()) { @@ -240,9 +245,9 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier { if (f > 0.0F) { if (this.attachedToEntity != null) { - CraftEventFactory.entityDamage = this; // CraftBukkit + CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // Folia - region threading this.attachedToEntity.hurt(this.damageSources().fireworks(this, this.getOwner()), 5.0F + (float) (nbttaglist.size() * 2)); - CraftEventFactory.entityDamage = null; // CraftBukkit + CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Folia - region threading } double d0 = 5.0D; @@ -269,9 +274,9 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier { if (flag) { float f1 = f * (float) Math.sqrt((5.0D - (double) this.distanceTo(entityliving)) / 5.0D); - CraftEventFactory.entityDamage = this; // CraftBukkit + CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // Folia - region threading entityliving.hurt(this.damageSources().fireworks(this, this.getOwner()), f1); - CraftEventFactory.entityDamage = null; // CraftBukkit + CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Folia - region threading } } } diff --git a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java index d77410588a1c10d8ac902f21a8bd7e35f74fecd2..675a01ec82b31140594beace0f0b651be096f90e 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java +++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java @@ -98,7 +98,7 @@ public class FishingHook extends Projectile { public FishingHook(net.minecraft.world.entity.player.Player thrower, Level world, int luckOfTheSeaLevel, int lureLevel) { this(EntityType.FISHING_BOBBER, world, luckOfTheSeaLevel, lureLevel); - this.setOwner(thrower); + //this.setOwner(thrower); // Folia - region threading - move this down after position so that thread-checks do not fail float f = thrower.getXRot(); float f1 = thrower.getYRot(); float f2 = Mth.cos(-f1 * 0.017453292F - 3.1415927F); @@ -110,6 +110,7 @@ public class FishingHook extends Projectile { double d2 = thrower.getZ() - (double) f2 * 0.3D; this.moveTo(d0, d1, d2, f1, f); + this.setOwner(thrower); // Folia - region threading - move this down after position so that thread-checks do not fail Vec3 vec3d = new Vec3((double) (-f3), (double) Mth.clamp(-(f5 / f4), -5.0F, 5.0F), (double) (-f2)); double d3 = vec3d.length(); diff --git a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java index c4f4a26e016eea744f587461af80461074d48303..42f6481eb92a3aa91010c235f08ea6581e36573e 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java +++ b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java @@ -30,6 +30,11 @@ public class LlamaSpit extends Projectile { public void tick() { super.tick(); Vec3 vec3d = this.getDeltaMovement(); + // Folia start - region threading - make sure entities do not move into regions they do not own + if (!io.papermc.paper.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.getLevel(), this.position(), this.getDeltaMovement(), 1)) { + return; + } + // Folia end - region threading - make sure entities do not move into regions they do not own HitResult movingobjectposition = ProjectileUtil.getHitResult(this, this::canHitEntity); this.preOnHit(movingobjectposition); // CraftBukkit - projectile hit event diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java index 8b2a3a8482018b7db7de81bc295862f783e17ce5..ad8cff8bc7f12a48e2b5c9bbc30b33676d61853b 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java @@ -53,9 +53,20 @@ public abstract class Projectile extends Entity implements TraceableEntity { } + // Folia start - region threading + // In general, this is an entire mess. At the time of writing, there are fifty usages of getOwner. + // Usage of this function is to avoid concurrency issues, even if it sacrifices behavior. @Nullable @Override public Entity getOwner() { + Entity ret = this.getOwnerRaw(); + return io.papermc.paper.util.TickThread.isTickThreadFor(ret) ? ret : null; + } + // Folia end - region threading + + @Nullable + public Entity getOwnerRaw() { // Folia - region threading + io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot update owner state asynchronously"); // Folia - region threading if (this.cachedOwner != null && !this.cachedOwner.isRemoved()) { return this.cachedOwner; } else if (this.ownerUUID != null && this.level instanceof ServerLevel) { @@ -277,6 +288,6 @@ public abstract class Projectile extends Entity implements TraceableEntity { public boolean mayInteract(Level world, BlockPos pos) { Entity entity = this.getOwner(); - return entity instanceof Player ? entity.mayInteract(world, pos) : entity == null || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); + return entity instanceof Player && io.papermc.paper.util.TickThread.isTickThreadFor(entity) ? entity.mayInteract(world, pos) : entity == null || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Folia - region threading } } diff --git a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java index b9e4955fecabbad8d6762f3d933ea1402e932d9b..e026f2517a0714bba0f51aa2bf087c1babb55990 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java +++ b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java @@ -23,7 +23,7 @@ public class SmallFireball extends Fireball { public SmallFireball(Level world, LivingEntity owner, double velocityX, double velocityY, double velocityZ) { super(EntityType.SMALL_FIREBALL, owner, velocityX, velocityY, velocityZ, world); // CraftBukkit start - if (this.getOwner() != null && this.getOwner() instanceof Mob) { + if (owner != null && owner instanceof Mob) { // Folia - region threading isIncendiary = this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); } // CraftBukkit end diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java b/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java index 88181c59e604ba3b132b9e695cef5eaf5b836029..0146b150b6cb70a7272e8d9781e100a012f93d9b 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java +++ b/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java @@ -44,6 +44,11 @@ public abstract class ThrowableProjectile extends Projectile { @Override public void tick() { super.tick(); + // Folia start - region threading - make sure entities do not move into regions they do not own + if (!io.papermc.paper.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.getLevel(), this.position(), this.getDeltaMovement(), 1)) { + return; + } + // Folia end - region threading - make sure entities do not move into regions they do not own HitResult movingobjectposition = ProjectileUtil.getHitResult(this, this::canHitEntity); boolean flag = false; diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java index 39ab9a283d856ba8d578d1378285758e32a24cf0..2b35495df87614c3578727598ebf1b4469829154 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java @@ -43,6 +43,62 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { entityHitResult.getEntity().hurt(this.damageSources().thrown(this, this.getOwner()), 0.0F); } + // Folia start - region threading + private static void attemptTeleport(Entity source, ServerLevel checkWorld, net.minecraft.world.phys.Vec3 to) { + // ignore retired callback, in those cases we do not want to teleport + source.getBukkitEntity().taskScheduler.schedule( + (Entity entity) -> { + // source is now an invalid reference, do not use it, use the entity parameter + + if (entity.getLevel() != checkWorld) { + // cannot teleport cross-world + return; + } + if (entity.isVehicle()) { + // cannot teleport vehicles + return; + } + // dismount from any vehicles, so we can teleport and to prevent desync + if (entity.isPassenger()) { + entity.stopRiding(); + } + + // reset fall damage so that if the entity was falling they do not instantly die + entity.resetFallDistance(); + + entity.teleportAsync( + checkWorld, to, null, null, null, + PlayerTeleportEvent.TeleportCause.ENDER_PEARL, + // chunk could have been unloaded + Entity.TELEPORT_FLAG_LOAD_CHUNK, + (Entity teleported) -> { + // entity is now an invalid reference, do not use it, instead use teleported + if (teleported instanceof ServerPlayer player) { + // connection teleport is already done + ServerLevel world = player.getLevel(); + + // endermite spawn chance + if (world.random.nextFloat() < 0.05F && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { + Endermite entityendermite = (Endermite) EntityType.ENDERMITE.create(world); + + if (entityendermite != null) { + entityendermite.moveTo(entity.getX(), entity.getY(), entity.getZ(), entity.getYRot(), entity.getXRot()); + world.addFreshEntity(entityendermite, CreatureSpawnEvent.SpawnReason.ENDER_PEARL); + } + } + + // damage player + player.hurt(player.damageSources().fall(), 5.0F); + } + } + ); + }, + null, + 1L + ); + } + // Folia end - region threading + @Override protected void onHit(HitResult hitResult) { super.onHit(hitResult); @@ -52,6 +108,20 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { } if (!this.level.isClientSide && !this.isRemoved()) { + // Folia start - region threading + if (true) { + // we can't fire events, because we do not actually know where the other entity is located + if (!io.papermc.paper.util.TickThread.isTickThreadFor(this)) { + throw new IllegalStateException("Must be on tick thread for ticking entity: " + this); + } + Entity entity = this.getOwnerRaw(); + if (entity != null) { + attemptTeleport(entity, (ServerLevel)this.getLevel(), this.position()); + } + this.discard(); + return; + } + // Folia end - region threading Entity entity = this.getOwner(); if (entity instanceof ServerPlayer) { @@ -83,9 +153,9 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { entityplayer.connection.teleport(teleEvent.getTo()); entity.resetFallDistance(); - CraftEventFactory.entityDamage = this; + CraftEventFactory.entityDamageRT.set(this); // Folia - region threading entity.hurt(this.damageSources().fall(), 5.0F); - CraftEventFactory.entityDamage = null; + CraftEventFactory.entityDamageRT.set(null); // Folia - region threading } // CraftBukkit end } @@ -111,6 +181,14 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { } + // Folia start - region threading + @Override + public void preChangeDimension() { + super.preChangeDimension(); + // Don't change the owner here, since the tick logic will consider it anyways. + } + // Folia end - region threading + @Nullable @Override public Entity changeDimension(ServerLevel destination) { diff --git a/src/main/java/net/minecraft/world/entity/raid/Raid.java b/src/main/java/net/minecraft/world/entity/raid/Raid.java index 879c3bb661e24b9682b654def57c2800f4f8ca92..359f1690497eac00899eb26c17308e0a6fe943ad 100644 --- a/src/main/java/net/minecraft/world/entity/raid/Raid.java +++ b/src/main/java/net/minecraft/world/entity/raid/Raid.java @@ -108,6 +108,13 @@ public class Raid { private int celebrationTicks; private Optional waveSpawnPos; + // Folia start - make raids thread-safe + public boolean ownsRaid() { + BlockPos center = this.getCenter(); + return center != null && io.papermc.paper.util.TickThread.isTickThreadFor(this.level, center.getX() >> 4, center.getZ() >> 4, 8); + } + // Folia end - make raids thread-safe + public Raid(int id, ServerLevel world, BlockPos pos) { this.raidEvent = new ServerBossEvent(Raid.RAID_NAME_COMPONENT, BossEvent.BossBarColor.RED, BossEvent.BossBarOverlay.NOTCHED_10); this.random = RandomSource.create(); @@ -213,7 +220,7 @@ public class Raid { return (entityplayer) -> { BlockPos blockposition = entityplayer.blockPosition(); - return entityplayer.isAlive() && this.level.getRaidAt(blockposition) == this; + return io.papermc.paper.util.TickThread.isTickThreadFor(entityplayer) && entityplayer.isAlive() && this.level.getRaidAt(blockposition) == this; // Folia - make raids thread-safe }; } @@ -527,7 +534,7 @@ public class Raid { boolean flag = true; Collection collection = this.raidEvent.getPlayers(); long i = this.random.nextLong(); - Iterator iterator = this.level.players().iterator(); + Iterator iterator = this.level.getLocalPlayers().iterator(); // Folia - region threading while (iterator.hasNext()) { ServerPlayer entityplayer = (ServerPlayer) iterator.next(); diff --git a/src/main/java/net/minecraft/world/entity/raid/Raider.java b/src/main/java/net/minecraft/world/entity/raid/Raider.java index 99e9d46d42ddd0b451c6aeb847f1b295ebe5c697..9bc6b5a0b476e019e719709e68314d8e84d55a59 100644 --- a/src/main/java/net/minecraft/world/entity/raid/Raider.java +++ b/src/main/java/net/minecraft/world/entity/raid/Raider.java @@ -91,7 +91,7 @@ public abstract class Raider extends PatrollingMonster { if (this.canJoinRaid()) { if (raid == null) { - if (this.level.getGameTime() % 20L == 0L) { + if (this.level.getRedstoneGameTime() % 20L == 0L) { // Folia - region threading Raid raid1 = ((ServerLevel) this.level).getRaidAt(this.blockPosition()); if (raid1 != null && Raids.canJoinRaid(this, raid1)) { diff --git a/src/main/java/net/minecraft/world/entity/raid/Raids.java b/src/main/java/net/minecraft/world/entity/raid/Raids.java index fabce3bc592b1b172b227395a07febdbb66ec3c9..ebdbadc1dba41f47ba4795ea43020b80102f5ccb 100644 --- a/src/main/java/net/minecraft/world/entity/raid/Raids.java +++ b/src/main/java/net/minecraft/world/entity/raid/Raids.java @@ -28,14 +28,14 @@ import net.minecraft.world.phys.Vec3; public class Raids extends SavedData { private static final String RAID_FILE_ID = "raids"; - public final Map raidMap = Maps.newHashMap(); + public final Map raidMap = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - make raids thread-safe private final ServerLevel level; - private int nextAvailableID; + private final java.util.concurrent.atomic.AtomicInteger nextAvailableID = new java.util.concurrent.atomic.AtomicInteger(); // Folia - make raids thread-safe private int tick; public Raids(ServerLevel world) { this.level = world; - this.nextAvailableID = 1; + this.nextAvailableID.set(1); // Folia - make raids thread-safe this.setDirty(); } @@ -43,12 +43,26 @@ public class Raids extends SavedData { return (Raid) this.raidMap.get(id); } - public void tick() { + // Folia start - make raids thread-safe + public void globalTick() { ++this.tick; + if (this.tick % 200 == 0) { + this.setDirty(); + } + } + // Folia end - make raids thread-safe + + public void tick() { + // Folia - make raids thread-safe - move to globalTick() Iterator iterator = this.raidMap.values().iterator(); while (iterator.hasNext()) { Raid raid = (Raid) iterator.next(); + // Folia start - make raids thread-safe + if (!raid.ownsRaid()) { + continue; + } + // Folia end - make raids thread-safe if (this.level.getGameRules().getBoolean(GameRules.RULE_DISABLE_RAIDS)) { raid.stop(); @@ -62,14 +76,17 @@ public class Raids extends SavedData { } } - if (this.tick % 200 == 0) { - this.setDirty(); - } + // Folia - make raids thread-safe - move to globalTick() DebugPackets.sendRaids(this.level, this.raidMap.values()); } public static boolean canJoinRaid(Raider raider, Raid raid) { + // Folia start - make raids thread-safe + if (!raid.ownsRaid()) { + return false; + } + // Folia end - make raids thread-safe return raider != null && raid != null && raid.getLevel() != null ? raider.isAlive() && raider.canJoinRaid() && raider.getNoActionTime() <= 2400 && raider.level.dimensionType() == raid.getLevel().dimensionType() : false; } @@ -82,7 +99,7 @@ public class Raids extends SavedData { } else { DimensionType dimensionmanager = player.level.dimensionType(); - if (!dimensionmanager.hasRaids()) { + if (!dimensionmanager.hasRaids() || !io.papermc.paper.util.TickThread.isTickThreadFor(this.level, player.chunkPosition().x, player.chunkPosition().z, 8)) { // Folia - region threading return null; } else { BlockPos blockposition = player.blockPosition(); @@ -162,7 +179,7 @@ public class Raids extends SavedData { public static Raids load(ServerLevel world, CompoundTag nbt) { Raids persistentraid = new Raids(world); - persistentraid.nextAvailableID = nbt.getInt("NextAvailableID"); + persistentraid.nextAvailableID.set(nbt.getInt("NextAvailableID")); // Folia - make raids thread-safe persistentraid.tick = nbt.getInt("Tick"); ListTag nbttaglist = nbt.getList("Raids", 10); @@ -178,7 +195,7 @@ public class Raids extends SavedData { @Override public CompoundTag save(CompoundTag nbt) { - nbt.putInt("NextAvailableID", this.nextAvailableID); + nbt.putInt("NextAvailableID", this.nextAvailableID.get()); // Folia - make raids thread-safe nbt.putInt("Tick", this.tick); ListTag nbttaglist = new ListTag(); Iterator iterator = this.raidMap.values().iterator(); @@ -200,7 +217,7 @@ public class Raids extends SavedData { } private int getUniqueId() { - return ++this.nextAvailableID; + return this.nextAvailableID.incrementAndGet(); // Folia - make raids thread-safe } @Nullable @@ -211,6 +228,11 @@ public class Raids extends SavedData { while (iterator.hasNext()) { Raid raid1 = (Raid) iterator.next(); + // Folia start - make raids thread-safe + if (!raid1.ownsRaid()) { + continue; + } + // Folia end - make raids thread-safe double d1 = raid1.getCenter().distSqr(pos); if (raid1.isActive() && d1 < d0) { diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java index 1fe630118981b02092054af3c5c6227a889ee3c2..57ecc5e506c895fe701cb24c9b503d6009602a26 100644 --- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java +++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java @@ -145,5 +145,11 @@ public class MinecartCommandBlock extends AbstractMinecart { return (org.bukkit.craftbukkit.entity.CraftMinecartCommand) MinecartCommandBlock.this.getBukkitEntity(); } // CraftBukkit end + // Folia start + @Override + public void threadCheck() { + io.papermc.paper.util.TickThread.ensureTickThread(MinecartCommandBlock.this, "Asynchronous sendSystemMessage to a command block"); + } + // Folia end } } diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java index 1b8f22805af87dc08e0dea9fd93a5f93c0b05107..00bc99948ddd67bb85c3797f869064540a8b1213 100644 --- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java +++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java @@ -128,7 +128,7 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper // Paper start public void immunize() { - this.activatedImmunityTick = Math.max(this.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 20); + this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); } // Paper end diff --git a/src/main/java/net/minecraft/world/item/ArmorItem.java b/src/main/java/net/minecraft/world/item/ArmorItem.java index d7a0cbde8f8c99276307502674c71463fbe7e89c..2a501b3fa8d69f627b279fd035fd2cb11d590f06 100644 --- a/src/main/java/net/minecraft/world/item/ArmorItem.java +++ b/src/main/java/net/minecraft/world/item/ArmorItem.java @@ -68,7 +68,7 @@ public class ArmorItem extends Item implements Equipable { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityliving.getBukkitEntity()); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading world.getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java index d690b8d0c1da1f56b226376df8c76c34375e3c73..6e6f73d50725057bc713bc6ae3a31b77ee97d415 100644 --- a/src/main/java/net/minecraft/world/item/ItemStack.java +++ b/src/main/java/net/minecraft/world/item/ItemStack.java @@ -334,6 +334,7 @@ public final class ItemStack { } public InteractionResult useOn(UseOnContext itemactioncontext, InteractionHand enumhand) { // CraftBukkit - add hand + net.minecraft.world.entity.player.Player entityhuman = itemactioncontext.getPlayer(); BlockPos blockposition = itemactioncontext.getClickedPos(); BlockInWorld shapedetectorblock = new BlockInWorld(itemactioncontext.getLevel(), blockposition, false); @@ -345,12 +346,13 @@ public final class ItemStack { CompoundTag oldData = this.getTagClone(); int oldCount = this.getCount(); ServerLevel world = (ServerLevel) itemactioncontext.getLevel(); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading if (!(this.getItem() instanceof BucketItem/* || this.getItem() instanceof SolidBucketItem*/)) { // if not bucket // Paper - capture block states for snow buckets - world.captureBlockStates = true; + worldData.captureBlockStates = true; // Folia - region threading // special case bonemeal if (this.getItem() == Items.BONE_MEAL) { - world.captureTreeGeneration = true; + worldData.captureTreeGeneration = true; // Folia - region threading } } Item item = this.getItem(); @@ -359,14 +361,14 @@ public final class ItemStack { int newCount = this.getCount(); this.setCount(oldCount); this.setTagClone(oldData); - world.captureBlockStates = false; - if (enuminteractionresult.consumesAction() && world.captureTreeGeneration && world.capturedBlockStates.size() > 0) { - world.captureTreeGeneration = false; + worldData.captureBlockStates = false; // Folia - region threading + if (enuminteractionresult.consumesAction() && worldData.captureTreeGeneration && worldData.capturedBlockStates.size() > 0) { // Folia - region threading + world.getCurrentWorldData().captureTreeGeneration = false; // Folia - region threading Location location = new Location(world.getWorld(), blockposition.getX(), blockposition.getY(), blockposition.getZ()); - TreeType treeType = SaplingBlock.treeType; - SaplingBlock.treeType = null; - List blocks = new java.util.ArrayList<>(world.capturedBlockStates.values()); - world.capturedBlockStates.clear(); + TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading + SaplingBlock.treeTypeRT.set(null); // Folia - region threading + List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading StructureGrowEvent structureEvent = null; if (treeType != null) { boolean isBonemeal = this.getItem() == Items.BONE_MEAL; @@ -394,12 +396,12 @@ public final class ItemStack { SignItem.openSign = null; // SPIGOT-6758 - Reset on early return return enuminteractionresult; } - world.captureTreeGeneration = false; + worldData.captureTreeGeneration = false; // Folia - region threading if (entityhuman != null && enuminteractionresult.shouldAwardStats()) { org.bukkit.event.block.BlockPlaceEvent placeEvent = null; - List blocks = new java.util.ArrayList<>(world.capturedBlockStates.values()); - world.capturedBlockStates.clear(); + List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading if (blocks.size() > 1) { placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockMultiPlaceEvent(world, entityhuman, enumhand, blocks, blockposition.getX(), blockposition.getY(), blockposition.getZ()); } else if (blocks.size() == 1 && item != Items.POWDER_SNOW_BUCKET) { // Paper - don't call event twice for snow buckets @@ -410,13 +412,13 @@ public final class ItemStack { enuminteractionresult = InteractionResult.FAIL; // cancel placement // PAIL: Remove this when MC-99075 fixed placeEvent.getPlayer().updateInventory(); - world.capturedTileEntities.clear(); // Paper - clear out tile entities as chests and such will pop loot + worldData.capturedTileEntities.clear(); // Paper - clear out tile entities as chests and such will pop loot // Folia - region threading // revert back all captured blocks - world.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 + worldData.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 // Folia - region threading for (BlockState blockstate : blocks) { blockstate.update(true, false); } - world.preventPoiUpdated = false; + worldData.preventPoiUpdated = false; // Folia - region threading // Brute force all possible updates BlockPos placedPos = ((CraftBlock) placeEvent.getBlock()).getPosition(); @@ -431,7 +433,7 @@ public final class ItemStack { this.setCount(newCount); } - for (Map.Entry e : world.capturedTileEntities.entrySet()) { + for (Map.Entry e : worldData.capturedTileEntities.entrySet()) { // Folia - region threading world.setBlockEntity(e.getValue()); } @@ -505,8 +507,8 @@ public final class ItemStack { entityhuman.awardStat(Stats.ITEM_USED.get(item)); } } - world.capturedTileEntities.clear(); - world.capturedBlockStates.clear(); + worldData.capturedTileEntities.clear(); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading // CraftBukkit end return enuminteractionresult; diff --git a/src/main/java/net/minecraft/world/item/MapItem.java b/src/main/java/net/minecraft/world/item/MapItem.java index 586852e347cfeb6e52d16e51b3f193e814036e81..eacd5b6f649a59f845cc8d9d9373d41ed3757c97 100644 --- a/src/main/java/net/minecraft/world/item/MapItem.java +++ b/src/main/java/net/minecraft/world/item/MapItem.java @@ -103,6 +103,7 @@ public class MapItem extends ComplexItem { } public void update(Level world, Entity entity, MapItemSavedData state) { + synchronized (state) { // Folia - make map data thread-safe if (world.dimension() == state.dimension && entity instanceof Player) { int i = 1 << state.scale; int j = state.centerX; @@ -134,9 +135,9 @@ public class MapItem extends ComplexItem { int j2 = (j / i + k1 - 64) * i; int k2 = (k / i + l1 - 64) * i; Multiset multiset = LinkedHashMultiset.create(); - LevelChunk chunk = world.getChunkIfLoaded(SectionPos.blockToSectionCoord(j2), SectionPos.blockToSectionCoord(k2)); // Paper - Maps shouldn't load chunks + LevelChunk chunk = world.getChunkIfLoaded(SectionPos.blockToSectionCoord(j2), SectionPos.blockToSectionCoord(k2)); // Paper - Maps shouldn't load chunks // Folia - super important this remains true - if (chunk != null && !chunk.isEmpty()) { // Paper - Maps shouldn't load chunks + if (chunk != null && !chunk.isEmpty() && io.papermc.paper.util.TickThread.isTickThreadFor((ServerLevel)world, chunk.getPos())) { // Paper - Maps shouldn't load chunks // Folia - make sure chunk is owned int l2 = 0; double d1 = 0.0D; int i3; @@ -227,6 +228,7 @@ public class MapItem extends ComplexItem { } } + } // Folia - make map data thread-safe } private BlockState getCorrectStateForFluidBlock(Level world, BlockState state, BlockPos pos) { @@ -243,6 +245,7 @@ public class MapItem extends ComplexItem { MapItemSavedData worldmap = MapItem.getSavedData(map, world); if (worldmap != null) { + synchronized (worldmap) { // Folia - make map data thread-safe if (world.dimension() == worldmap.dimension) { int i = 1 << worldmap.scale; int j = worldmap.centerX; @@ -317,6 +320,7 @@ public class MapItem extends ComplexItem { } } + } // Folia - make map data thread-safe } } @@ -326,6 +330,7 @@ public class MapItem extends ComplexItem { MapItemSavedData worldmap = MapItem.getSavedData(stack, world); if (worldmap != null) { + synchronized (worldmap) { // Folia - region threading if (entity instanceof Player) { Player entityhuman = (Player) entity; @@ -335,6 +340,7 @@ public class MapItem extends ComplexItem { if (!worldmap.locked && (selected || entity instanceof Player && ((Player) entity).getOffhandItem() == stack)) { this.update(world, entity, worldmap); } + } // Folia - region threading } } diff --git a/src/main/java/net/minecraft/world/item/MinecartItem.java b/src/main/java/net/minecraft/world/item/MinecartItem.java index c6d2f764efa9b8bec730bbe757d480e365b25ccc..af9313a3b3aaa0af4f2a2f4fb2424dc3e9140d9c 100644 --- a/src/main/java/net/minecraft/world/item/MinecartItem.java +++ b/src/main/java/net/minecraft/world/item/MinecartItem.java @@ -67,7 +67,7 @@ public class MinecartItem extends Item { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseEvent(block2, craftItem.clone(), new org.bukkit.util.Vector(d0, d1 + d3, d2)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/world/level/BaseCommandBlock.java b/src/main/java/net/minecraft/world/level/BaseCommandBlock.java index 888936385196a178ab8b730fd5e4fff4a5466428..a49621dcc5915c86bf0327a4903706817c7b8ff4 100644 --- a/src/main/java/net/minecraft/world/level/BaseCommandBlock.java +++ b/src/main/java/net/minecraft/world/level/BaseCommandBlock.java @@ -20,7 +20,7 @@ import net.minecraft.world.phys.Vec3; public abstract class BaseCommandBlock implements CommandSource { - private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss"); + private static final ThreadLocal TIME_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("HH:mm:ss")); // Folia - region threading - SDF is not thread-safe private static final Component DEFAULT_NAME = Component.literal("@"); private long lastExecution = -1L; private boolean updateLastExecution = true; @@ -111,6 +111,7 @@ public abstract class BaseCommandBlock implements CommandSource { } public boolean performCommand(Level world) { + if (true) return false; // Folia - region threading if (!world.isClientSide && world.getGameTime() != this.lastExecution) { if ("Searge".equalsIgnoreCase(this.command)) { this.lastOutput = Component.literal("#itzlipofutzli"); @@ -169,11 +170,13 @@ public abstract class BaseCommandBlock implements CommandSource { } + public void threadCheck() {} // Folia + @Override public void sendSystemMessage(Component message) { if (this.trackOutput) { - org.spigotmc.AsyncCatcher.catchOp("sendSystemMessage to a command block"); // Paper - SimpleDateFormat simpledateformat = BaseCommandBlock.TIME_FORMAT; + this.threadCheck(); // Folia + SimpleDateFormat simpledateformat = BaseCommandBlock.TIME_FORMAT.get(); // Folia - region threading - SDF is not thread-safe Date date = new Date(); this.lastOutput = Component.literal("[" + simpledateformat.format(date) + "] ").append(message); diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java index 3b959f42d958bf0f426853aee56753d6c455fcdb..b1a6a66ed02706c1adc36dcedfa415f5a24a25a0 100644 --- a/src/main/java/net/minecraft/world/level/EntityGetter.java +++ b/src/main/java/net/minecraft/world/level/EntityGetter.java @@ -38,6 +38,12 @@ public interface EntityGetter { return this.getEntities(EntityTypeTest.forClass(entityClass), box, predicate); } + // Folia start - region threading + default List getLocalPlayers() { + return java.util.Collections.emptyList(); + } + // Folia end - region threading + List players(); default List getEntities(@Nullable Entity except, AABB box) { @@ -92,7 +98,7 @@ public interface EntityGetter { double d = -1.0D; Player player = null; - for(Player player2 : this.players()) { + for(Player player2 : this.getLocalPlayers()) { // Folia - region threading if (targetPredicate == null || targetPredicate.test(player2)) { double e = player2.distanceToSqr(x, y, z); if ((maxDistance < 0.0D || e < maxDistance * maxDistance) && (d == -1.0D || e < d)) { @@ -113,7 +119,7 @@ public interface EntityGetter { default List findNearbyBukkitPlayers(double x, double y, double z, double radius, @Nullable Predicate predicate) { com.google.common.collect.ImmutableList.Builder builder = com.google.common.collect.ImmutableList.builder(); - for (Player human : this.players()) { + for (Player human : this.getLocalPlayers()) { // Folia - region threading if (predicate == null || predicate.test(human)) { double distanceSquared = human.distanceToSqr(x, y, z); @@ -140,7 +146,7 @@ public interface EntityGetter { // Paper start default boolean hasNearbyAlivePlayerThatAffectsSpawning(double x, double y, double z, double range) { - for (Player player : this.players()) { + for (Player player : this.getLocalPlayers()) { // Folia - region threading if (EntitySelector.PLAYER_AFFECTS_SPAWNING.test(player)) { // combines NO_SPECTATORS and LIVING_ENTITY_STILL_ALIVE with an "affects spawning" check double distanceSqr = player.distanceToSqr(x, y, z); if (range < 0.0D || distanceSqr < range * range) { @@ -153,7 +159,7 @@ public interface EntityGetter { // Paper end default boolean hasNearbyAlivePlayer(double x, double y, double z, double range) { - for(Player player : this.players()) { + for(Player player : this.getLocalPlayers()) { // Folia - region threading if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) { double d = player.distanceToSqr(x, y, z); if (range < 0.0D || d < range * range) { @@ -167,17 +173,17 @@ public interface EntityGetter { @Nullable default Player getNearestPlayer(TargetingConditions targetPredicate, LivingEntity entity) { - return this.getNearestEntity(this.players(), targetPredicate, entity, entity.getX(), entity.getY(), entity.getZ()); + return this.getNearestEntity(this.getLocalPlayers(), targetPredicate, entity, entity.getX(), entity.getY(), entity.getZ()); // Folia - region threading } @Nullable default Player getNearestPlayer(TargetingConditions targetPredicate, LivingEntity entity, double x, double y, double z) { - return this.getNearestEntity(this.players(), targetPredicate, entity, x, y, z); + return this.getNearestEntity(this.getLocalPlayers(), targetPredicate, entity, x, y, z); // Folia - region threading } @Nullable default Player getNearestPlayer(TargetingConditions targetPredicate, double x, double y, double z) { - return this.getNearestEntity(this.players(), targetPredicate, (LivingEntity)null, x, y, z); + return this.getNearestEntity(this.getLocalPlayers(), targetPredicate, (LivingEntity)null, x, y, z); // Folia - region threading } @Nullable @@ -208,7 +214,7 @@ public interface EntityGetter { default List getNearbyPlayers(TargetingConditions targetPredicate, LivingEntity entity, AABB box) { List list = Lists.newArrayList(); - for(Player player : this.players()) { + for(Player player : this.getLocalPlayers()) { // Folia - region threading if (box.contains(player.getX(), player.getY(), player.getZ()) && targetPredicate.test(entity, player)) { list.add(player); } @@ -234,8 +240,7 @@ public interface EntityGetter { @Nullable default Player getPlayerByUUID(UUID uuid) { - for(int i = 0; i < this.players().size(); ++i) { - Player player = this.players().get(i); + for(Player player : this.getLocalPlayers()) { // Folia - region threading if (uuid.equals(player.getUUID())) { return player; } diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java index 59837144c2c0460aca6e8c349eb3d6528111d1dc..7f32d5d5b709e8bb0395ccbeada2322c6e89cea3 100644 --- a/src/main/java/net/minecraft/world/level/Explosion.java +++ b/src/main/java/net/minecraft/world/level/Explosion.java @@ -246,7 +246,7 @@ public class Explosion { continue; } - CraftEventFactory.entityDamage = this.source; + CraftEventFactory.entityDamageRT.set(this.source); // Folia - region threading entity.lastDamageCancelled = false; if (entity instanceof EnderDragon) { @@ -262,7 +262,7 @@ public class Explosion { entity.hurt(this.getDamageSource(), (float) ((int) ((d13 * d13 + d13) / 2.0D * 7.0D * (double) f2 + 1.0D))); } - CraftEventFactory.entityDamage = null; + CraftEventFactory.entityDamageRT.set(null); // Folia - region threading if (entity.lastDamageCancelled) { // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Skip entity if damage event was cancelled continue; } @@ -515,17 +515,10 @@ public class Explosion { } // Paper start - Optimize explosions private float getBlockDensity(Vec3 vec3d, Entity entity) { - if (!this.level.paperConfig().environment.optimizeExplosions) { + if (true || !this.level.paperConfig().environment.optimizeExplosions) { // Folia - region threading return getSeenPercent(vec3d, entity); } - CacheKey key = new CacheKey(this, entity.getBoundingBox()); - Float blockDensity = this.level.explosionDensityCache.get(key); - if (blockDensity == null) { - blockDensity = getSeenPercent(vec3d, entity); - this.level.explosionDensityCache.put(key, blockDensity); - } - - return blockDensity; + return 0.0f; // Folia - region threading } static class CacheKey { diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java index 944da18bcc993ab0488a34cbbe9df134c355301a..32b9358bacabedf4513bb17c68200ef84a95a91b 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -118,10 +118,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public static final int TICKS_PER_DAY = 24000; public static final int MAX_ENTITY_SPAWN_Y = 20000000; public static final int MIN_ENTITY_SPAWN_Y = -20000000; - protected final List blockEntityTickers = Lists.newArrayList(); public final int getTotalTileEntityTickers() { return this.blockEntityTickers.size(); } // Paper - protected final NeighborUpdater neighborUpdater; - private final List pendingBlockEntityTickers = Lists.newArrayList(); - private boolean tickingBlockEntities; + //protected final List blockEntityTickers = Lists.newArrayList(); public final int getTotalTileEntityTickers() { return this.blockEntityTickers.size(); } // Paper // Folia - region threading + public final int neighbourUpdateMax; //protected final NeighborUpdater neighborUpdater; + //private final List pendingBlockEntityTickers = Lists.newArrayList(); // Folia - region threading + //private boolean tickingBlockEntities; // Folia - region threading public final Thread thread; private final boolean isDebug; private int skyDarken; @@ -131,7 +131,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public float rainLevel; protected float oThunderLevel; public float thunderLevel; - public final RandomSource random = RandomSource.create(); + public final RandomSource random = new Entity.RandomRandomSource(); // Folia - region threading /** @deprecated */ @Deprecated private final RandomSource threadSafeRandom = RandomSource.createThreadSafe(); @@ -145,7 +145,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { private final ResourceKey dimension; private final RegistryAccess registryAccess; private final DamageSources damageSources; - private long subTickCount; + private final java.util.concurrent.atomic.AtomicLong subTickCount = new java.util.concurrent.atomic.AtomicLong(); //private long subTickCount; // Folia - region threading // CraftBukkit start Added the following private final CraftWorld world; @@ -154,20 +154,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public org.bukkit.generator.ChunkGenerator generator; public static final boolean DEBUG_ENTITIES = Boolean.getBoolean("debug.entities"); // Paper - public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710 - public boolean captureBlockStates = false; - public boolean captureTreeGeneration = false; - public Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper - public Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - public List captureDrops; + // Folia - region threading - moved to regionised data public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>(); - // Paper start - public int wakeupInactiveRemainingAnimals; - public int wakeupInactiveRemainingFlying; - public int wakeupInactiveRemainingMonsters; - public int wakeupInactiveRemainingVillagers; - // Paper end - public boolean populating; + // Folia - region threading - moved to regionised data + // Folia - region threading public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot // Paper start private final io.papermc.paper.configuration.WorldConfiguration paperConfig; @@ -181,9 +171,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public static BlockPos lastPhysicsProblem; // Spigot private org.spigotmc.TickLimiter entityLimiter; private org.spigotmc.TickLimiter tileLimiter; - private int tileTickPosition; - public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions - public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Move from Map in BlockRedstoneTorch to here + //private int tileTickPosition; // Folia - region threading + //public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions // Folia - region threading + //public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Move from Map in BlockRedstoneTorch to here // Folia - region threading // Paper start - fix and optimise world upgrading // copied from below @@ -227,7 +217,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { List ret = new java.util.ArrayList<>(); double maxRangeSquared = maxRange * maxRange; - for (net.minecraft.server.level.ServerPlayer player : (List)this.players()) { + for (net.minecraft.server.level.ServerPlayer player : (List)this.getLocalPlayers()) { // Folia - region threading if ((maxRange < 0.0 || player.distanceToSqr(sourceX, sourceY, sourceZ) < maxRangeSquared)) { if (predicate == null || predicate.test(player)) { ret.add(player); @@ -243,7 +233,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { net.minecraft.server.level.ServerPlayer closest = null; double closestRangeSquared = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange; - for (net.minecraft.server.level.ServerPlayer player : (List)this.players()) { + for (net.minecraft.server.level.ServerPlayer player : (List)this.getLocalPlayers()) { // Folia - region threading double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ); if (distanceSquared < closestRangeSquared && (predicate == null || predicate.test(player))) { closest = player; @@ -274,6 +264,33 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public abstract ResourceKey getTypeKey(); + // Folia start - region ticking + public final io.papermc.paper.threadedregions.RegionizedData worldRegionData + = new io.papermc.paper.threadedregions.RegionizedData<>( + (ServerLevel)this, () -> new io.papermc.paper.threadedregions.RegionizedWorldData((ServerLevel)Level.this), + io.papermc.paper.threadedregions.RegionizedWorldData.REGION_CALLBACK + ); + public volatile io.papermc.paper.threadedregions.RegionizedServer.WorldLevelData tickData; + public final java.util.concurrent.ConcurrentHashMap.KeySetView needsChangeBroadcasting = java.util.concurrent.ConcurrentHashMap.newKeySet(); + + public io.papermc.paper.threadedregions.RegionizedWorldData getCurrentWorldData() { + final io.papermc.paper.threadedregions.RegionizedWorldData ret = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); + if (ret == null) { + return ret; + } + Level world = ret.world; + if (world != this) { + throw new IllegalStateException("World mismatch: expected " + this.getWorld().getName() + " but got " + world.getWorld().getName()); + } + return ret; + } + + @Override + public List getLocalPlayers() { + return this.getCurrentWorldData().getLocalPlayers(); + } + // Folia end - region ticking + protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, RegistryAccess iregistrycustom, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - Async-Anti-Xray - Pass executor this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper @@ -317,7 +334,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { this.thread = Thread.currentThread(); this.biomeManager = new BiomeManager(this, i); this.isDebug = flag1; - this.neighborUpdater = new CollectingNeighborUpdater(this, j); + this.neighbourUpdateMax = j; // Folia - region threading this.registryAccess = iregistrycustom; this.damageSources = new DamageSources(iregistrycustom); // CraftBukkit start @@ -458,8 +475,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { @Nullable public final BlockState getBlockStateIfLoaded(BlockPos pos) { // CraftBukkit start - tree generation - if (this.captureTreeGeneration) { - CraftBlockState previous = this.capturedBlockStates.get(pos); + if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading + CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Folia - region threading if (previous != null) { return previous.getHandle(); } @@ -521,16 +538,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable { @Override public boolean setBlock(BlockPos pos, BlockState state, int flags, int maxUpdateDepth) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); // Folia - region threading // CraftBukkit start - tree generation - if (this.captureTreeGeneration) { + if (worldData.captureTreeGeneration) { // Folia - region threading // Paper start BlockState type = getBlockState(pos); if (!type.isDestroyable()) return false; // Paper end - CraftBlockState blockstate = this.capturedBlockStates.get(pos); + CraftBlockState blockstate = worldData.capturedBlockStates.get(pos); // Folia - region threading if (blockstate == null) { blockstate = CapturedBlockState.getTreeBlockState(this, pos, flags); - this.capturedBlockStates.put(pos.immutable(), blockstate); + worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading } blockstate.setFlag(flags); // Paper - update the flag also blockstate.setData(state); @@ -547,10 +565,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { // CraftBukkit start - capture blockstates boolean captured = false; - if (this.captureBlockStates && !this.capturedBlockStates.containsKey(pos)) { + if (worldData.captureBlockStates && !worldData.capturedBlockStates.containsKey(pos)) { // Folia - region threading CraftBlockState blockstate = (CraftBlockState) world.getBlockAt(pos.getX(), pos.getY(), pos.getZ()).getState(); // Paper - use CB getState to get a suitable snapshot blockstate.setFlag(flags); // Paper - set flag - this.capturedBlockStates.put(pos.immutable(), blockstate); + worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading captured = true; } // CraftBukkit end @@ -560,8 +578,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { if (iblockdata1 == null) { // CraftBukkit start - remove blockstate if failed (or the same) - if (this.captureBlockStates && captured) { - this.capturedBlockStates.remove(pos); + if (worldData.captureBlockStates && captured) { // Folia - region threading + worldData.capturedBlockStates.remove(pos); // Folia - region threading } // CraftBukkit end return false; @@ -604,7 +622,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { */ // CraftBukkit start - if (!this.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates + if (!worldData.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates // Folia - region threading // Modularize client and physic updates // Spigot start try { @@ -653,7 +671,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { // CraftBukkit start iblockdata1.updateIndirectNeighbourShapes(this, blockposition, k, j - 1); // Don't call an event for the old block to limit event spam CraftWorld world = ((ServerLevel) this).getWorld(); - if (world != null && ((ServerLevel)this).hasPhysicsEvent) { // Paper + if (world != null && ((ServerLevel)this).getCurrentWorldData().hasPhysicsEvent) { // Paper // Folia - region threading BlockPhysicsEvent event = new BlockPhysicsEvent(world.getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()), CraftBlockData.fromData(iblockdata)); this.getCraftServer().getPluginManager().callEvent(event); @@ -667,7 +685,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { } // CraftBukkit start - SPIGOT-5710 - if (!this.preventPoiUpdated) { + if (!this.getCurrentWorldData().preventPoiUpdated) { // Folia - region threading this.onBlockStateChange(blockposition, iblockdata1, iblockdata2); } // CraftBukkit end @@ -746,7 +764,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { @Override public void neighborShapeChanged(Direction direction, BlockState neighborState, BlockPos pos, BlockPos neighborPos, int flags, int maxUpdateDepth) { - this.neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, maxUpdateDepth); + this.getCurrentWorldData().neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, maxUpdateDepth); // Folia - region threading } @Override @@ -771,11 +789,34 @@ public abstract class Level implements LevelAccessor, AutoCloseable { return this.getChunkSource().getLightEngine(); } + // Folia start - region threading + @Nullable + public BlockState getBlockStateFromEmptyChunkIfLoaded(BlockPos pos) { + net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource(); + ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4); + if (chunk != null) { + return chunk.getBlockState(pos); + } + return null; + } + + @Nullable + public BlockState getBlockStateFromEmptyChunk(BlockPos pos) { + net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource(); + ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4); + if (chunk != null) { + return chunk.getBlockState(pos); + } + chunk = chunkProvider.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.EMPTY, true); + return chunk.getBlockState(pos); + } + // Folia end - region threading + @Override public BlockState getBlockState(BlockPos pos) { // CraftBukkit start - tree generation - if (this.captureTreeGeneration) { - CraftBlockState previous = this.capturedBlockStates.get(pos); // Paper + if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading + CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Paper // Folia - region threading if (previous != null) { return previous.getHandle(); } @@ -866,7 +907,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { } public void addBlockEntityTicker(TickingBlockEntity ticker) { - (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker); + ((ServerLevel)this).getCurrentWorldData().addBlockEntityTicker(ticker); // Folia - regionised ticking } protected void tickBlockEntities() { @@ -874,11 +915,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { gameprofilerfiller.push("blockEntities"); timings.tileEntityPending.startTiming(); // Spigot - this.tickingBlockEntities = true; - if (!this.pendingBlockEntityTickers.isEmpty()) { - this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); - this.pendingBlockEntityTickers.clear(); - } + final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = ((ServerLevel)this).getCurrentWorldData(); // Folia - regionised ticking + regionizedWorldData.seTtickingBlockEntities(true); // Folia - regionised ticking + regionizedWorldData.pushPendingTickingBlockEntities(); // Folia - regionised ticking + List blockEntityTickers = regionizedWorldData.getBlockEntityTickers(); // Folia - regionised ticking timings.tileEntityPending.stopTiming(); // Spigot timings.tileEntityTick.startTiming(); // Spigot @@ -887,9 +927,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { int tilesThisCycle = 0; var toRemove = new it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet(net.minecraft.Util.identityStrategy()); // Paper - use removeAll toRemove.add(null); - for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters - this.tileTickPosition = (this.tileTickPosition < this.blockEntityTickers.size()) ? this.tileTickPosition : 0; - TickingBlockEntity tickingblockentity = (TickingBlockEntity) this.blockEntityTickers.get(tileTickPosition); + for (int i = 0; i < blockEntityTickers.size(); i++) { // Paper - Disable tick limiters // Folia - regionised ticking + TickingBlockEntity tickingblockentity = (TickingBlockEntity) blockEntityTickers.get(i); // Folia - regionised ticking // Spigot start if (tickingblockentity == null) { this.getCraftServer().getLogger().severe("Spigot has detected a null entity and has removed it, preventing a crash"); @@ -906,19 +945,19 @@ public abstract class Level implements LevelAccessor, AutoCloseable { } else if (this.shouldTickBlocksAt(tickingblockentity.getPos())) { tickingblockentity.tick(); // Paper start - execute chunk tasks during tick - if ((this.tileTickPosition & 7) == 0) { + if ((i & 7) == 0) { // Folia - regionised ticking MinecraftServer.getServer().executeMidTickTasks(); } // Paper end - execute chunk tasks during tick } } - this.blockEntityTickers.removeAll(toRemove); + blockEntityTickers.removeAll(toRemove); // Folia - regionised ticking timings.tileEntityTick.stopTiming(); // Spigot - this.tickingBlockEntities = false; - co.aikar.timings.TimingHistory.tileEntityTicks += this.blockEntityTickers.size(); // Paper + regionizedWorldData.seTtickingBlockEntities(false); // Folia - regionised ticking + //co.aikar.timings.TimingHistory.tileEntityTicks += this.blockEntityTickers.size(); // Paper // Folia - region threading gameprofilerfiller.pop(); - spigotConfig.currentPrimedTnt = 0; // Spigot + regionizedWorldData.currentPrimedTnt = 0; // Spigot // Folia - region threading } public void guardEntityTick(Consumer tickConsumer, T entity) { @@ -931,7 +970,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level.getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); MinecraftServer.LOGGER.error(msg, throwable); getCraftServer().getPluginManager().callEvent(new ServerExceptionEvent(new ServerInternalException(msg, throwable))); - entity.discard(); + if (!(entity instanceof net.minecraft.server.level.ServerPlayer)) entity.discard(); // Folia - properly disconnect players + if (entity instanceof net.minecraft.server.level.ServerPlayer player) player.connection.disconnect(net.minecraft.network.chat.Component.translatable("multiplayer.disconnect.generic"), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); // Folia - properly disconnect players // Paper end } } @@ -1014,9 +1054,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable { @Nullable public BlockEntity getBlockEntity(BlockPos blockposition, boolean validate) { + // Folia start - region threading + if (!io.papermc.paper.util.TickThread.isTickThread()) { + return null; + } + // Folia end - region threading // Paper start - Optimize capturedTileEntities lookup net.minecraft.world.level.block.entity.BlockEntity blockEntity; - if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(blockposition)) != null) { + if (!this.getCurrentWorldData().capturedTileEntities.isEmpty() && (blockEntity = this.getCurrentWorldData().capturedTileEntities.get(blockposition)) != null) { // Folia - region threading return blockEntity; } // Paper end @@ -1029,8 +1074,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { if (!this.isOutsideBuildHeight(blockposition)) { // CraftBukkit start - if (this.captureBlockStates) { - this.capturedTileEntities.put(blockposition.immutable(), blockEntity); + if (this.getCurrentWorldData().captureBlockStates) { // Folia - region threading + this.getCurrentWorldData().capturedTileEntities.put(blockposition.immutable(), blockEntity); // Folia - region threading return; } // CraftBukkit end @@ -1110,6 +1155,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { @Override public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { + io.papermc.paper.util.TickThread.ensureTickThread((ServerLevel)this, box, "Cannot getEntities asynchronously"); // Folia - region threading this.getProfiler().incrementCounter("getEntities"); List list = Lists.newArrayList(); ((ServerLevel)this).getEntityLookup().getEntities(except, box, list, predicate); // Paper - optimise this call @@ -1129,6 +1175,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { } public void getEntities(EntityTypeTest filter, AABB box, Predicate predicate, List result, int limit) { + io.papermc.paper.util.TickThread.ensureTickThread((ServerLevel)this, box, "Cannot getEntities asynchronously"); // Folia - region threading this.getProfiler().incrementCounter("getEntities"); // Paper start - optimise this call //TODO use limit @@ -1234,13 +1281,30 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public void disconnect() {} + @Override // Folia - region threading public long getGameTime() { - return this.levelData.getGameTime(); + // Dumb world gen thread calls this for some reason. So, check for null. + io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); + return worldData == null ? this.getLevelData().getGameTime() : worldData.getTickData().nonRedstoneGameTime(); } public long getDayTime() { - return this.levelData.getDayTime(); + // Dumb world gen thread calls this for some reason. So, check for null. + io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); + return worldData == null ? this.getLevelData().getDayTime() : worldData.getTickData().dayTime(); + } + + // Folia start - region threading + @Override + public long dayTime() { + return this.getDayTime(); + } + + @Override + public long getRedstoneGameTime() { + return this.getCurrentWorldData().getRedstoneGameTime(); } + // Folia end - region threading public boolean mayInteract(Player player, BlockPos pos) { return true; @@ -1442,8 +1506,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { } public final BlockPos.MutableBlockPos getRandomBlockPosition(int x, int y, int z, int l, BlockPos.MutableBlockPos out) { // Paper end - this.randValue = this.randValue * 3 + 1013904223; - int i1 = this.randValue >> 2; + int i1 = this.random.nextInt() >> 2; // Folia - region threading out.set(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); // Paper - change to setValues call return out; // Paper @@ -1474,7 +1537,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { @Override public long nextSubTickCount() { - return (long) (this.subTickCount++); + return this.subTickCount.getAndIncrement(); // Folia - region threading } @Override diff --git a/src/main/java/net/minecraft/world/level/LevelAccessor.java b/src/main/java/net/minecraft/world/level/LevelAccessor.java index 73d1adc5ddf0363966eac0c77c8dfbbb20a2b6a3..375a2b57bcb29458443c1a4e2be3c0e5f4e6019c 100644 --- a/src/main/java/net/minecraft/world/level/LevelAccessor.java +++ b/src/main/java/net/minecraft/world/level/LevelAccessor.java @@ -35,12 +35,22 @@ public interface LevelAccessor extends CommonLevelAccessor, LevelTimeAccess { LevelTickAccess getBlockTicks(); + // Folia start - region threading + default long getGameTime() { + return this.getLevelData().getGameTime(); + } + + default long getRedstoneGameTime() { + return this.getLevelData().getGameTime(); + } + // Folia end - region threading + default ScheduledTick createTick(BlockPos pos, T type, int delay, TickPriority priority) { // CraftBukkit - decompile error - return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + (long) delay, priority, this.nextSubTickCount()); + return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + (long) delay, priority, this.nextSubTickCount()); // Folia - region threading } default ScheduledTick createTick(BlockPos pos, T type, int delay) { // CraftBukkit - decompile error - return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + (long) delay, this.nextSubTickCount()); + return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + (long) delay, this.nextSubTickCount()); // Folia - region threading } default void scheduleTick(BlockPos pos, Block block, int delay, TickPriority priority) { diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java index 7fe1b8856bf916796fa6d2a984f0a07a2331e23b..07802d0a25e49519c3c9b33c217e05002cf05e31 100644 --- a/src/main/java/net/minecraft/world/level/LevelReader.java +++ b/src/main/java/net/minecraft/world/level/LevelReader.java @@ -135,6 +135,15 @@ public interface LevelReader extends BlockAndTintGetter, CollisionGetter, BiomeM return this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); } + // Folia start - region threaeding + default ChunkAccess syncLoadNonFull(int chunkX, int chunkZ, ChunkStatus status) { + if (status == null || status.isOrAfter(ChunkStatus.FULL)) { + throw new IllegalArgumentException("Status: " + status.getName()); + } + return this.getChunk(chunkX, chunkZ, status, true); + } + // Folia end - region threaeding + default ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status) { return this.getChunk(chunkX, chunkZ, status, true); } @@ -204,6 +213,25 @@ public interface LevelReader extends BlockAndTintGetter, CollisionGetter, BiomeM return maxY >= this.getMinBuildHeight() && minY < this.getMaxBuildHeight() ? this.hasChunksAt(minX, minZ, maxX, maxZ) : false; } + // Folia start - region threading + default boolean hasAndOwnsChunksAt(int minX, int minZ, int maxX, int maxZ) { + int i = SectionPos.blockToSectionCoord(minX); + int j = SectionPos.blockToSectionCoord(maxX); + int k = SectionPos.blockToSectionCoord(minZ); + int l = SectionPos.blockToSectionCoord(maxZ); + + for(int m = i; m <= j; ++m) { + for(int n = k; n <= l; ++n) { + if (!this.hasChunk(m, n) || (this instanceof net.minecraft.server.level.ServerLevel world && !io.papermc.paper.util.TickThread.isTickThreadFor(world, m, n))) { + return false; + } + } + } + + return true; + } + // Folia end - region threading + /** @deprecated */ @Deprecated default boolean hasChunksAt(int minX, int minZ, int maxX, int maxZ) { diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java index 15d266fc97eb73338f4f6fb2cfe25d6861e79810..52a7d5ef4a25bee618d9c9a784df95a2341fe015 100644 --- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java +++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java @@ -115,11 +115,7 @@ public final class NaturalSpawner { } object2intopenhashmap.addTo(enumcreaturetype, 1); - // Paper start - if (countMobs) { - chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity); - } - // Paper end + // Folia - rewrite chunk system - revert per player mob caps }); } } @@ -146,7 +142,7 @@ public final class NaturalSpawner { int limit = enumcreaturetype.getMaxInstancesPerChunk(); SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(enumcreaturetype); if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { - spawnThisTick = world.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worlddata.getGameTime() % world.ticksPerSpawnCategory.getLong(spawnCategory) == 0; + spawnThisTick = world.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && world.getRedstoneGameTime() % world.ticksPerSpawnCategory.getLong(spawnCategory) == 0; // Folia - region threading limit = world.getWorld().getSpawnLimit(spawnCategory); } @@ -154,37 +150,13 @@ public final class NaturalSpawner { continue; } - // Paper start - only allow spawns upto the limit per chunk and update count afterwards - int currEntityCount = info.mobCategoryCounts.getInt(enumcreaturetype); - int k1 = limit * info.getSpawnableChunkCount() / NaturalSpawner.MAGIC_NUMBER; - int difference = k1 - currEntityCount; - - if (world.paperConfig().entities.spawning.perPlayerMobSpawns) { - int minDiff = Integer.MAX_VALUE; - final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = world.getChunkSource().chunkMap.playerMobDistanceMap.getObjectsInRange(chunk.getPos()); - if (inRange != null) { - final Object[] backingSet = inRange.getBackingSet(); - for (int k = 0; k < backingSet.length; k++) { - if (!(backingSet[k] instanceof final net.minecraft.server.level.ServerPlayer player)) { - continue; - } - minDiff = Math.min(limit - world.getChunkSource().chunkMap.getMobCountNear(player, enumcreaturetype), minDiff); - } - } - difference = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff; - } - if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rareSpawn || !enumcreaturetype.isPersistent()) && difference > 0) { - // Paper end + if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rareSpawn || !enumcreaturetype.isPersistent()) && info.canSpawnForCategory(enumcreaturetype, chunk.getPos(), limit)) { // Folia - region threading - revert per player mob caps // CraftBukkit end Objects.requireNonNull(info); NaturalSpawner.SpawnPredicate spawnercreature_c = info::canSpawn; Objects.requireNonNull(info); - // Paper start - int spawnCount = NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn, - difference, world.paperConfig().entities.spawning.perPlayerMobSpawns ? world.getChunkSource().chunkMap::updatePlayerMobTypeMap : null); - info.mobCategoryCounts.mergeInt(enumcreaturetype, spawnCount, Integer::sum); - // Paper end + NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn); // Folia - region threading - revert per player mob caps } } diff --git a/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java b/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java index 3d377b9e461040405e0a7dcbd72d1506b48eb44e..782890e227ff9dab44dd92327979c201985f116e 100644 --- a/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java +++ b/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java @@ -7,6 +7,12 @@ public interface ServerLevelAccessor extends LevelAccessor { ServerLevel getLevel(); + // Folia start - region threading + default public StructureManager structureManager() { + throw new UnsupportedOperationException(); + } + // Folia end - region threading + default void addFreshEntityWithPassengers(Entity entity) { // CraftBukkit start this.addFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT); diff --git a/src/main/java/net/minecraft/world/level/StructureManager.java b/src/main/java/net/minecraft/world/level/StructureManager.java index 09c85ed428b8eaf51f8b3c6e45cce925f05ab354..3d797880b5964dd07f3757495ea1255a034aad89 100644 --- a/src/main/java/net/minecraft/world/level/StructureManager.java +++ b/src/main/java/net/minecraft/world/level/StructureManager.java @@ -44,11 +44,8 @@ public class StructureManager { } public List startsForStructure(ChunkPos pos, Predicate predicate) { - // Paper start - return this.startsForStructure(pos, predicate, null); - } - public List startsForStructure(ChunkPos pos, Predicate predicate, @Nullable ServerLevelAccessor levelAccessor) { - Map map = (levelAccessor == null ? this.level : levelAccessor).getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); + // Folia - region threading + Map map = this.level.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); // Paper end ImmutableList.Builder builder = ImmutableList.builder(); @@ -113,18 +110,14 @@ public class StructureManager { } public StructureStart getStructureWithPieceAt(BlockPos pos, TagKey structureTag) { - // Paper start - return this.getStructureWithPieceAt(pos, structureTag, null); - } - public StructureStart getStructureWithPieceAt(BlockPos pos, TagKey structureTag, @Nullable ServerLevelAccessor levelAccessor) { - // Paper end + // Folia - region threading Registry registry = this.registryAccess().registryOrThrow(Registries.STRUCTURE); for(StructureStart structureStart : this.startsForStructure(new ChunkPos(pos), (structure) -> { return registry.getHolder(registry.getId(structure)).map((reference) -> { return reference.is(structureTag); }).orElse(false); - }, levelAccessor)) { // Paper + })) { // Paper // Folia - region threading if (this.structureHasPieceAt(pos, structureStart)) { return structureStart; } @@ -168,7 +161,7 @@ public class StructureManager { } public void addReference(StructureStart structureStart) { - structureStart.addReference(); + //structureStart.addReference(); // Folia - region threading - move to caller this.structureCheck.incrementReference(structureStart.getChunkPos(), structureStart.getStructure()); } diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java index 4f91e4832a94c3facbc711fcae4cb5ad540a5ca0..e5ca38b375becfb3a10fb94739bdaed354c426c6 100644 --- a/src/main/java/net/minecraft/world/level/block/Block.java +++ b/src/main/java/net/minecraft/world/level/block/Block.java @@ -394,8 +394,8 @@ public class Block extends BlockBehaviour implements ItemLike { entityitem.setDefaultPickUpDelay(); // CraftBukkit start - if (world.captureDrops != null) { - world.captureDrops.add(entityitem); + if (world.getCurrentWorldData().captureDrops != null) { // Folia - region threading + world.getCurrentWorldData().captureDrops.add(entityitem); // Folia - region threading } else { world.addFreshEntity(entityitem); } diff --git a/src/main/java/net/minecraft/world/level/block/BushBlock.java b/src/main/java/net/minecraft/world/level/block/BushBlock.java index 03fde6e47c4a347c62fe9b4a3351769aedf874f6..d2e3e1d20d60f5edd0d93709b808f812c31e7491 100644 --- a/src/main/java/net/minecraft/world/level/block/BushBlock.java +++ b/src/main/java/net/minecraft/world/level/block/BushBlock.java @@ -24,7 +24,7 @@ public class BushBlock extends Block { public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { // CraftBukkit start if (!state.canSurvive(world, pos)) { - if (!(world instanceof net.minecraft.server.level.ServerLevel && ((net.minecraft.server.level.ServerLevel) world).hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper + if (!(world instanceof net.minecraft.server.level.ServerLevel && ((net.minecraft.server.level.ServerLevel) world).getCurrentWorldData().hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper // Folia - region threading return Blocks.AIR.defaultBlockState(); } } diff --git a/src/main/java/net/minecraft/world/level/block/CactusBlock.java b/src/main/java/net/minecraft/world/level/block/CactusBlock.java index 7579946ce222b6ab3685a7fd9821bcd5a4babe33..08f0360e12087cdce0671a0f3f782b62a75bac47 100644 --- a/src/main/java/net/minecraft/world/level/block/CactusBlock.java +++ b/src/main/java/net/minecraft/world/level/block/CactusBlock.java @@ -117,9 +117,9 @@ public class CactusBlock extends Block { @Override public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper - CraftEventFactory.blockDamage = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()); // CraftBukkit + CraftEventFactory.blockDamageRT.set(world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ())); // CraftBukkit // Folia - region threading entity.hurt(world.damageSources().cactus(), 1.0F); - CraftEventFactory.blockDamage = null; // CraftBukkit + CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Folia - region threading } @Override diff --git a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java index 219c87dcf065e86512f330fbeec59e55f4675083..90e56394554b2b6d5dd12fa7f1f5279dc9a38478 100644 --- a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java +++ b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java @@ -94,9 +94,9 @@ public class CampfireBlock extends BaseEntityBlock implements SimpleWaterloggedB public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper if ((Boolean) state.getValue(CampfireBlock.LIT) && entity instanceof LivingEntity && !EnchantmentHelper.hasFrostWalker((LivingEntity) entity)) { - org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = CraftBlock.at(world, pos); // CraftBukkit + org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // CraftBukkit // Folia - region threading entity.hurt(world.damageSources().inFire(), (float) this.fireDamage); - org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = null; // CraftBukkit + org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Folia - region threading } super.entityInside(state, world, pos, entity); diff --git a/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java b/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java index 81376e725151f723dad8a7b5c1a4bd597e60294e..a9876256edd5354015e83d943a83d1c0e4060d4e 100644 --- a/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java +++ b/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java @@ -113,7 +113,7 @@ public class DaylightDetectorBlock extends BaseEntityBlock { } private static void tickEntity(Level world, BlockPos pos, BlockState state, DaylightDetectorBlockEntity blockEntity) { - if (world.getGameTime() % 20L == 0L) { + if (world.getRedstoneGameTime() % 20L == 0L) { // Folia - region threading DaylightDetectorBlock.updateSignalStrength(state, world, pos); } diff --git a/src/main/java/net/minecraft/world/level/block/DispenserBlock.java b/src/main/java/net/minecraft/world/level/block/DispenserBlock.java index 8f55d0753fa26924235c943595f0d1a06a933a6f..a195d37847e3278d7721641b5db858913c0afe46 100644 --- a/src/main/java/net/minecraft/world/level/block/DispenserBlock.java +++ b/src/main/java/net/minecraft/world/level/block/DispenserBlock.java @@ -47,7 +47,7 @@ public class DispenserBlock extends BaseEntityBlock { object2objectopenhashmap.defaultReturnValue(new DefaultDispenseItemBehavior()); }); private static final int TRIGGER_DURATION = 4; - public static boolean eventFired = false; // CraftBukkit + public static ThreadLocal eventFired = ThreadLocal.withInitial(() -> Boolean.FALSE); // CraftBukkit // Folia - region threading public static void registerBehavior(ItemLike provider, DispenseItemBehavior behavior) { DispenserBlock.DISPENSER_REGISTRY.put(provider.asItem(), behavior); @@ -94,7 +94,7 @@ public class DispenserBlock extends BaseEntityBlock { if (idispensebehavior != DispenseItemBehavior.NOOP) { if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockPreDispenseEvent(world, pos, itemstack, i)) return; // Paper - BlockPreDispenseEvent is called here - DispenserBlock.eventFired = false; // CraftBukkit - reset event status + DispenserBlock.eventFired.set(false); // CraftBukkit - reset event status // Folia - region threading tileentitydispenser.setItem(i, idispensebehavior.dispense(sourceblock, itemstack)); } diff --git a/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java b/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java index 9db66b393e057d93a8025b803ae0ad2a1bca61f6..bace9a699aa6aeaec417434730a7ca6dd9983946 100644 --- a/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java +++ b/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java @@ -95,7 +95,7 @@ public class DoublePlantBlock extends BushBlock { protected static void preventCreativeDropFromBottomPart(Level world, BlockPos pos, BlockState state, Player player) { // CraftBukkit start - if (((net.minecraft.server.level.ServerLevel)world).hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper + if (((net.minecraft.server.level.ServerLevel)world).getCurrentWorldData().hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper // Folia - region threading return; } // CraftBukkit end diff --git a/src/main/java/net/minecraft/world/level/block/FungusBlock.java b/src/main/java/net/minecraft/world/level/block/FungusBlock.java index 9d0fc6b5b8d8fb31cacf7e8b346b9babf1a3e8a2..85701eba8dea71c64f92d5eaf99cb232bf2d9722 100644 --- a/src/main/java/net/minecraft/world/level/block/FungusBlock.java +++ b/src/main/java/net/minecraft/world/level/block/FungusBlock.java @@ -61,9 +61,9 @@ public class FungusBlock extends BushBlock implements BonemealableBlock { this.getFeature(world).ifPresent((holder) -> { // CraftBukkit start if (this == Blocks.WARPED_FUNGUS) { - SaplingBlock.treeType = org.bukkit.TreeType.WARPED_FUNGUS; + SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.WARPED_FUNGUS); // Folia - region threading } else if (this == Blocks.CRIMSON_FUNGUS) { - SaplingBlock.treeType = org.bukkit.TreeType.CRIMSON_FUNGUS; + SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.CRIMSON_FUNGUS); // Folia - region threading } // CraftBukkit end ((ConfiguredFeature) holder.value()).place(world, world.getChunkSource().getGenerator(), random, pos); diff --git a/src/main/java/net/minecraft/world/level/block/HoneyBlock.java b/src/main/java/net/minecraft/world/level/block/HoneyBlock.java index 29efe50dd3911a1d7ef5175034e82191130b8fd1..83810585125403a8ed1f85ff3dfc0f2d6ec40c75 100644 --- a/src/main/java/net/minecraft/world/level/block/HoneyBlock.java +++ b/src/main/java/net/minecraft/world/level/block/HoneyBlock.java @@ -80,7 +80,7 @@ public class HoneyBlock extends HalfTransparentBlock { } private void maybeDoSlideAchievement(Entity entity, BlockPos pos) { - if (entity instanceof ServerPlayer && entity.level.getGameTime() % 20L == 0L) { + if (entity instanceof ServerPlayer && entity.level.getRedstoneGameTime() % 20L == 0L) { // Folia - region threading CriteriaTriggers.HONEY_BLOCK_SLIDE.trigger((ServerPlayer)entity, entity.level.getBlockState(pos)); } diff --git a/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java b/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java index da3b301a42a93c891d083a6e02d1be8ed35adf1d..f354981843868bf938be0b5ac1ef2ce39fb067ef 100644 --- a/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java +++ b/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java @@ -112,7 +112,7 @@ public class LightningRodBlock extends RodBlock implements SimpleWaterloggedBloc @Override public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) { - if (world.isThundering() && (long) world.random.nextInt(200) <= world.getGameTime() % 200L && pos.getY() == world.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1) { + if (world.isThundering() && (long) world.random.nextInt(200) <= world.getRedstoneGameTime() % 200L && pos.getY() == world.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1) { // Folia - region threading ParticleUtils.spawnParticlesAlongAxis(((Direction) state.getValue(LightningRodBlock.FACING)).getAxis(), world, pos, 0.125D, ParticleTypes.ELECTRIC_SPARK, UniformInt.of(1, 2)); } } diff --git a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java index 12ffb5714f088f4aeafa1ad6a36f5b64a86c4c96..57eceff3fd313449c6db02c7719ad9f74ed15b17 100644 --- a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java +++ b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java @@ -28,9 +28,9 @@ public class MagmaBlock extends Block { @Override public void stepOn(Level world, BlockPos pos, BlockState state, Entity entity) { if (!entity.isSteppingCarefully() && entity instanceof LivingEntity && !EnchantmentHelper.hasFrostWalker((LivingEntity) entity)) { - org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()); // CraftBukkit + org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ())); // CraftBukkit // Folia - region threading entity.hurt(world.damageSources().hotFloor(), 1.0F); - org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = null; // CraftBukkit + org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Folia - region threading } super.stepOn(world, pos, state, entity); diff --git a/src/main/java/net/minecraft/world/level/block/MushroomBlock.java b/src/main/java/net/minecraft/world/level/block/MushroomBlock.java index f6f8e155223cba10c4073ddca602d1aa3aa872d7..bc42faf2f5cf54197849b1ad133a88510bfea09d 100644 --- a/src/main/java/net/minecraft/world/level/block/MushroomBlock.java +++ b/src/main/java/net/minecraft/world/level/block/MushroomBlock.java @@ -92,7 +92,7 @@ public class MushroomBlock extends BushBlock implements BonemealableBlock { return false; } else { world.removeBlock(pos, false); - SaplingBlock.treeType = (this == Blocks.BROWN_MUSHROOM) ? TreeType.BROWN_MUSHROOM : TreeType.RED_MUSHROOM; // CraftBukkit // Paper + SaplingBlock.treeTypeRT.set((this == Blocks.BROWN_MUSHROOM) ? TreeType.BROWN_MUSHROOM : TreeType.RED_MUSHROOM); // CraftBukkit // Paper // Folia - region threading if (((ConfiguredFeature) ((Holder) optional.get()).value()).place(world, world.getChunkSource().getGenerator(), random, pos)) { return true; } else { diff --git a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java index 6b909d41ccdf6c1ac3ac0c4e673ff52f0d14a238..e7079e164f5943f21aff487c22c525f7bf98911f 100644 --- a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java +++ b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java @@ -143,9 +143,9 @@ public class PointedDripstoneBlock extends Block implements Fallable, SimpleWate @Override public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { if (state.getValue(PointedDripstoneBlock.TIP_DIRECTION) == Direction.UP && state.getValue(PointedDripstoneBlock.THICKNESS) == DripstoneThickness.TIP) { - CraftEventFactory.blockDamage = CraftBlock.at(world, pos); // CraftBukkit + CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // CraftBukkit // Folia - region threading entity.causeFallDamage(fallDistance + 2.0F, 2.0F, world.damageSources().stalagmite()); - CraftEventFactory.blockDamage = null; // CraftBukkit + CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Folia - region threading } else { super.fallOn(world, state, pos, entity, fallDistance); } diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java index 5ea09cc455bd86beb450f0e0275d7c6c8da98084..cc019091a2decbc1e5c760129778b42ea1d449d6 100644 --- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java @@ -67,7 +67,7 @@ public class RedStoneWireBlock extends Block { }); private static final float PARTICLE_DENSITY = 0.2F; private final BlockState crossState; - public boolean shouldSignal = true; + //public boolean shouldSignal = true; // Folia - region threading - move to regionised world data public RedStoneWireBlock(BlockBehaviour.Properties settings) { super(settings); @@ -262,7 +262,7 @@ public class RedStoneWireBlock extends Block { * Note: Added 'source' argument so as to help determine direction of information flow */ private void updateSurroundingRedstone(Level worldIn, BlockPos pos, BlockState state, BlockPos source) { - if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT) { + if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT) { // Folia - region threading turbo.updateSurroundingRedstone(worldIn, pos, state, source); return; } @@ -282,11 +282,11 @@ public class RedStoneWireBlock extends Block { int i = state.getValue(POWER); int j = 0; j = this.getPower(j, worldIn.getBlockState(pos2)); - this.shouldSignal = false; + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading int k = worldIn.getBestNeighborSignal(pos1); - this.shouldSignal = true; + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading - if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { + if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { // Folia - region threading // This code is totally redundant to if statements just below the loop. if (k > 0 && k > j - 1) { j = k; @@ -300,7 +300,7 @@ public class RedStoneWireBlock extends Block { // redstone wire will be set to 'k'. If 'k' is already 15, then nothing inside the // following loop can affect the power level of the wire. Therefore, the loop is // skipped if k is already 15. - if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA || k < 15) { + if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA || k < 15) { // Folia - region threading for (Direction enumfacing : Direction.Plane.HORIZONTAL) { BlockPos blockpos = pos1.relative(enumfacing); boolean flag = blockpos.getX() != pos2.getX() || blockpos.getZ() != pos2.getZ(); @@ -319,7 +319,7 @@ public class RedStoneWireBlock extends Block { } } - if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { + if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { // Folia - region threading // The old code would decrement the wire value only by 1 at a time. if (l > j) { j = l - 1; @@ -403,10 +403,10 @@ public class RedStoneWireBlock extends Block { } private int calculateTargetStrength(Level world, BlockPos pos) { - this.shouldSignal = false; + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading int i = world.getBestNeighborSignal(pos); - this.shouldSignal = true; + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading int j = 0; if (i < 15) { @@ -455,7 +455,7 @@ public class RedStoneWireBlock extends Block { public void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { if (!oldState.is(state.getBlock()) && !world.isClientSide) { // Paper start - optimize redstone - replace call to updatePowerStrength - if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { + if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { // Folia - region threading world.getWireHandler().onWireAdded(pos); // Alternate Current } else { this.updateSurroundingRedstone(world, pos, state, null); // vanilla/Eigencraft @@ -488,7 +488,7 @@ public class RedStoneWireBlock extends Block { } // Paper start - optimize redstone - replace call to updatePowerStrength - if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { + if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { // Folia - region threading world.getWireHandler().onWireRemoved(pos, state); // Alternate Current } else { this.updateSurroundingRedstone(world, pos, state, null); // vanilla/Eigencraft @@ -529,7 +529,7 @@ public class RedStoneWireBlock extends Block { if (!world.isClientSide) { // Paper start - optimize redstone (Alternate Current) // Alternate Current handles breaking of redstone wires in the WireHandler. - if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { + if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { // Folia - region threading world.getWireHandler().onWireUpdated(pos); } else // Paper end @@ -545,12 +545,12 @@ public class RedStoneWireBlock extends Block { @Override public int getDirectSignal(BlockState state, BlockGetter world, BlockPos pos, Direction direction) { - return !this.shouldSignal ? 0 : state.getSignal(world, pos, direction); + return !io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal ? 0 : state.getSignal(world, pos, direction); // Folia - region threading } @Override public int getSignal(BlockState state, BlockGetter world, BlockPos pos, Direction direction) { - if (this.shouldSignal && direction != Direction.DOWN) { + if (io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal && direction != Direction.DOWN) { // Folia - region threading int i = (Integer) state.getValue(RedStoneWireBlock.POWER); return i == 0 ? 0 : (direction != Direction.UP && !((RedstoneSide) this.getConnectionState(world, state, pos).getValue((Property) RedStoneWireBlock.PROPERTY_BY_DIRECTION.get(direction.getOpposite()))).isConnected() ? 0 : i); @@ -577,7 +577,7 @@ public class RedStoneWireBlock extends Block { @Override public boolean isSignalSource(BlockState state) { - return this.shouldSignal; + return io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal; // Folia - region threading } public static int getColorForPower(int powerLevel) { diff --git a/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java b/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java index da07fce0cf7c9fbdb57d2c59e431b59bf583bf50..16e46bb6205c3f7444e864c553e8072f0519746d 100644 --- a/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java +++ b/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java @@ -73,10 +73,10 @@ public class RedstoneTorchBlock extends TorchBlock { public void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { boolean flag = this.hasNeighborSignal(world, pos, state); // Paper start - java.util.ArrayDeque redstoneUpdateInfos = world.redstoneUpdateInfos; + java.util.ArrayDeque redstoneUpdateInfos = world.getCurrentWorldData().redstoneUpdateInfos; // Folia - region threading if (redstoneUpdateInfos != null) { RedstoneTorchBlock.Toggle curr; - while ((curr = redstoneUpdateInfos.peek()) != null && world.getGameTime() - curr.when > 60L) { + while ((curr = redstoneUpdateInfos.peek()) != null && world.getRedstoneGameTime() - curr.when > 60L) { // Folia - region threading redstoneUpdateInfos.poll(); } } @@ -157,14 +157,14 @@ public class RedstoneTorchBlock extends TorchBlock { private static boolean isToggledTooFrequently(Level world, BlockPos pos, boolean addNew) { // Paper start - java.util.ArrayDeque list = world.redstoneUpdateInfos; + java.util.ArrayDeque list = world.getCurrentWorldData().redstoneUpdateInfos; // Folia - region threading if (list == null) { - list = world.redstoneUpdateInfos = new java.util.ArrayDeque<>(); + list = world.getCurrentWorldData().redstoneUpdateInfos = new java.util.ArrayDeque<>(); // Folia - region threading } if (addNew) { - list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), world.getGameTime())); + list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), world.getRedstoneGameTime())); // Folia - region threading } int i = 0; @@ -185,12 +185,18 @@ public class RedstoneTorchBlock extends TorchBlock { public static class Toggle { - final BlockPos pos; - final long when; + public final BlockPos pos; // Folia - region threading + long when; // Folia - region ticking public Toggle(BlockPos pos, long time) { this.pos = pos; this.when = time; } + + // Folia start - region ticking + public void offsetTime(long offset) { + this.when += offset; + } + // Folia end - region ticking } } diff --git a/src/main/java/net/minecraft/world/level/block/SaplingBlock.java b/src/main/java/net/minecraft/world/level/block/SaplingBlock.java index 901978a338f0f1b6f20ffb65aac59704bfa6f36a..4ec92ebdfd803c181cc51a71ba910ed34643a578 100644 --- a/src/main/java/net/minecraft/world/level/block/SaplingBlock.java +++ b/src/main/java/net/minecraft/world/level/block/SaplingBlock.java @@ -26,7 +26,7 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock { protected static final float AABB_OFFSET = 6.0F; protected static final VoxelShape SHAPE = Block.box(2.0D, 0.0D, 2.0D, 14.0D, 12.0D, 14.0D); private final AbstractTreeGrower treeGrower; - public static TreeType treeType; // CraftBukkit + public static final ThreadLocal treeTypeRT = new ThreadLocal<>(); // CraftBukkit // Folia - region threading protected SaplingBlock(AbstractTreeGrower generator, BlockBehaviour.Properties settings) { super(settings); @@ -52,18 +52,19 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock { world.setBlock(pos, (net.minecraft.world.level.block.state.BlockState) state.cycle(SaplingBlock.STAGE), 4); } else { // CraftBukkit start - if (world.captureTreeGeneration) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + if (worldData.captureTreeGeneration) { // Folia - region threading this.treeGrower.growTree(world, world.getChunkSource().getGenerator(), pos, state, random); } else { - world.captureTreeGeneration = true; + worldData.captureTreeGeneration = true; // Folia - region threading this.treeGrower.growTree(world, world.getChunkSource().getGenerator(), pos, state, random); - world.captureTreeGeneration = false; - if (world.capturedBlockStates.size() > 0) { - TreeType treeType = SaplingBlock.treeType; - SaplingBlock.treeType = null; + worldData.captureTreeGeneration = false; // Folia - region threading + if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading + TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading + SaplingBlock.treeTypeRT.set(null); // Folia - region threading Location location = new Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ()); - java.util.List blocks = new java.util.ArrayList<>(world.capturedBlockStates.values()); - world.capturedBlockStates.clear(); + java.util.List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading StructureGrowEvent event = null; if (treeType != null) { event = new StructureGrowEvent(location, treeType, false, null, blocks); diff --git a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java index af46c05a34292d271fd4a809398e6b299e10b12b..2eeb9352bc35a31efe98be51746014afd7e5e71c 100644 --- a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java +++ b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java @@ -51,7 +51,7 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { @Override public void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { - if (this instanceof GrassBlock && world.paperConfig().tickRates.grassSpread != 1 && (world.paperConfig().tickRates.grassSpread < 1 || (MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper + if (this instanceof GrassBlock && world.paperConfig().tickRates.grassSpread != 1 && (world.paperConfig().tickRates.grassSpread < 1 || (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + pos.hashCode()) % world.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper // Folia - regionised ticking // Paper start net.minecraft.world.level.chunk.ChunkAccess cachedBlockChunk = world.getChunkIfLoaded(pos); if (cachedBlockChunk == null) { // Is this needed? diff --git a/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java b/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java index 08a11888133b97e52535cb49cad218a1b6c6ac97..9d9564d603d5b05e60dc48d85039c384157a0dad 100644 --- a/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java +++ b/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java @@ -85,9 +85,9 @@ public class SweetBerryBushBlock extends BushBlock implements BonemealableBlock double d1 = Math.abs(entity.getZ() - entity.zOld); if (d0 >= 0.003000000026077032D || d1 >= 0.003000000026077032D) { - CraftEventFactory.blockDamage = CraftBlock.at(world, pos); // CraftBukkit + CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // CraftBukkit // Folia - region threading entity.hurt(world.damageSources().sweetBerryBush(), 1.0F); - CraftEventFactory.blockDamage = null; // CraftBukkit + CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Folia - region threading } } diff --git a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java index b91effe91dad2e1aeea0ea31140f7432833b343f..46979b4ee8c24b499577aa64167c759664148240 100644 --- a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java +++ b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java @@ -53,7 +53,7 @@ public class WitherSkullBlock extends SkullBlock { } public static void checkSpawn(Level world, BlockPos pos, SkullBlockEntity blockEntity) { - if (world.captureBlockStates) return; // CraftBukkit + if (world.getCurrentWorldData().captureBlockStates) return; // CraftBukkit // Folia - region threading if (!world.isClientSide) { BlockState iblockdata = blockEntity.getBlockState(); boolean flag = iblockdata.is(Blocks.WITHER_SKELETON_SKULL) || iblockdata.is(Blocks.WITHER_SKELETON_WALL_SKULL); diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java index ef740d1ad6352ca4af299001a081b720bc472d2e..cf41ff83b1465b89e96d029b72530cdba10f3d99 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java @@ -202,7 +202,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name } i1 = blockEntity.levels; - if (world.getGameTime() % 80L == 0L) { + if (world.getRedstoneGameTime() % 80L == 0L) { // Folia - region threading if (!blockEntity.beamSections.isEmpty()) { blockEntity.levels = BeaconBlockEntity.updateBase(world, i, j, k); } diff --git a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java index 1b248db497500aa6bd346b306dcb908af77626f3..ac1f6d5c78c1970b3242c017031679fb9a906fb0 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java @@ -26,7 +26,7 @@ import co.aikar.timings.MinecraftTimings; // Paper import co.aikar.timings.Timing; // Paper public abstract class BlockEntity { - static boolean ignoreTileUpdates; // Paper + static final ThreadLocal IGNORE_TILE_UPDATES = ThreadLocal.withInitial(() -> Boolean.FALSE); // Paper // Folia - region threading public Timing tickTimer = MinecraftTimings.getTileEntityTimings(this); // Paper // CraftBukkit start - data containers @@ -42,6 +42,12 @@ public abstract class BlockEntity { protected boolean remove; private BlockState blockState; + // Folia start - region ticking + public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) { + + } + // Folia end - region ticking + public BlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { this.type = type; this.worldPosition = pos.immutable(); @@ -163,7 +169,7 @@ public abstract class BlockEntity { public void setChanged() { if (this.level != null) { - if (ignoreTileUpdates) return; // Paper + if (IGNORE_TILE_UPDATES.get()) return; // Paper // Folia - region threading BlockEntity.setChanged(this.level, this.worldPosition, this.blockState); } diff --git a/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java index c57efcb9a79337ec791e4e8f6671612f0a82b441..526d1bfd5ad0de7bcfd0c2da902515f3dec94c54 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java @@ -56,7 +56,7 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements public int fuel; protected final ContainerData dataAccess; // CraftBukkit start - add fields and methods - private int lastTick = MinecraftServer.currentTick; + //private int lastTick = MinecraftServer.currentTick; // Folia - region ticking - restore original timers public List transaction = new java.util.ArrayList(); private int maxStack = 64; @@ -173,11 +173,10 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements ItemStack itemstack1 = (ItemStack) blockEntity.items.get(3); // CraftBukkit start - Use wall time instead of ticks for brewing - int elapsedTicks = MinecraftServer.currentTick - blockEntity.lastTick; - blockEntity.lastTick = MinecraftServer.currentTick; + // Folia - region ticking - restore original timers if (flag1) { - blockEntity.brewTime -= elapsedTicks; + --blockEntity.brewTime; // Folia - region ticking - restore original timers boolean flag2 = blockEntity.brewTime <= 0; // == -> <= // CraftBukkit end diff --git a/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java index a33d98a3ab2d00256e3c85b3de3680b4765df8d3..b311d150b0e4f1e36ba22042b02d8acd2cd5ce8c 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java @@ -56,6 +56,12 @@ public class CommandBlockEntity extends BlockEntity { return new CommandSourceStack(this, Vec3.atCenterOf(CommandBlockEntity.this.worldPosition), new Vec2(0.0F, enumdirection.toYRot()), this.getLevel(), 2, this.getName().getString(), this.getName(), this.getLevel().getServer(), (Entity) null); } + // Folia start + @Override + public void threadCheck() { + io.papermc.paper.util.TickThread.ensureTickThread((ServerLevel) CommandBlockEntity.this.level, CommandBlockEntity.this.worldPosition, "Asynchronous sendSystemMessage to a command block"); + } + // Folia end }; public CommandBlockEntity(BlockPos pos, BlockState state) { diff --git a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java index 963a596154091b79ca139af6274aa323518ad1ad..57b11cb78270a8094f772da497ad3264a0a67db1 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java @@ -87,7 +87,7 @@ public class ConduitBlockEntity extends BlockEntity { public static void clientTick(Level world, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) { ++blockEntity.tickCount; - long i = world.getGameTime(); + long i = world.getRedstoneGameTime(); // Folia - region threading List list = blockEntity.effectBlocks; if (i % 40L == 0L) { @@ -105,7 +105,7 @@ public class ConduitBlockEntity extends BlockEntity { public static void serverTick(Level world, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) { ++blockEntity.tickCount; - long i = world.getGameTime(); + long i = world.getRedstoneGameTime(); // Folia - region threading List list = blockEntity.effectBlocks; if (i % 40L == 0L) { @@ -235,11 +235,11 @@ public class ConduitBlockEntity extends BlockEntity { if (blockEntity.destroyTarget != null) { // CraftBukkit start - CraftEventFactory.blockDamage = CraftBlock.at(world, pos); + CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // Folia - region threading if (blockEntity.destroyTarget.hurt(world.damageSources().magic(), 4.0F)) { world.playSound((Player) null, blockEntity.destroyTarget.getX(), blockEntity.destroyTarget.getY(), blockEntity.destroyTarget.getZ(), SoundEvents.CONDUIT_ATTACK_TARGET, SoundSource.BLOCKS, 1.0F, 1.0F); } - CraftEventFactory.blockDamage = null; + CraftEventFactory.blockDamageRT.set(null); // Folia - region threading // CraftBukkit end } diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java index cba114f554644a37339c93026630c66c43f524b9..0134370081182260d578ee1d8a16d544c03b6a0d 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java @@ -194,12 +194,11 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen } // Paper start - Optimize Hoppers - private static boolean skipPullModeEventFire; - private static boolean skipPushModeEventFire; - public static boolean skipHopperEvents; + // Folia - region threading - moved to RegionizedWorldData private static boolean hopperPush(final Level level, final Container destination, final Direction direction, final HopperBlockEntity hopper) { - skipPushModeEventFire = skipHopperEvents; + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading + worldData.skipPushModeEventFire = worldData.skipHopperEvents; // Folia - region threading boolean foundItem = false; for (int i = 0; i < hopper.getContainerSize(); ++i) { final ItemStack item = hopper.getItem(i); @@ -214,7 +213,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen // We only need to fire the event once to give protection plugins a chance to cancel this event // Because nothing uses getItem, every event call should end up the same result. - if (!skipPushModeEventFire) { + if (!worldData.skipPushModeEventFire) { // Folia - region threading movedItem = callPushMoveEvent(destination, movedItem, hopper); if (movedItem == null) { // cancelled origItemStack.setCount(originalItemCount); @@ -244,12 +243,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen } private static boolean hopperPull(final Level level, final Hopper hopper, final Container container, ItemStack origItemStack, final int i) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading ItemStack movedItem = origItemStack; final int originalItemCount = origItemStack.getCount(); final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount); movedItem.setCount(movedItemCount); - if (!skipPullModeEventFire) { + if (!worldData.skipPullModeEventFire) { // Folia - region threading movedItem = callPullMoveEvent(hopper, container, movedItem); if (movedItem == null) { // cancelled origItemStack.setCount(originalItemCount); @@ -269,9 +269,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); } - ignoreTileUpdates = true; + IGNORE_TILE_UPDATES.set(true); // Folia - region threading container.setItem(i, origItemStack); - ignoreTileUpdates = false; + IGNORE_TILE_UPDATES.set(false); // Folia - region threading container.setChanged(); return true; } @@ -286,12 +286,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen @Nullable private static ItemStack callPushMoveEvent(Container iinventory, ItemStack itemstack, HopperBlockEntity hopper) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading final Inventory destinationInventory = getInventory(iinventory); final InventoryMoveItemEvent event = new InventoryMoveItemEvent(hopper.getOwner(false).getInventory(), CraftItemStack.asCraftMirror(itemstack), destinationInventory, true); final boolean result = event.callEvent(); if (!event.calledGetItem && !event.calledSetItem) { - skipPushModeEventFire = true; + worldData.skipPushModeEventFire = true; // Folia - region threading } if (!result) { cooldownHopper(hopper); @@ -307,6 +308,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen @Nullable private static ItemStack callPullMoveEvent(final Hopper hopper, final Container container, final ItemStack itemstack) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading final Inventory sourceInventory = getInventory(container); final Inventory destination = getInventory(hopper); @@ -314,7 +316,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen final InventoryMoveItemEvent event = new InventoryMoveItemEvent(sourceInventory, CraftItemStack.asCraftMirror(itemstack), destination, false); final boolean result = event.callEvent(); if (!event.calledGetItem && !event.calledSetItem) { - skipPullModeEventFire = true; + worldData.skipPullModeEventFire = true; // Folia - region threading } if (!result) { cooldownHopper(hopper); @@ -459,13 +461,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen } public static boolean suckInItems(Level world, Hopper hopper) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading Container iinventory = HopperBlockEntity.getSourceContainer(world, hopper); if (iinventory != null) { Direction enumdirection = Direction.DOWN; // Paper start - optimize hoppers and remove streams - skipPullModeEventFire = skipHopperEvents; + worldData.skipPullModeEventFire = worldData.skipHopperEvents; // Folia - region threading return !HopperBlockEntity.isEmptyContainer(iinventory, enumdirection) && anyMatch(iinventory, enumdirection, (item, i) -> { // Logic copied from below to avoid extra getItem calls if (!item.isEmpty() && canTakeItemFromContainer(hopper, iinventory, item, i, enumdirection)) { @@ -646,9 +649,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen stack = stack.split(to.getMaxStackSize()); } // Spigot end - ignoreTileUpdates = true; // Paper + IGNORE_TILE_UPDATES.set(true); // Paper // Folia - region threading to.setItem(slot, stack); - ignoreTileUpdates = false; // Paper + IGNORE_TILE_UPDATES.set(false); // Paper // Folia - region threading stack = leftover; // Paper flag = true; } else if (HopperBlockEntity.canMergeItems(itemstack1, stack)) { diff --git a/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java index 902f2b39104bf059849228829bfe93b6dbc757d4..9a6c51652bd2dbb3d474809372df03038e34ce75 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java @@ -86,9 +86,9 @@ public class SculkCatalystBlockEntity extends BlockEntity implements GameEventLi } public static void serverTick(Level world, BlockPos pos, BlockState state, SculkCatalystBlockEntity blockEntity) { - org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = blockEntity.getBlockPos(); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. + org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(blockEntity.getBlockPos()); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // Folia - region threading blockEntity.sculkSpreader.updateCursors(world, pos, world.getRandom(), true); - org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = null; // CraftBukkit + org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(null); // CraftBukkit // Folia - region threading } @Override diff --git a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java index c73024cc62490c336ffe26313580e88d25ca7078..ce8557aa7b208c2ea85b361b68306fcb5f420369 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java @@ -51,9 +51,12 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { public long age; private int teleportCooldown; @Nullable - public BlockPos exitPortal; + public volatile BlockPos exitPortal; // Folia - region threading - volatile public boolean exactTeleport; + private static final java.util.concurrent.atomic.AtomicLong SEARCHING_FOR_EXIT_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); // Folia - region threading + private Long searchingForExitId; // Folia - region threading + public TheEndGatewayBlockEntity(BlockPos pos, BlockState state) { super(BlockEntityType.END_GATEWAY, pos, state); } @@ -128,7 +131,7 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { } public static boolean canEntityTeleport(Entity entity) { - return EntitySelector.NO_SPECTATORS.test(entity) && !entity.getRootVehicle().isOnPortalCooldown(); + return EntitySelector.NO_SPECTATORS.test(entity) && !entity.getRootVehicle().isOnPortalCooldown() && entity.canPortalAsync(true); // Folia - region threading - correct portal check } public boolean isSpawning() { @@ -176,8 +179,112 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { } } + // Folia start - region threading + private void trySearchForExit(ServerLevel world, BlockPos fromPos) { + if (this.searchingForExitId != null) { + return; + } + this.searchingForExitId = Long.valueOf(SEARCHING_FOR_EXIT_ID_GENERATOR.getAndIncrement()); + int chunkX = fromPos.getX() >> 4; + int chunkZ = fromPos.getZ() >> 4; + world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel( + net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH, + chunkX, chunkZ, + io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL, + this.searchingForExitId + ); + + ca.spottedleaf.concurrentutil.completable.Completable complete = new ca.spottedleaf.concurrentutil.completable.Completable<>(); + + complete.addWaiter((tpLoc, throwable) -> { + // create the exit portal + TheEndGatewayBlockEntity.LOGGER.debug("Creating portal at {}", tpLoc); + TheEndGatewayBlockEntity.spawnGatewayPortal(world, tpLoc, EndGatewayConfiguration.knownExit(fromPos, false)); + + // need to go onto the tick thread to avoid saving issues + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + world, chunkX, chunkZ, + () -> { + // update the exit portal location + TheEndGatewayBlockEntity.this.exitPortal = tpLoc; + + // remove ticket keeping the gateway loaded + world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel( + net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH, + chunkX, chunkZ, + io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL, + this.searchingForExitId + ); + TheEndGatewayBlockEntity.this.searchingForExitId = null; + } + ); + }); + + findOrCreateValidTeleportPosRegionThreading(world, fromPos, complete); + } + + private static void teleportRegionThreading(Level world, BlockPos pos, BlockState state, Entity entity, TheEndGatewayBlockEntity blockEntity) { + // can we even teleport in this dimension? + if (blockEntity.exitPortal == null && world.getTypeKey() != LevelStem.END) { + return; + } + + ServerLevel serverWorld = (ServerLevel)world; + + // First, find the position we are trying to teleport to + BlockPos teleportPos = blockEntity.exitPortal; + boolean isExactTeleport = blockEntity.exactTeleport; + + if (teleportPos == null) { + blockEntity.trySearchForExit(serverWorld, pos); + return; + } + + // This needs to be first, as we are only guaranteed to be on the corresponding region tick thread here + TheEndGatewayBlockEntity.triggerCooldown(world, pos, state, blockEntity); + + if (isExactTeleport) { + // blind teleport + entity.teleportAsync( + serverWorld, Vec3.atCenterOf(teleportPos), null, null, null, + PlayerTeleportEvent.TeleportCause.END_GATEWAY, Entity.TELEPORT_FLAG_LOAD_CHUNK | Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, + (Entity teleportedEntity) -> { + for (Entity passenger : teleportedEntity.getSelfAndPassengers().toList()) { + passenger.setPortalCooldown(); + } + } + ); + } else { + // we could hack around by first loading the chunks, then calling back to here and checking if the entity + // should be teleported, something something else... + // however, we know the target location cannot differ by one region section: so we can + // just teleport and adjust the position after + entity.teleportAsync( + serverWorld, Vec3.atCenterOf(teleportPos), null, null, null, + PlayerTeleportEvent.TeleportCause.END_GATEWAY, Entity.TELEPORT_FLAG_LOAD_CHUNK | Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, + (Entity teleportedEntity) -> { + for (Entity passenger : teleportedEntity.getSelfAndPassengers().toList()) { + passenger.setPortalCooldown(); + } + + // adjust to the final exit position + Vec3 adjusted = Vec3.atCenterOf(TheEndGatewayBlockEntity.findExitPosition(serverWorld, teleportPos)); + // teleportTo will adjust rider positions + teleportedEntity.teleportTo(adjusted.x, adjusted.y, adjusted.z); + } + ); + } + } + // Folia end - region threading + public static void teleportEntity(Level world, BlockPos pos, BlockState state, Entity entity, TheEndGatewayBlockEntity blockEntity) { if (world instanceof ServerLevel && !blockEntity.isCoolingDown()) { + // Folia start - region threading + if (true) { + teleportRegionThreading(world, pos, state, entity.getRootVehicle(), blockEntity); + return; + } + // Folia end - region threading ServerLevel worldserver = (ServerLevel) world; blockEntity.teleportCooldown = 100; @@ -281,6 +388,129 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { return TheEndGatewayBlockEntity.findTallestBlock(world, blockposition1, 16, true); } + // Folia start - region threading + private static void findOrCreateValidTeleportPosRegionThreading(ServerLevel world, BlockPos pos, + ca.spottedleaf.concurrentutil.completable.Completable complete) { + ca.spottedleaf.concurrentutil.completable.Completable tentativeSelection = new ca.spottedleaf.concurrentutil.completable.Completable<>(); + + tentativeSelection.addWaiter((vec3d, throwable) -> { + LevelChunk chunk = TheEndGatewayBlockEntity.getChunk(world, vec3d); + BlockPos blockposition1 = TheEndGatewayBlockEntity.findValidSpawnInChunk(chunk); + if (blockposition1 == null) { + BlockPos blockposition2 = new BlockPos( + io.papermc.paper.util.CoordinateUtils.getBlockCoordinate(vec3d.x + 0.5D), + io.papermc.paper.util.CoordinateUtils.getBlockCoordinate(75.0D), + io.papermc.paper.util.CoordinateUtils.getBlockCoordinate(vec3d.z + 0.5D) + ); + + TheEndGatewayBlockEntity.LOGGER.debug("Failed to find a suitable block to teleport to, spawning an island on {}", blockposition2); + world.registryAccess().registry(Registries.CONFIGURED_FEATURE).flatMap((iregistry) -> { + return iregistry.getHolder(EndFeatures.END_ISLAND); + }).ifPresent((holder_c) -> { + ((ConfiguredFeature) holder_c.value()).place(world, world.getChunkSource().getGenerator(), RandomSource.create(blockposition2.asLong()), blockposition2); + }); + blockposition1 = blockposition2; + } else { + TheEndGatewayBlockEntity.LOGGER.debug("Found suitable block to teleport to: {}", blockposition1); + } + + // Here, there is no guarantee the chunks in 1 radius are in this region due to the fact that we just chained + // possibly 16x chunk loads along an axis (findExitPortalXZPosTentativeRegionThreading) using the chunk queue + // (regioniser only guarantees at least 8 chunks along a single axis) + // so, we need to schedule for the next tick + int posX = blockposition1.getX(); + int posZ = blockposition1.getZ(); + int radius = 16; + + BlockPos finalBlockPosition1 = blockposition1; + world.loadChunksAsync(blockposition1, radius, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (List chunks) -> { + // make sure chunks are kept loaded + for (net.minecraft.world.level.chunk.ChunkAccess access : chunks) { + world.chunkSource.addTicketAtLevel( + net.minecraft.server.level.TicketType.DELAYED, access.getPos(), + io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, + net.minecraft.util.Unit.INSTANCE + ); + } + // now after the chunks are loaded, we can delay by one tick + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + world, posX >> 4, posZ >> 4, () -> { + // find final location + BlockPos tpLoc = TheEndGatewayBlockEntity.findTallestBlock(world, finalBlockPosition1, radius, true); + + // done + complete.complete(tpLoc.above(10)); + } + ); + } + ); + }); + + // fire off chain + findExitPortalXZPosTentativeRegionThreading(world, pos, tentativeSelection); + } + + private static void findExitPortalXZPosTentativeRegionThreading(ServerLevel world, BlockPos pos, + ca.spottedleaf.concurrentutil.completable.Completable complete) { + Vec3 posDirFromOrigin = new Vec3(pos.getX(), 0.0D, pos.getZ()).normalize(); + Vec3 posDirExtruded = posDirFromOrigin.scale(1024.0D); + + class Vars { + int i = 16; + boolean mode = false; + Vec3 currPos = posDirExtruded; + } + Vars vars = new Vars(); + + Runnable handle = new Runnable() { + @Override + public void run() { + if (vars.mode != TheEndGatewayBlockEntity.isChunkEmpty(world, vars.currPos)) { + vars.i = 0; // fall back to completing + } + + // try to load next chunk + if (vars.i-- <= 0) { + if (vars.mode) { + complete.complete(vars.currPos); + return; + } + vars.mode = true; + vars.i = 16; + } + + vars.currPos = vars.currPos.add(posDirFromOrigin.scale(vars.mode ? 16.0 : -16.0)); + // schedule next iteration + Runnable handleButInitialised = this; + world.chunkTaskScheduler.scheduleChunkLoad( + io.papermc.paper.util.CoordinateUtils.getChunkX(vars.currPos), + io.papermc.paper.util.CoordinateUtils.getChunkZ(vars.currPos), + net.minecraft.world.level.chunk.ChunkStatus.FULL, + true, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (chunk) -> { + handleButInitialised.run(); + } + ); + } + }; + + // kick off first chunk load + world.chunkTaskScheduler.scheduleChunkLoad( + io.papermc.paper.util.CoordinateUtils.getChunkX(posDirExtruded), + io.papermc.paper.util.CoordinateUtils.getChunkZ(posDirExtruded), + net.minecraft.world.level.chunk.ChunkStatus.FULL, + true, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (chunk) -> { + handle.run(); + } + ); + } + // Folia end - region threading + private static Vec3 findExitPortalXZPosTentative(ServerLevel world, BlockPos pos) { Vec3 vec3d = (new Vec3((double) pos.getX(), 0.0D, (double) pos.getZ())).normalize(); boolean flag = true; diff --git a/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java index 28e3b73507b988f7234cbf29c4024c88180d0aef..c8facee29ee08e0975528083f89b64f0b593957f 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java @@ -10,4 +10,6 @@ public interface TickingBlockEntity { BlockPos getPos(); String getType(); + + BlockEntity getTileEntity(); // Folia - region threading } diff --git a/src/main/java/net/minecraft/world/level/block/grower/AbstractTreeGrower.java b/src/main/java/net/minecraft/world/level/block/grower/AbstractTreeGrower.java index a743f36f2682a6b72ffa6644782fc081d1479eb7..f5263a71d97f404c6f6dbd3354a5ed2e98bb71db 100644 --- a/src/main/java/net/minecraft/world/level/block/grower/AbstractTreeGrower.java +++ b/src/main/java/net/minecraft/world/level/block/grower/AbstractTreeGrower.java @@ -75,51 +75,53 @@ public abstract class AbstractTreeGrower { // CraftBukkit start protected void setTreeType(Holder> holder) { ResourceKey> worldgentreeabstract = holder.unwrapKey().get(); + TreeType treeType; // Folia - region threading if (worldgentreeabstract == TreeFeatures.OAK || worldgentreeabstract == TreeFeatures.OAK_BEES_005) { - SaplingBlock.treeType = TreeType.TREE; + treeType = TreeType.TREE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.HUGE_RED_MUSHROOM) { - SaplingBlock.treeType = TreeType.RED_MUSHROOM; + treeType = TreeType.RED_MUSHROOM; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.HUGE_BROWN_MUSHROOM) { - SaplingBlock.treeType = TreeType.BROWN_MUSHROOM; + treeType = TreeType.BROWN_MUSHROOM; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.JUNGLE_TREE) { - SaplingBlock.treeType = TreeType.COCOA_TREE; + treeType = TreeType.COCOA_TREE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.JUNGLE_TREE_NO_VINE) { - SaplingBlock.treeType = TreeType.SMALL_JUNGLE; + treeType = TreeType.SMALL_JUNGLE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.PINE) { - SaplingBlock.treeType = TreeType.TALL_REDWOOD; + treeType = TreeType.TALL_REDWOOD; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.SPRUCE) { - SaplingBlock.treeType = TreeType.REDWOOD; + treeType = TreeType.REDWOOD; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.ACACIA) { - SaplingBlock.treeType = TreeType.ACACIA; + treeType = TreeType.ACACIA; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.BIRCH || worldgentreeabstract == TreeFeatures.BIRCH_BEES_005) { - SaplingBlock.treeType = TreeType.BIRCH; + treeType = TreeType.BIRCH; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.SUPER_BIRCH_BEES_0002) { - SaplingBlock.treeType = TreeType.TALL_BIRCH; + treeType = TreeType.TALL_BIRCH; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.SWAMP_OAK) { - SaplingBlock.treeType = TreeType.SWAMP; + treeType = TreeType.SWAMP; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.FANCY_OAK || worldgentreeabstract == TreeFeatures.FANCY_OAK_BEES_005) { - SaplingBlock.treeType = TreeType.BIG_TREE; + treeType = TreeType.BIG_TREE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.JUNGLE_BUSH) { - SaplingBlock.treeType = TreeType.JUNGLE_BUSH; + treeType = TreeType.JUNGLE_BUSH; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.DARK_OAK) { - SaplingBlock.treeType = TreeType.DARK_OAK; + treeType = TreeType.DARK_OAK; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.MEGA_SPRUCE) { - SaplingBlock.treeType = TreeType.MEGA_REDWOOD; + treeType = TreeType.MEGA_REDWOOD; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.MEGA_PINE) { - SaplingBlock.treeType = TreeType.MEGA_REDWOOD; + treeType = TreeType.MEGA_REDWOOD; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.MEGA_JUNGLE_TREE) { - SaplingBlock.treeType = TreeType.JUNGLE; + treeType = TreeType.JUNGLE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.AZALEA_TREE) { - SaplingBlock.treeType = TreeType.AZALEA; + treeType = TreeType.AZALEA; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.MANGROVE) { - SaplingBlock.treeType = TreeType.MANGROVE; + treeType = TreeType.MANGROVE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.TALL_MANGROVE) { - SaplingBlock.treeType = TreeType.TALL_MANGROVE; + treeType = TreeType.TALL_MANGROVE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.CHERRY || worldgentreeabstract == TreeFeatures.CHERRY_BEES_005) { - SaplingBlock.treeType = TreeType.CHERRY; + treeType = TreeType.CHERRY; // Folia - region threading } else { throw new IllegalArgumentException("Unknown tree generator " + worldgentreeabstract); } + SaplingBlock.treeTypeRT.set(treeType); // Folia - region threading } // CraftBukkit end } diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java index 221c5d080d55326e458c1182823d6b49224ef498..0a1c876d68e834136f71f226c421699eb523de5a 100644 --- a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java @@ -144,8 +144,8 @@ public class PistonMovingBlockEntity extends BlockEntity { entity.setDeltaMovement(e, g, h); // Paper - EAR items stuck in in slime pushed by a piston - entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10); - entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10); + entity.activatedTick = Math.max(entity.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading + entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading // Paper end break; } diff --git a/src/main/java/net/minecraft/world/level/border/WorldBorder.java b/src/main/java/net/minecraft/world/level/border/WorldBorder.java index 204f008dc36212e696fba781fede88044b2f735a..1bc2b24deba7a534478184a6a3f3d41184a86734 100644 --- a/src/main/java/net/minecraft/world/level/border/WorldBorder.java +++ b/src/main/java/net/minecraft/world/level/border/WorldBorder.java @@ -33,19 +33,19 @@ public class WorldBorder { public WorldBorder() {} + // Folia - region threading - TODO make this shit thread-safe + public boolean isWithinBounds(BlockPos pos) { return (double) (pos.getX() + 1) > this.getMinX() && (double) pos.getX() < this.getMaxX() && (double) (pos.getZ() + 1) > this.getMinZ() && (double) pos.getZ() < this.getMaxZ(); } // Paper start - private final BlockPos.MutableBlockPos mutPos = new BlockPos.MutableBlockPos(); + private static final ThreadLocal mutPos = ThreadLocal.withInitial(() -> new BlockPos.MutableBlockPos()); // Folia - region threading public boolean isBlockInBounds(int chunkX, int chunkZ) { - this.mutPos.set(chunkX, 64, chunkZ); - return this.isWithinBounds(this.mutPos); + return this.isWithinBounds(mutPos.get().set(chunkX, 64, chunkZ)); // Folia - region threading } public boolean isChunkInBounds(int chunkX, int chunkZ) { - this.mutPos.set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15); - return this.isWithinBounds(this.mutPos); + return this.isWithinBounds(mutPos.get().set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15)); // Folia - region threading } // Paper end diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java index 42ecfc1237bfc724dc9134a6a8bae3670251449e..2fbfdf1fdba12417e2dbca041b9c7e29af27d02d 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java @@ -306,7 +306,7 @@ public abstract class ChunkGenerator { return Pair.of(placement.getLocatePos(pos), holder); } - ChunkAccess ichunkaccess = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); + ChunkAccess ichunkaccess = world.syncLoadNonFull(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); // Folia - region threading structurestart = structureAccessor.getStartForStructure(SectionPos.bottomOf(ichunkaccess), (Structure) holder.value(), ichunkaccess); } while (structurestart == null); @@ -317,7 +317,7 @@ public abstract class ChunkGenerator { } private static boolean tryAddReference(StructureManager structureAccessor, StructureStart start) { - if (start.canBeReferenced()) { + if (start.tryReference()) { // Folia - region threading structureAccessor.addReference(start); return true; } else { 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 9599af33c683ec47e28b1c8e4dc965d30d9081a7..f4c4951f815062c2791531a703ed8cbe9337d574 100644 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java @@ -61,6 +61,13 @@ public class LevelChunk extends ChunkAccess { @Override public void tick() {} + // Folia start - region threading + @Override + public BlockEntity getTileEntity() { + return null; + } + // Folia end - region threading + @Override public boolean isRemoved() { return true; @@ -233,51 +240,15 @@ public class LevelChunk extends ChunkAccess { } // Paper end // Paper start - optimise checkDespawn - private boolean playerGeneralAreaCacheSet; - private com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playerGeneralAreaCache; - - public com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getPlayerGeneralAreaCache() { - if (!this.playerGeneralAreaCacheSet) { - this.updateGeneralAreaCache(); - } - return this.playerGeneralAreaCache; - } - - public void updateGeneralAreaCache() { - this.updateGeneralAreaCache(((ServerLevel)this.level).getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(this.coordinateKey)); - } - - public void removeGeneralAreaCache() { - this.playerGeneralAreaCacheSet = false; - this.playerGeneralAreaCache = null; - } - - public void updateGeneralAreaCache(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet value) { - this.playerGeneralAreaCacheSet = true; - this.playerGeneralAreaCache = value; - } - + // Folia - region threading public net.minecraft.server.level.ServerPlayer findNearestPlayer(double sourceX, double sourceY, double sourceZ, double maxRange, java.util.function.Predicate predicate) { - if (!this.playerGeneralAreaCacheSet) { - this.updateGeneralAreaCache(); - } - - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby = this.playerGeneralAreaCache; - - if (nearby == null) { - return null; - } - - Object[] backingSet = nearby.getBackingSet(); + // Folia start - region threading double closestDistance = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange; net.minecraft.server.level.ServerPlayer closest = null; - for (int i = 0, len = backingSet.length; i < len; ++i) { - Object _player = backingSet[i]; - if (!(_player instanceof net.minecraft.server.level.ServerPlayer)) { - continue; - } - net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)_player; + java.util.List nearby = this.level.getLocalPlayers(); + for (int i = 0, len = nearby.size(); i < len; ++i) { + net.minecraft.server.level.ServerPlayer player = nearby.get(i); double distance = player.distanceToSqr(sourceX, sourceY, sourceZ); if (distance < closestDistance && predicate.test(player)) { @@ -285,31 +256,17 @@ public class LevelChunk extends ChunkAccess { closestDistance = distance; } } - return closest; + // Folia end - region threading } public void getNearestPlayers(double sourceX, double sourceY, double sourceZ, java.util.function.Predicate predicate, double range, java.util.List ret) { - if (!this.playerGeneralAreaCacheSet) { - this.updateGeneralAreaCache(); - } - - com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby = this.playerGeneralAreaCache; - - if (nearby == null) { - return; - } - + // Folia start - region threading double rangeSquared = range * range; - - Object[] backingSet = nearby.getBackingSet(); - for (int i = 0, len = backingSet.length; i < len; ++i) { - Object _player = backingSet[i]; - if (!(_player instanceof net.minecraft.server.level.ServerPlayer)) { - continue; - } - net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)_player; + java.util.List nearby = this.level.getLocalPlayers(); + for (int i = 0, len = nearby.size(); i < len; ++i) { + net.minecraft.server.level.ServerPlayer player = nearby.get(i); if (range >= 0.0) { double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ); @@ -322,6 +279,7 @@ public class LevelChunk extends ChunkAccess { ret.add(player); } } + // Folia end - region threading } // Paper end - optimise checkDespawn @@ -557,7 +515,7 @@ public class LevelChunk extends ChunkAccess { return null; } else { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. - if (!this.level.isClientSide && doPlace && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { + if (!this.level.isClientSide && doPlace && (!this.level.getCurrentWorldData().captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // Folia - region threading iblockdata.onPlace(this.level, blockposition, iblockdata1, flag); } @@ -604,7 +562,7 @@ public class LevelChunk extends ChunkAccess { @Nullable public BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) { // CraftBukkit start - BlockEntity tileentity = level.capturedTileEntities.get(pos); + BlockEntity tileentity = level.getCurrentWorldData().capturedTileEntities.get(pos); // Folia - region threading if (tileentity == null) { tileentity = (BlockEntity) this.blockEntities.get(pos); } @@ -889,13 +847,13 @@ public class LevelChunk extends ChunkAccess { org.bukkit.World world = this.level.getWorld(); if (world != null) { - this.level.populating = true; + this.level.getCurrentWorldData().populating = true; // Folia - region threading try { for (org.bukkit.generator.BlockPopulator populator : world.getPopulators()) { populator.populate(world, random, bukkitChunk); } } finally { - this.level.populating = false; + this.level.getCurrentWorldData().populating = false; // Folia - region threading } } server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(this.bukkitChunk)); @@ -944,7 +902,7 @@ public class LevelChunk extends ChunkAccess { @Override public boolean isUnsaved() { // Paper start - add dirty system to tick lists - long gameTime = this.level.getLevelData().getGameTime(); + long gameTime = this.level.getRedstoneGameTime(); // Folia - region threading if (this.blockTicks.isDirty(gameTime) || this.fluidTicks.isDirty(gameTime)) { return true; } @@ -1225,6 +1183,13 @@ public class LevelChunk extends ChunkAccess { this.ticker = wrapped; } + // Folia start - region threading + @Override + public BlockEntity getTileEntity() { + return this.ticker == null ? null : this.ticker.getTileEntity(); + } + // Folia end - region threading + @Override public void tick() { this.ticker.tick(); @@ -1261,6 +1226,13 @@ public class LevelChunk extends ChunkAccess { this.ticker = blockentityticker; } + // Folia start - region threading + @Override + public BlockEntity getTileEntity() { + return this.blockEntity; + } + // Folia end - region threading + @Override public void tick() { if (!this.blockEntity.isRemoved() && this.blockEntity.hasLevel()) { diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java index 9d6f4749ed72fe319754ccea28f20fa97286527d..3c9ae42290c2e0cb70bb08e97f3ea2c3fb594c7d 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java @@ -686,7 +686,7 @@ public class ChunkSerializer { } private static void saveTicks(ServerLevel world, CompoundTag nbt, ChunkAccess.TicksToSave tickSchedulers) { - long i = world.getLevelData().getGameTime(); + long i = world.getRedstoneGameTime(); // Folia - region threading nbt.put("block_ticks", tickSchedulers.blocks().save(i, (block) -> { return BuiltInRegistries.BLOCK.getKey(block).toString(); diff --git a/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java b/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java index e9eb32469a5c03f7a3677ef50fd4541c1ed662ad..88b1a06fc44980d05dabd3943caa24091ff4de98 100644 --- a/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java +++ b/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java @@ -168,6 +168,7 @@ public class EndDragonFight { if (!this.dragonEvent.getPlayers().isEmpty()) { this.level.getChunkSource().addRegionTicket(TicketType.DRAGON, new ChunkPos(0, 0), 9, Unit.INSTANCE); boolean bl = this.isArenaLoaded(); + if (!bl) { return; }// Folia - region threading - don't tick if we don't own the entire region if (this.needsStateScanning && bl) { this.scanState(); this.needsStateScanning = false; @@ -214,6 +215,12 @@ public class EndDragonFight { } List list = this.level.getDragons(); + // Folia start - region threading + // we do not want to deal with any dragons NOT nearby + list.removeIf((dragon) -> { + return !io.papermc.paper.util.TickThread.isTickThreadFor(dragon); + }); + // Folia end - region threading if (list.isEmpty()) { this.dragonKilled = true; } else { @@ -324,8 +331,8 @@ public class EndDragonFight { private boolean isArenaLoaded() { for(int i = -8; i <= 8; ++i) { for(int j = 8; j <= 8; ++j) { - ChunkAccess chunkAccess = this.level.getChunk(i, j, ChunkStatus.FULL, false); - if (!(chunkAccess instanceof LevelChunk)) { + ChunkAccess chunkAccess = this.level.getChunkIfLoaded(i, j); // Folia - region threading + if (!(chunkAccess instanceof LevelChunk) || !io.papermc.paper.util.TickThread.isTickThreadFor(this.level, i, j, this.level.regioniser.regionSectionChunkSize)) { // Folia - region threading return false; } @@ -465,7 +472,7 @@ public class EndDragonFight { } public void onCrystalDestroyed(EndCrystal enderCrystal, DamageSource source) { - if (this.respawnStage != null && this.respawnCrystals.contains(enderCrystal)) { + if (io.papermc.paper.util.TickThread.isTickThreadFor(this.level, 0, 0) && this.respawnStage != null && this.respawnCrystals.contains(enderCrystal)) { LOGGER.debug("Aborting respawn sequence"); this.respawnStage = null; this.respawnTime = 0; @@ -486,7 +493,7 @@ public class EndDragonFight { } public void tryRespawn() { - if (this.dragonKilled && this.respawnStage == null) { + if (this.dragonKilled && this.respawnStage == null && io.papermc.paper.util.TickThread.isTickThreadFor(this.level, 0, 0)) { // Folia - region threading BlockPos blockPos = this.portalLocation; if (blockPos == null) { LOGGER.debug("Tried to respawn, but need to find the portal first."); diff --git a/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java index a908652f1ebb426d265ef614746f70cd1e538268..e615b79f68a0467aa8cfa1c61b06ae048a28ef9b 100644 --- a/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java +++ b/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java @@ -19,7 +19,7 @@ import net.minecraft.world.level.block.state.BlockState; public class PatrolSpawner implements CustomSpawner { - private int nextTick; + //private int nextTick; // Folia - region threading public PatrolSpawner() {} @@ -32,15 +32,16 @@ public class PatrolSpawner implements CustomSpawner { return 0; } else { RandomSource randomsource = world.random; + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading // Paper start - Patrol settings // Random player selection moved up for per player spawning and configuration - int j = world.players().size(); + int j = world.getLocalPlayers().size(); // Folia - region threading if (j < 1) { return 0; } - net.minecraft.server.level.ServerPlayer entityhuman = world.players().get(randomsource.nextInt(j)); + net.minecraft.server.level.ServerPlayer entityhuman = world.getLocalPlayers().get(randomsource.nextInt(j)); // Folia - region threading if (entityhuman.isSpectator()) { return 0; } @@ -50,8 +51,8 @@ public class PatrolSpawner implements CustomSpawner { --entityhuman.patrolSpawnDelay; patrolSpawnDelay = entityhuman.patrolSpawnDelay; } else { - this.nextTick--; - patrolSpawnDelay = this.nextTick; + worldData.patrolSpawnerNextTick--; // Folia - region threading + patrolSpawnDelay = worldData.patrolSpawnerNextTick; // Folia - region threading } if (patrolSpawnDelay > 0) { @@ -66,7 +67,7 @@ public class PatrolSpawner implements CustomSpawner { if (world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.perPlayer) { entityhuman.patrolSpawnDelay += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200); } else { - this.nextTick += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200); + worldData.patrolSpawnerNextTick += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200); // Folia - region threading } if (days >= world.paperConfig().entities.behavior.pillagerPatrols.start.day && world.isDay()) { diff --git a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java index e8ae4449696d73c8c9b8b27d4d2e20db933a72cc..99959b50d06d550ce56bb26e35ec38ec741479da 100644 --- a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +++ b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java @@ -24,7 +24,7 @@ import net.minecraft.world.level.material.FluidState; public class PhantomSpawner implements CustomSpawner { - private int nextTick; + //private int nextTick; // Folia - region threading public PhantomSpawner() {} @@ -42,20 +42,22 @@ public class PhantomSpawner implements CustomSpawner { // Paper end RandomSource randomsource = world.random; - --this.nextTick; - if (this.nextTick > 0) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + + --worldData.phantomSpawnerNextTick; // Folia - region threading + if (worldData.phantomSpawnerNextTick > 0) { // Folia - region threading return 0; } else { // Paper start int spawnAttemptMinSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMinSeconds; int spawnAttemptMaxSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds; - this.nextTick += (spawnAttemptMinSeconds + randomsource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; + worldData.phantomSpawnerNextTick += (spawnAttemptMinSeconds + randomsource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; // Folia - region threading // Paper end if (world.getSkyDarken() < 5 && world.dimensionType().hasSkyLight()) { return 0; } else { int i = 0; - Iterator iterator = world.players().iterator(); + Iterator iterator = world.getLocalPlayers().iterator(); // Folia - region threading while (iterator.hasNext()) { Player entityhuman = (Player) iterator.next(); diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java index 1ca00340aaa201dd34e5c350d23ef53e126a0ca6..cf664ed17c6c138aed2a8680b51480ded47a2e50 100644 --- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java +++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java @@ -50,8 +50,101 @@ public class StructureCheck { private final BiomeSource biomeSource; private final long seed; private final DataFixer fixerUpper; - private final Long2ObjectMap> loadedChunks = new Long2ObjectOpenHashMap<>(); - private final Map featureChecks = new HashMap<>(); + // Foila start - synchronise this class + // additionally, make sure to purge entries from the maps so it does not leak memory + private static final int CHUNK_TOTAL_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups + private static final int PER_FEATURE_CHECK_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups + + private final SynchronisedLong2ObjectMap> loadedChunksSafe = new SynchronisedLong2ObjectMap<>(CHUNK_TOTAL_LIMIT); + private final java.util.concurrent.ConcurrentHashMap featureChecksSafe = new java.util.concurrent.ConcurrentHashMap<>(); + + private static final class SynchronisedLong2ObjectMap { + private final it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap map = new it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<>(); + private final int limit; + + public SynchronisedLong2ObjectMap(final int limit) { + this.limit = limit; + } + + // must hold lock on map + private void purgeEntries() { + while (this.map.size() > this.limit) { + this.map.removeLast(); + } + } + + public V get(final long key) { + synchronized (this.map) { + return this.map.getAndMoveToFirst(key); + } + } + + public V put(final long key, final V value) { + synchronized (this.map) { + final V ret = this.map.putAndMoveToFirst(key, value); + this.purgeEntries(); + return ret; + } + } + + public V compute(final long key, final java.util.function.BiFunction remappingFunction) { + synchronized (this.map) { + // first, compute the value - if one is added, it will be at the last entry + this.map.compute(key, remappingFunction); + // move the entry to first, just in case it was added at last + final V ret = this.map.getAndMoveToFirst(key); + // now purge the last entries + this.purgeEntries(); + + return ret; + } + } + } + + private static final class SynchronisedLong2BooleanMap { + private final it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap map = new it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap(); + private final int limit; + + public SynchronisedLong2BooleanMap(final int limit) { + this.limit = limit; + } + + // must hold lock on map + private void purgeEntries() { + while (this.map.size() > this.limit) { + this.map.removeLastBoolean(); + } + } + + public boolean remove(final long key) { + synchronized (this.map) { + return this.map.remove(key); + } + } + + // note: + public boolean getOrCompute(final long key, final it.unimi.dsi.fastutil.longs.Long2BooleanFunction ifAbsent) { + synchronized (this.map) { + if (this.map.containsKey(key)) { + return this.map.getAndMoveToFirst(key); + } + } + + final boolean put = ifAbsent.get(key); + + synchronized (this.map) { + if (this.map.containsKey(key)) { + return this.map.getAndMoveToFirst(key); + } + this.map.putAndMoveToFirst(key, put); + + this.purgeEntries(); + + return put; + } + } + } + // Foila end - synchronise this class public StructureCheck(ChunkScanAccess chunkIoWorker, RegistryAccess registryManager, StructureTemplateManager structureTemplateManager, ResourceKey worldKey, ChunkGenerator chunkGenerator, RandomState noiseConfig, LevelHeightAccessor world, BiomeSource biomeSource, long seed, DataFixer dataFixer) { // Paper - fix missing CB diff this.storageAccess = chunkIoWorker; @@ -70,7 +163,7 @@ public class StructureCheck { public StructureCheckResult checkStart(ChunkPos pos, Structure type, boolean skipReferencedStructures) { long l = pos.toLong(); - Object2IntMap object2IntMap = this.loadedChunks.get(l); + Object2IntMap object2IntMap = this.loadedChunksSafe.get(l); // Folia - region threading if (object2IntMap != null) { return this.checkStructureInfo(object2IntMap, type, skipReferencedStructures); } else { @@ -78,9 +171,9 @@ public class StructureCheck { if (structureCheckResult != null) { return structureCheckResult; } else { - boolean bl = this.featureChecks.computeIfAbsent(type, (structure2) -> { - return new Long2BooleanOpenHashMap(); - }).computeIfAbsent(l, (chunkPos) -> { + boolean bl = this.featureChecksSafe.computeIfAbsent(type, (structure2) -> { // Folia - region threading + return new SynchronisedLong2BooleanMap(PER_FEATURE_CHECK_LIMIT); // Folia - region threading + }).getOrCompute(l, (chunkPos) -> { // Folia - region threading return this.canCreateStructure(pos, type); }); return !bl ? StructureCheckResult.START_NOT_PRESENT : StructureCheckResult.CHUNK_LOAD_NEEDED; @@ -193,17 +286,26 @@ public class StructureCheck { } private void storeFullResults(long pos, Object2IntMap referencesByStructure) { - this.loadedChunks.put(pos, deduplicateEmptyMap(referencesByStructure)); - this.featureChecks.values().forEach((generationPossibilityByChunkPos) -> { - generationPossibilityByChunkPos.remove(pos); - }); + // Folia start - region threading - make access mt-safe + this.loadedChunksSafe.put(pos, deduplicateEmptyMap(referencesByStructure)); + // once we insert into loadedChunks, we don't really need to be very careful about removing everything + // from this map, as everything that checks this map uses loadedChunks first + // so, one way or another it's a race condition that doesn't matter + for (SynchronisedLong2BooleanMap value : this.featureChecksSafe.values()) { + value.remove(pos); + } + // Folia end - region threading - make access mt-safe } public void incrementReference(ChunkPos pos, Structure structure) { - this.loadedChunks.compute(pos.toLong(), (posx, referencesByStructure) -> { - if (referencesByStructure == null || referencesByStructure.isEmpty()) { + this.loadedChunksSafe.compute(pos.toLong(), (posx, referencesByStructure) -> { // Folia start - region threading + // make this COW so that we do not mutate state that may be currently in use + if (referencesByStructure == null) { referencesByStructure = new Object2IntOpenHashMap<>(); + } else { + referencesByStructure = referencesByStructure instanceof Object2IntOpenHashMap fastClone ? fastClone.clone() : new Object2IntOpenHashMap<>(referencesByStructure); } + // Folia end - region threading referencesByStructure.computeInt(structure, (feature, references) -> { return references == null ? 1 : references + 1; diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java index 6570e0b61d7602c57c61398ddce50418d0719ff2..bcee13beed247f7830ee85d099c367dbfd1ea51d 100644 --- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java +++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java @@ -26,14 +26,14 @@ public final class StructureStart { private final Structure structure; private final PiecesContainer pieceContainer; private final ChunkPos chunkPos; - private int references; + private final java.util.concurrent.atomic.AtomicInteger references; // Folia - region threading @Nullable private volatile BoundingBox cachedBoundingBox; public StructureStart(Structure structure, ChunkPos pos, int references, PiecesContainer children) { this.structure = structure; this.chunkPos = pos; - this.references = references; + this.references = new java.util.concurrent.atomic.AtomicInteger(references); // Folia - region threading this.pieceContainer = children; } @@ -101,7 +101,7 @@ public final class StructureStart { compoundTag.putString("id", context.registryAccess().registryOrThrow(Registries.STRUCTURE).getKey(this.structure).toString()); compoundTag.putInt("ChunkX", chunkPos.x); compoundTag.putInt("ChunkZ", chunkPos.z); - compoundTag.putInt("references", this.references); + compoundTag.putInt("references", this.references.get()); // Folia - region threading compoundTag.put("Children", this.pieceContainer.save(context)); return compoundTag; } else { @@ -119,15 +119,29 @@ public final class StructureStart { } public boolean canBeReferenced() { - return this.references < this.getMaxReferences(); + throw new UnsupportedOperationException("Use tryReference()"); // Folia - region threading } + // Folia start - region threading + public boolean tryReference() { + for (int curr = this.references.get();;) { + if (curr >= this.getMaxReferences()) { + return false; + } + + if (curr == (curr = this.references.compareAndExchange(curr, curr + 1))) { + return true; + } // else: try again + } + } + // Folia end - region threading + public void addReference() { - ++this.references; + throw new UnsupportedOperationException("Use tryReference()"); // Folia - region threading } public int getReferences() { - return this.references; + return this.references.get(); // Folia - region threading } protected int getMaxReferences() { diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java index 92d13c9f1ec1e5ff72c1d68f924a8d1c86c91565..3f224315f1e0a8f69e9d84d27fd7dbe45ea75667 100644 --- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java @@ -89,10 +89,10 @@ public class PortalForcer { BlockPos blockposition1 = villageplacerecord.getPos(); this.level.getChunkSource().addRegionTicket(TicketType.PORTAL, new ChunkPos(blockposition1), 3, blockposition1); - BlockState iblockdata = this.level.getBlockState(blockposition1); + BlockState iblockdata = this.level.getBlockStateFromEmptyChunk(blockposition1); // Folia - region threading return BlockUtil.getLargestRectangleAround(blockposition1, (Direction.Axis) iblockdata.getValue(BlockStateProperties.HORIZONTAL_AXIS), 21, Direction.Axis.Y, 21, (blockposition2) -> { - return this.level.getBlockState(blockposition2) == iblockdata; + return this.level.getBlockStateFromEmptyChunk(blockposition2) == iblockdata; // Folia - region threading }); }); } diff --git a/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java b/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java index b1c594dc6a6b8a6c737b99272acab9e7dbd0ed63..7c1768452fa0f7278ccc84470ef0965a3f96b0df 100644 --- a/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java +++ b/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java @@ -46,6 +46,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater { } private void addAndRun(BlockPos pos, CollectingNeighborUpdater.NeighborUpdates entry) { + io.papermc.paper.util.TickThread.ensureTickThread((net.minecraft.server.level.ServerLevel)this.level, pos, "Adding block without owning region"); // Folia - region threading boolean bl = this.count > 0; boolean bl2 = this.maxChainedNeighborUpdates >= 0 && this.count >= this.maxChainedNeighborUpdates; ++this.count; diff --git a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java index 353e602d476beea23e591ad770227c5d6c1e97fa..6f254ee89e1f2eb2b853888e2ff1259cd1f9a6db 100644 --- a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java +++ b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java @@ -10,7 +10,7 @@ import org.slf4j.Logger; public abstract class SavedData { private static final Logger LOGGER = LogUtils.getLogger(); - private boolean dirty; + private volatile boolean dirty; // Folia - make map data thread-safe public abstract CompoundTag save(CompoundTag nbt); @@ -28,6 +28,7 @@ public abstract class SavedData { public void save(File file) { if (this.isDirty()) { + this.setDirty(false); // Folia - make map data thread-safe - move before save, so that any changes after are not lost CompoundTag compoundTag = new CompoundTag(); compoundTag.put("data", this.save(new CompoundTag())); NbtUtils.addCurrentDataVersion(compoundTag); @@ -38,7 +39,7 @@ public abstract class SavedData { LOGGER.error("Could not save data {}", this, var4); } - this.setDirty(false); + // Folia - make map data thread-safe - move before save, so that any changes after are not lost } } } diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java index 9b2948b5150c8f039ca667a50765109721b93947..1b76e4edce628f2b25815e28cd4cb7504a83a00f 100644 --- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java +++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java @@ -27,17 +27,21 @@ public class MapIndex extends SavedData { @Override public CompoundTag save(CompoundTag nbt) { + synchronized (this.usedAuxIds) { // Folia - make map data thread-safe for(Object2IntMap.Entry entry : this.usedAuxIds.object2IntEntrySet()) { nbt.putInt(entry.getKey(), entry.getIntValue()); } + } // Folia - make map data thread-safe return nbt; } public int getFreeAuxValueForMap() { + synchronized (this.usedAuxIds) { // Folia - make map data thread-safe int i = this.usedAuxIds.getInt("map") + 1; this.usedAuxIds.put("map", i); this.setDirty(); return i; + } // Folia - make map data thread-safe } } diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java index 50348a5eec1e2ffabfc8405d708461376a162913..56e277f328a18e342c1adbe19660a31cbb175ba9 100644 --- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java @@ -185,7 +185,7 @@ public class MapItemSavedData extends SavedData { } @Override - public CompoundTag save(CompoundTag nbt) { + public synchronized CompoundTag save(CompoundTag nbt) { // Folia - make map data thread-safe DataResult dataresult = ResourceLocation.CODEC.encodeStart(NbtOps.INSTANCE, this.dimension.location()); // CraftBukkit - decompile error Logger logger = MapItemSavedData.LOGGER; @@ -242,7 +242,7 @@ public class MapItemSavedData extends SavedData { return nbt; } - public MapItemSavedData locked() { + public synchronized MapItemSavedData locked() { // Folia - make map data thread-safe MapItemSavedData worldmap = new MapItemSavedData(this.centerX, this.centerZ, this.scale, this.trackingPosition, this.unlimitedTracking, true, this.dimension); worldmap.bannerMarkers.putAll(this.bannerMarkers); @@ -253,11 +253,12 @@ public class MapItemSavedData extends SavedData { return worldmap; } - public MapItemSavedData scaled(int zoomOutScale) { + public synchronized MapItemSavedData scaled(int zoomOutScale) { // Folia - make map data thread-safe return MapItemSavedData.createFresh((double) this.centerX, (double) this.centerZ, (byte) Mth.clamp(this.scale + zoomOutScale, 0, 4), this.trackingPosition, this.unlimitedTracking, this.dimension); } - public void tickCarriedBy(Player player, ItemStack stack) { + public synchronized void tickCarriedBy(Player player, ItemStack stack) { // Folia - make map data thread-safe + io.papermc.paper.util.TickThread.ensureTickThread(player, "Ticking map player in incorrect region"); // Folia - region threading if (!this.carriedByPlayers.containsKey(player)) { MapItemSavedData.HoldingPlayer worldmap_worldmaphumantracker = new MapItemSavedData.HoldingPlayer(player); @@ -368,7 +369,7 @@ public class MapItemSavedData extends SavedData { rotation += rotation < 0.0D ? -8.0D : 8.0D; b2 = (byte) ((int) (rotation * 16.0D / 360.0D)); if (this.dimension == Level.NETHER && world != null) { - int j = (int) (world.getLevelData().getDayTime() / 10L); + int j = (int) (world.getLevelData().getDayTime() / 10L); // Folia - region threading - TODO b2 = (byte) (j * j * 34187121 + j * 121 >> 15 & 15); } @@ -427,14 +428,14 @@ public class MapItemSavedData extends SavedData { } @Nullable - public Packet getUpdatePacket(int id, Player player) { + public synchronized Packet getUpdatePacket(int id, Player player) { // Folia - make map data thread-safe MapItemSavedData.HoldingPlayer worldmap_worldmaphumantracker = (MapItemSavedData.HoldingPlayer) this.carriedByPlayers.get(player); return worldmap_worldmaphumantracker == null ? null : worldmap_worldmaphumantracker.nextUpdatePacket(id); } - public void setColorsDirty(int x, int z) { - this.setDirty(); + public synchronized void setColorsDirty(int x, int z) { // Folia - make map data thread-safe + // Folia - make dirty only after updating data - moved down Iterator iterator = this.carriedBy.iterator(); while (iterator.hasNext()) { @@ -442,15 +443,16 @@ public class MapItemSavedData extends SavedData { worldmap_worldmaphumantracker.markColorsDirty(x, z); } - + this.setDirty(); // Folia - make dirty only after updating data - moved from above } - public void setDecorationsDirty() { - this.setDirty(); + public synchronized void setDecorationsDirty() { // Folia - make map data thread-safe + // Folia - make dirty only after updating data - moved down this.carriedBy.forEach(MapItemSavedData.HoldingPlayer::markDecorationsDirty); + this.setDirty(); // Folia - make dirty only after updating data - moved from above } - public MapItemSavedData.HoldingPlayer getHoldingPlayer(Player player) { + public synchronized MapItemSavedData.HoldingPlayer getHoldingPlayer(Player player) { // Folia - make map data thread-safe MapItemSavedData.HoldingPlayer worldmap_worldmaphumantracker = (MapItemSavedData.HoldingPlayer) this.carriedByPlayers.get(player); if (worldmap_worldmaphumantracker == null) { @@ -462,7 +464,7 @@ public class MapItemSavedData extends SavedData { return worldmap_worldmaphumantracker; } - public boolean toggleBanner(LevelAccessor world, BlockPos pos) { + public synchronized boolean toggleBanner(LevelAccessor world, BlockPos pos) { // Folia - make map data thread-safe double d0 = (double) pos.getX() + 0.5D; double d1 = (double) pos.getZ() + 0.5D; int i = 1 << this.scale; @@ -471,7 +473,7 @@ public class MapItemSavedData extends SavedData { boolean flag = true; if (d2 >= -63.0D && d3 >= -63.0D && d2 <= 63.0D && d3 <= 63.0D) { - MapBanner mapiconbanner = MapBanner.fromWorld(world, pos); + MapBanner mapiconbanner = world.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null || !io.papermc.paper.util.TickThread.isTickThreadFor(world.getMinecraftWorld(), pos) ? null : MapBanner.fromWorld(world, pos); // Folia - make map data thread-safe - don't sync load or read data we do not own if (mapiconbanner == null) { return false; @@ -492,7 +494,7 @@ public class MapItemSavedData extends SavedData { return false; } - public void checkBanners(BlockGetter world, int x, int z) { + public synchronized void checkBanners(BlockGetter world, int x, int z) { // Folia - make map data thread-safe Iterator iterator = this.bannerMarkers.values().iterator(); while (iterator.hasNext()) { @@ -514,12 +516,12 @@ public class MapItemSavedData extends SavedData { return this.bannerMarkers.values(); } - public void removedFromFrame(BlockPos pos, int id) { + public synchronized void removedFromFrame(BlockPos pos, int id) { // Folia - make map data thread-safe this.removeDecoration("frame-" + id); this.frameMarkers.remove(MapFrame.frameId(pos)); } - public boolean updateColor(int x, int z, byte color) { + public synchronized boolean updateColor(int x, int z, byte color) { // Folia - make map data thread-safe byte b1 = this.colors[x + z * 128]; if (b1 != color) { @@ -530,12 +532,12 @@ public class MapItemSavedData extends SavedData { } } - public void setColor(int x, int z, byte color) { + public synchronized void setColor(int x, int z, byte color) { // Folia - make map data thread-safe this.colors[x + z * 128] = color; this.setColorsDirty(x, z); } - public boolean isExplorationMap() { + public synchronized boolean isExplorationMap() { // Folia - make map data thread-safe Iterator iterator = this.decorations.values().iterator(); MapDecoration mapicon; @@ -570,7 +572,7 @@ public class MapItemSavedData extends SavedData { return this.decorations.values(); } - public boolean isTrackedCountOverLimit(int iconCount) { + public synchronized boolean isTrackedCountOverLimit(int iconCount) { // Folia - make map data thread-safe return this.trackedDecorationCount >= iconCount; } @@ -696,11 +698,13 @@ public class MapItemSavedData extends SavedData { } public void applyToMap(MapItemSavedData mapState) { + synchronized (mapState) { // Folia - make map data thread-safe for (int i = 0; i < this.width; ++i) { for (int j = 0; j < this.height; ++j) { mapState.setColor(this.startX + i, this.startY + j, this.mapColors[i + j * this.width]); } } + } // Folia - make map data thread-safe } } diff --git a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java index defe31a5d3aa89a3d18b94f2ff005594e38754b3..55609e5bfb2e1d6d8d832355080f520ec7f5b7e7 100644 --- a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java +++ b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java @@ -36,6 +36,7 @@ public class DimensionDataStorage { } public T computeIfAbsent(Function readFunction, Supplier supplier, String id) { + synchronized (this.cache) { // Folia - make map data thread-safe T savedData = this.get(readFunction, id); if (savedData != null) { return savedData; @@ -44,10 +45,12 @@ public class DimensionDataStorage { this.set(id, savedData2); return savedData2; } + } // Folia - make map data thread-safe } @Nullable public T get(Function readFunction, String id) { + synchronized (this.cache) { // Folia - make map data thread-safe SavedData savedData = this.cache.get(id); if (savedData == null && !this.cache.containsKey(id)) { savedData = this.readSavedData(readFunction, id); @@ -55,6 +58,7 @@ public class DimensionDataStorage { } return (T)savedData; + } // Folia - make map data thread-safe } @Nullable @@ -73,7 +77,9 @@ public class DimensionDataStorage { } public void set(String id, SavedData state) { + synchronized (this.cache) { // Folia - make map data thread-safe this.cache.put(id, state); + } // Folia - make map data thread-safe } public CompoundTag readTagFromDisk(String id, int dataVersion) throws IOException { diff --git a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java index ac807277a6b26d140ea9873d17c7aa4fb5fe37b2..e13d8700593f1f486cfc5c96ac25894202c07b71 100644 --- a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java +++ b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java @@ -37,6 +37,21 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon this.dirty = false; } // Paper end - add dirty flag + // Folia start - region threading + public void offsetTicks(final long offset) { + if (offset == 0 || this.tickQueue.isEmpty()) { + return; + } + final ScheduledTick[] queue = this.tickQueue.toArray(new ScheduledTick[0]); + this.tickQueue.clear(); + for (final ScheduledTick entry : queue) { + final ScheduledTick newEntry = new ScheduledTick<>( + entry.type(), entry.pos(), entry.triggerTick() + offset, entry.subTickOrder() + ); + this.tickQueue.add(newEntry); + } + } + // Folia end - region threading public LevelChunkTicks() { } diff --git a/src/main/java/net/minecraft/world/ticks/LevelTicks.java b/src/main/java/net/minecraft/world/ticks/LevelTicks.java index 1d7c663fa0e550bd0cfb9a4b83ccd7e2968666f0..f3df9c9b6cff85565514f990597f3fe53652812c 100644 --- a/src/main/java/net/minecraft/world/ticks/LevelTicks.java +++ b/src/main/java/net/minecraft/world/ticks/LevelTicks.java @@ -42,13 +42,70 @@ public class LevelTicks implements LevelTickAccess { private final List> alreadyRunThisTick = new ArrayList<>(); private final Set> toRunThisTickSet = new ObjectOpenCustomHashSet<>(ScheduledTick.UNIQUE_TICK_HASH); private final BiConsumer, ScheduledTick> chunkScheduleUpdater = (chunkTickScheduler, tick) -> { - if (tick.equals(chunkTickScheduler.peek())) { - this.updateContainerScheduling(tick); + if (tick.equals(chunkTickScheduler.peek())) { // Folia - diff on change + this.updateContainerScheduling(tick); // Folia - diff on change } }; - public LevelTicks(LongPredicate tickingFutureReadyPredicate, Supplier profilerGetter) { + // Folia start - region threading + public final net.minecraft.server.level.ServerLevel world; + public final boolean isBlock; + + public void merge(final LevelTicks into, final long tickOffset) { + // note: containersToTick, toRunThisTick, alreadyRunThisTick, toRunThisTickSet + // are all transient state, only ever non-empty during tick. But merging regions occurs while there + // is no tick happening, so we assume they are empty. + for (final java.util.Iterator>> iterator = + ((Long2ObjectOpenHashMap>)this.allContainers).long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry> entry = iterator.next(); + final LevelChunkTicks tickContainer = entry.getValue(); + tickContainer.offsetTicks(tickOffset); + into.allContainers.put(entry.getLongKey(), tickContainer); + } + for (final java.util.Iterator iterator = ((Long2LongOpenHashMap)this.nextTickForContainer).long2LongEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2LongMap.Entry entry = iterator.next(); + into.nextTickForContainer.put(entry.getLongKey(), entry.getLongValue() + tickOffset); + } + } + + public void split(final int chunkToRegionShift, + final it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap> regionToData) { + for (final java.util.Iterator>> iterator = + ((Long2ObjectOpenHashMap>)this.allContainers).long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry> entry = iterator.next(); + + final long chunkKey = entry.getLongKey(); + final int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkX(chunkKey); + final int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkZ(chunkKey); + + final long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey( + chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift + ); + // Should always be non-null, since containers are removed on unload. + regionToData.get(regionSectionKey).allContainers.put(chunkKey, entry.getValue()); + } + for (final java.util.Iterator iterator = ((Long2LongOpenHashMap)this.nextTickForContainer).long2LongEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2LongMap.Entry entry = iterator.next(); + final long chunkKey = entry.getLongKey(); + final int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkX(chunkKey); + final int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkZ(chunkKey); + + final long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey( + chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift + ); + + // Should always be non-null, since containers are removed on unload. + regionToData.get(regionSectionKey).nextTickForContainer.put(chunkKey, entry.getLongValue()); + } + } + // Folia end - region threading + + public LevelTicks(LongPredicate tickingFutureReadyPredicate, Supplier profilerGetter, net.minecraft.server.level.ServerLevel world, boolean isBlock) { this.world = world; this.isBlock = isBlock; // Folia - add world and isBlock this.tickCheck = tickingFutureReadyPredicate; this.profiler = profilerGetter; } @@ -61,7 +118,17 @@ public class LevelTicks implements LevelTickAccess { this.nextTickForContainer.put(l, scheduledTick.triggerTick()); } - scheduler.setOnTickAdded(this.chunkScheduleUpdater); + // Folia start - region threading + final boolean isBlock = this.isBlock; + final net.minecraft.server.level.ServerLevel world = this.world; + // make sure the lambda contains no reference to this LevelTicks + scheduler.setOnTickAdded((chunkTickScheduler, tick) -> { + if (tick.equals(chunkTickScheduler.peek())) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); + (isBlock ? worldData.getBlockLevelTicks() : worldData.getFluidLevelTicks()).updateContainerScheduling((ScheduledTick)tick); + } + }); + // Folia end - region threading } public void removeContainer(ChunkPos pos) { @@ -76,6 +143,7 @@ public class LevelTicks implements LevelTickAccess { @Override public void schedule(ScheduledTick orderedTick) { + io.papermc.paper.util.TickThread.ensureTickThread(this.world, orderedTick.pos(), "Cannot schedule tick for another region!"); // Folia - region threading long l = ChunkPos.asLong(orderedTick.pos()); LevelChunkTicks levelChunkTicks = this.allContainers.get(l); if (levelChunkTicks == null) { diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java index e0b6c737f9de2b6e692d6813d8dea4c35f038573..27445d7bcfaaa34a60e65e8d339f15367d7b3b6e 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -303,6 +303,82 @@ public final class CraftServer implements Server { CraftItemFactory.instance(); } + // Folia start - region threading API + private final io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler(); + private final io.papermc.paper.threadedregions.scheduler.FoliaAsyncScheduler asyncScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaAsyncScheduler(); + private final io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler globalRegionScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler(); + + @Override + public final io.papermc.paper.threadedregions.scheduler.RegionScheduler getRegionScheduler() { + return this.regionizedScheduler; + } + + @Override + public final io.papermc.paper.threadedregions.scheduler.AsyncScheduler getAsyncScheduler() { + return this.asyncScheduler; + } + + @Override + public final io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler getGlobalRegionScheduler() { + return this.globalRegionScheduler; + } + + @Override + public final boolean isOwnedByCurrentRegion(World world, io.papermc.paper.math.Position position) { + return io.papermc.paper.util.TickThread.isTickThreadFor( + ((CraftWorld) world).getHandle(), position.blockX() >> 4, position.blockZ() >> 4 + ); + } + + @Override + public final boolean isOwnedByCurrentRegion(World world, io.papermc.paper.math.Position position, int squareRadiusChunks) { + return io.papermc.paper.util.TickThread.isTickThreadFor( + ((CraftWorld) world).getHandle(), position.blockX() >> 4, position.blockZ() >> 4, squareRadiusChunks + ); + } + + @Override + public final boolean isOwnedByCurrentRegion(Location location) { + World world = location.getWorld(); + return io.papermc.paper.util.TickThread.isTickThreadFor( + ((CraftWorld) world).getHandle(), location.getBlockX() >> 4, location.getBlockZ() >> 4 + ); + } + + @Override + public final boolean isOwnedByCurrentRegion(Location location, int squareRadiusChunks) { + World world = location.getWorld(); + return io.papermc.paper.util.TickThread.isTickThreadFor( + ((CraftWorld) world).getHandle(), location.getBlockX() >> 4, location.getBlockZ() >> 4, squareRadiusChunks + ); + } + + @Override + public final boolean isOwnedByCurrentRegion(World world, int chunkX, int chunkZ) { + return io.papermc.paper.util.TickThread.isTickThreadFor( + ((CraftWorld) world).getHandle(), chunkX, chunkZ + ); + } + + @Override + public final boolean isOwnedByCurrentRegion(World world, int chunkX, int chunkZ, int squareRadiusChunks) { + return io.papermc.paper.util.TickThread.isTickThreadFor( + ((CraftWorld) world).getHandle(), chunkX, chunkZ, squareRadiusChunks + ); + } + + @Override + public final boolean isOwnedByCurrentRegion(Entity entity) { + return io.papermc.paper.util.TickThread.isTickThreadFor(((org.bukkit.craftbukkit.entity.CraftEntity)entity).getHandle()); + } + + @Override + public boolean isGlobalTickThread() { + return io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread(); + } + + // Folia end - region threading API + public CraftServer(DedicatedServer console, PlayerList playerList) { this.console = console; this.playerList = (DedicatedPlayerList) playerList; @@ -856,6 +932,9 @@ public final class CraftServer implements Server { // NOTE: Should only be called from DedicatedServer.ah() public boolean dispatchServerCommand(CommandSender sender, ConsoleInput serverCommand) { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("May not dispatch server commands async"); + // Folia end - region threading if (sender instanceof Conversable) { Conversable conversable = (Conversable) sender; @@ -875,12 +954,44 @@ public final class CraftServer implements Server { } } + // Folia start - region threading + public void dispatchCmdAsync(CommandSender sender, String commandLine) { + if ((sender instanceof Entity entity)) { + ((org.bukkit.craftbukkit.entity.CraftEntity)entity).taskScheduler.schedule( + (nmsEntity) -> { + CraftServer.this.dispatchCommand(nmsEntity.getBukkitEntity(), commandLine); + }, + null, + 1L + ); + } else if (sender instanceof ConsoleCommandSender console) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + CraftServer.this.dispatchCommand(sender, commandLine); + }); + } else { + // huh? + throw new UnsupportedOperationException("Dispatching command for " + sender); + } + } + // Folia end - region threading + @Override public boolean dispatchCommand(CommandSender sender, String commandLine) { Validate.notNull(sender, "Sender cannot be null"); Validate.notNull(commandLine, "CommandLine cannot be null"); org.spigotmc.AsyncCatcher.catchOp("command dispatch"); // Spigot + // Folia start - region threading + if ((sender instanceof Entity entity)) { + io.papermc.paper.util.TickThread.ensureTickThread(((org.bukkit.craftbukkit.entity.CraftEntity)entity).getHandle(), "Dispatching command async"); + } else if (sender instanceof ConsoleCommandSender console) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Dispatching command async"); + } else { + // huh? + throw new UnsupportedOperationException("Dispatching command for " + sender); + } + // Folia end - region threading + // Paper Start if (!org.spigotmc.AsyncCatcher.shuttingDown && !Bukkit.isPrimaryThread()) { final CommandSender fSender = sender; @@ -2894,7 +3005,7 @@ public final class CraftServer implements Server { @Override public int getCurrentTick() { - return net.minecraft.server.MinecraftServer.currentTick; + return (int)io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading } @Override diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index ff6559bf563f2fdcc0f2843d4f4aa24d7ddfb6db..2ead9af36c4d41c61b68a960ff17e25894efeb2a 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -180,7 +180,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public int getTickableTileEntityCount() { - return world.getTotalTileEntityTickers(); + throw new UnsupportedOperationException(); // Folia - region threading - TODO fix this? } @Override @@ -237,7 +237,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { // Paper start - per world spawn limits for (SpawnCategory spawnCategory : SpawnCategory.values()) { if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { - setSpawnLimit(spawnCategory, this.world.paperConfig().entities.spawning.spawnLimits.getInt(CraftSpawnCategory.toNMS(spawnCategory))); + this.spawnCategoryLimit.put(spawnCategory, this.world.paperConfig().entities.spawning.spawnLimits.getInt(CraftSpawnCategory.toNMS(spawnCategory))); // Folia - region threading } } // Paper end @@ -329,6 +329,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public Chunk getChunkAt(int x, int z) { + io.papermc.paper.util.TickThread.isTickThreadFor(this.getHandle(), x, z); // Folia - region threading warnUnsafeChunk("getting a faraway chunk", x, z); // Paper // Paper start - add ticket to hold chunk for a little while longer if plugin accesses it net.minecraft.world.level.chunk.LevelChunk chunk = world.getChunkSource().getChunkAtIfLoadedImmediately(x, z); @@ -342,7 +343,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { // Paper start private void addTicket(int x, int z) { - io.papermc.paper.util.MCUtil.MAIN_EXECUTOR.execute(() -> world.getChunkSource().addRegionTicket(TicketType.PLUGIN, new ChunkPos(x, z), 0, Unit.INSTANCE)); // Paper + world.getChunkSource().addRegionTicket(TicketType.PLUGIN, new ChunkPos(x, z), 0, Unit.INSTANCE); // Paper // Folia - region threading - does not need to be on the main thread anymore } // Paper end @@ -361,10 +362,10 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean isChunkGenerated(int x, int z) { // Paper start - Fix this method - if (!Bukkit.isPrimaryThread()) { + if (!io.papermc.paper.util.TickThread.isTickThreadFor(this.getHandle(), x, z)) { // Folia - region threading return java.util.concurrent.CompletableFuture.supplyAsync(() -> { return CraftWorld.this.isChunkGenerated(x, z); - }, world.getChunkSource().mainThreadProcessor).join(); + }, (run) -> { io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(this.getHandle(), x, z, run);}).join(); // Folia - region threading } ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z); if (chunk == null) { @@ -418,7 +419,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { } private boolean unloadChunk0(int x, int z, boolean save) { - org.spigotmc.AsyncCatcher.catchOp("chunk unload"); // Spigot + io.papermc.paper.util.TickThread.ensureTickThread(this.world, x, z, "Cannot unload chunk asynchronously"); // Folia - region threading if (!this.isChunkLoaded(x, z)) { return true; } @@ -433,7 +434,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean regenerateChunk(int x, int z) { - org.spigotmc.AsyncCatcher.catchOp("chunk regenerate"); // Spigot + io.papermc.paper.util.TickThread.ensureTickThread(this.world, x, z, "Cannot regenerate chunk asynchronously"); // Folia - region threading warnUnsafeChunk("regenerating a faraway chunk", x, z); // Paper // Paper start - implement regenerateChunk method final ServerLevel serverLevel = this.world; @@ -495,6 +496,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean refreshChunk(int x, int z) { + io.papermc.paper.util.TickThread.ensureTickThread(this.world, x, z, "Cannot refresh chunk asynchronously"); // Folia - region threading ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); if (playerChunk == null) return false; @@ -530,7 +532,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean loadChunk(int x, int z, boolean generate) { - org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot + io.papermc.paper.util.TickThread.ensureTickThread(this.getHandle(), x, z, "May not sync load chunks asynchronously"); // Folia - region threading warnUnsafeChunk("loading a faraway chunk", x, z); // Paper // Paper start - Optimize this method ChunkPos chunkPos = new ChunkPos(x, z); @@ -603,7 +605,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; if (chunkDistanceManager.addRegionTicketAtDistance(TicketType.PLUGIN_TICKET, new ChunkPos(x, z), 2, plugin)) { // keep in-line with force loading, add at level 31 - this.getChunkAt(x, z); // ensure loaded + //this.getChunkAt(x, z); // ensure loaded // Folia - region threading - do not load chunks for tickets anymore to make this mt-safe return true; } @@ -788,13 +790,15 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean generateTree(Location loc, TreeType type, BlockChangeDelegate delegate) { - world.captureTreeGeneration = true; - world.captureBlockStates = true; + io.papermc.paper.util.TickThread.ensureTickThread(this.world, loc.getX(), loc.getZ(), "Cannot generate tree asynchronously"); // Folia - region threading + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + worldData.captureTreeGeneration = true; // Folia - region threading + worldData.captureBlockStates = true; // Folia - region threading boolean grownTree = this.generateTree(loc, type); - world.captureBlockStates = false; - world.captureTreeGeneration = false; + worldData.captureBlockStates = false; // Folia - region threading + worldData.captureTreeGeneration = false; // Folia - region threading if (grownTree) { // Copy block data to delegate - for (BlockState blockstate : world.capturedBlockStates.values()) { + for (BlockState blockstate : worldData.capturedBlockStates.values()) { // Folia - region threading BlockPos position = ((CraftBlockState) blockstate).getPosition(); net.minecraft.world.level.block.state.BlockState oldBlock = this.world.getBlockState(position); int flag = ((CraftBlockState) blockstate).getFlag(); @@ -802,10 +806,10 @@ public class CraftWorld extends CraftRegionAccessor implements World { net.minecraft.world.level.block.state.BlockState newBlock = this.world.getBlockState(position); this.world.notifyAndUpdatePhysics(position, null, oldBlock, newBlock, newBlock, flag, 512); } - world.capturedBlockStates.clear(); + worldData.capturedBlockStates.clear(); // Folia - region threading return true; } else { - world.capturedBlockStates.clear(); + worldData.capturedBlockStates.clear(); // Folia - region threading return false; } } @@ -839,6 +843,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setTime(long time) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify time off of the global region"); // Folia - region threading long margin = (time - this.getFullTime()) % 24000; if (margin < 0) margin += 24000; this.setFullTime(this.getFullTime() + margin); @@ -851,6 +856,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setFullTime(long time) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify time off of the global region"); // Folia - region threading // Notify anyone who's listening TimeSkipEvent event = new TimeSkipEvent(this, TimeSkipEvent.SkipReason.CUSTOM, time - this.world.getDayTime()); this.server.getPluginManager().callEvent(event); @@ -878,7 +884,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public long getGameTime() { - return world.levelData.getGameTime(); + return this.getHandle().getGameTime(); // Folia - region threading } @Override @@ -898,11 +904,13 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean createExplosion(double x, double y, double z, float power, boolean setFire, boolean breakBlocks, Entity source) { + io.papermc.paper.util.TickThread.ensureTickThread(this.world, x, z, "Cannot create explosion asynchronously"); return !this.world.explode(source == null ? null : ((CraftEntity) source).getHandle(), x, y, z, power, setFire, breakBlocks ? net.minecraft.world.level.Level.ExplosionInteraction.MOB : net.minecraft.world.level.Level.ExplosionInteraction.NONE).wasCanceled; } // Paper start @Override public boolean createExplosion(Entity source, Location loc, float power, boolean setFire, boolean breakBlocks) { + io.papermc.paper.util.TickThread.ensureTickThread(this.world, loc.getX(), loc.getZ(), "Cannot create explosion asynchronously"); return !world.explode(source != null ? ((org.bukkit.craftbukkit.entity.CraftEntity) source).getHandle() : null, loc.getX(), loc.getY(), loc.getZ(), power, setFire, breakBlocks ? net.minecraft.world.level.Level.ExplosionInteraction.MOB : net.minecraft.world.level.Level.ExplosionInteraction.NONE).wasCanceled; } // Paper end @@ -977,6 +985,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public int getHighestBlockYAt(int x, int z, org.bukkit.HeightMap heightMap) { + io.papermc.paper.util.TickThread.ensureTickThread(this.world, x >> 4, z >> 4, "Cannot retrieve chunk asynchronously"); // Folia - region threading warnUnsafeChunk("getting a faraway chunk", x >> 4, z >> 4); // Paper // Transient load for this tick return this.world.getChunk(x >> 4, z >> 4).getHeight(CraftHeightMap.toNMS(heightMap), x, z); @@ -1012,6 +1021,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setBiome(int x, int y, int z, Holder bb) { BlockPos pos = new BlockPos(x, 0, z); + io.papermc.paper.util.TickThread.ensureTickThread(this.world, pos, "Cannot retrieve chunk asynchronously"); // Folia - region threading if (this.world.hasChunkAt(pos)) { net.minecraft.world.level.chunk.LevelChunk chunk = this.world.getChunkAt(pos); @@ -1287,6 +1297,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setStorm(boolean hasStorm) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading world.serverLevelData.setRaining(hasStorm, org.bukkit.event.weather.WeatherChangeEvent.Cause.PLUGIN); // Paper this.setWeatherDuration(0); // Reset weather duration (legacy behaviour) this.setClearWeatherDuration(0); // Reset clear weather duration (reset "/weather clear" commands) @@ -1299,6 +1310,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setWeatherDuration(int duration) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading world.serverLevelData.setRainTime(duration); } @@ -1309,6 +1321,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setThundering(boolean thundering) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading world.serverLevelData.setThundering(thundering, org.bukkit.event.weather.ThunderChangeEvent.Cause.PLUGIN); // Paper this.setThunderDuration(0); // Reset weather duration (legacy behaviour) this.setClearWeatherDuration(0); // Reset clear weather duration (reset "/weather clear" commands) @@ -1321,6 +1334,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setThunderDuration(int duration) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading world.serverLevelData.setThunderTime(duration); } @@ -1331,6 +1345,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setClearWeatherDuration(int duration) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading world.serverLevelData.setClearWeatherTime(duration); } @@ -1524,6 +1539,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setKeepSpawnInMemory(boolean keepLoaded) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify keep spawn in memory off of the global region"); // Folia - region threading // Paper start - Configurable spawn radius if (keepLoaded == world.keepSpawnInMemory) { // do nothing, nothing has changed @@ -1602,6 +1618,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setHardcore(boolean hardcore) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading world.serverLevelData.settings.hardcore = hardcore; } @@ -1614,6 +1631,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setTicksPerAnimalSpawns(int ticksPerAnimalSpawns) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setTicksPerSpawns(SpawnCategory.ANIMAL, ticksPerAnimalSpawns); } @@ -1626,6 +1644,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setTicksPerMonsterSpawns(int ticksPerMonsterSpawns) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setTicksPerSpawns(SpawnCategory.MONSTER, ticksPerMonsterSpawns); } @@ -1638,6 +1657,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setTicksPerWaterSpawns(int ticksPerWaterSpawns) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setTicksPerSpawns(SpawnCategory.WATER_ANIMAL, ticksPerWaterSpawns); } @@ -1650,6 +1670,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setTicksPerWaterAmbientSpawns(int ticksPerWaterAmbientSpawns) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setTicksPerSpawns(SpawnCategory.WATER_AMBIENT, ticksPerWaterAmbientSpawns); } @@ -1662,6 +1683,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setTicksPerWaterUndergroundCreatureSpawns(int ticksPerWaterUndergroundCreatureSpawns) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setTicksPerSpawns(SpawnCategory.WATER_UNDERGROUND_CREATURE, ticksPerWaterUndergroundCreatureSpawns); } @@ -1674,11 +1696,13 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setTicksPerAmbientSpawns(int ticksPerAmbientSpawns) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setTicksPerSpawns(SpawnCategory.AMBIENT, ticksPerAmbientSpawns); } @Override public void setTicksPerSpawns(SpawnCategory spawnCategory, int ticksPerCategorySpawn) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading Validate.notNull(spawnCategory, "SpawnCategory cannot be null"); Validate.isTrue(CraftSpawnCategory.isValidForLimits(spawnCategory), "SpawnCategory." + spawnCategory + " are not supported."); @@ -1695,21 +1719,25 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setMetadata(String metadataKey, MetadataValue newMetadataValue) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify metadata off of the global region"); // Folia - region threading this.server.getWorldMetadata().setMetadata(this, metadataKey, newMetadataValue); } @Override public List getMetadata(String metadataKey) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot retrieve metadata off of the global region"); // Folia - region threading return this.server.getWorldMetadata().getMetadata(this, metadataKey); } @Override public boolean hasMetadata(String metadataKey) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot retrieve metadata off of the global region"); // Folia - region threading return this.server.getWorldMetadata().hasMetadata(this, metadataKey); } @Override public void removeMetadata(String metadataKey, Plugin owningPlugin) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify metadata off of the global region"); // Folia - region threading this.server.getWorldMetadata().removeMetadata(this, metadataKey, owningPlugin); } @@ -1722,6 +1750,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setMonsterSpawnLimit(int limit) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setSpawnLimit(SpawnCategory.MONSTER, limit); } @@ -1734,6 +1763,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setAnimalSpawnLimit(int limit) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setSpawnLimit(SpawnCategory.ANIMAL, limit); } @@ -1746,6 +1776,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setWaterAnimalSpawnLimit(int limit) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setSpawnLimit(SpawnCategory.WATER_ANIMAL, limit); } @@ -1758,6 +1789,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setWaterAmbientSpawnLimit(int limit) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setSpawnLimit(SpawnCategory.WATER_AMBIENT, limit); } @@ -1770,6 +1802,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setWaterUndergroundCreatureSpawnLimit(int limit) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setSpawnLimit(SpawnCategory.WATER_UNDERGROUND_CREATURE, limit); } @@ -1782,6 +1815,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setAmbientSpawnLimit(int limit) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setSpawnLimit(SpawnCategory.AMBIENT, limit); } @@ -1804,6 +1838,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setSpawnLimit(SpawnCategory spawnCategory, int limit) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading Validate.notNull(spawnCategory, "SpawnCategory cannot be null"); Validate.isTrue(CraftSpawnCategory.isValidForLimits(spawnCategory), "SpawnCategory." + spawnCategory + " are not supported."); @@ -1858,7 +1893,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { if (!(entity instanceof CraftEntity craftEntity) || entity.getWorld() != this || sound == null || category == null) return; ClientboundSoundEntityPacket packet = new ClientboundSoundEntityPacket(BuiltInRegistries.SOUND_EVENT.wrapAsHolder(CraftSound.getSoundEffect(sound)), net.minecraft.sounds.SoundSource.valueOf(category.name()), craftEntity.getHandle(), volume, pitch, this.getHandle().getRandom().nextLong()); - ChunkMap.TrackedEntity entityTracker = this.getHandle().getChunkSource().chunkMap.entityMap.get(entity.getEntityId()); + ChunkMap.TrackedEntity entityTracker = ((CraftEntity) entity).getHandle().tracker; // Folia - region threading if (entityTracker != null) { entityTracker.broadcastAndSend(packet); } @@ -1869,7 +1904,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { if (!(entity instanceof CraftEntity craftEntity) || entity.getWorld() != this || sound == null || category == null) return; ClientboundSoundEntityPacket packet = new ClientboundSoundEntityPacket(Holder.direct(SoundEvent.createVariableRangeEvent(new ResourceLocation(sound))), net.minecraft.sounds.SoundSource.valueOf(category.name()), craftEntity.getHandle(), volume, pitch, this.getHandle().getRandom().nextLong()); - ChunkMap.TrackedEntity entityTracker = this.getHandle().getChunkSource().chunkMap.entityMap.get(entity.getEntityId()); + ChunkMap.TrackedEntity entityTracker = ((CraftEntity)entity).getHandle().tracker; // Folia - region threading if (entityTracker != null) { entityTracker.broadcastAndSend(packet); } @@ -1922,6 +1957,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean setGameRuleValue(String rule, String value) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading // No null values allowed if (rule == null || value == null) return false; @@ -1963,6 +1999,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean setGameRule(GameRule rule, T newValue) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading Validate.notNull(rule, "GameRule cannot be null"); Validate.notNull(newValue, "GameRule value cannot be null"); @@ -2228,6 +2265,12 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void sendGameEvent(Entity sourceEntity, org.bukkit.GameEvent gameEvent, Vector position) { + // Folia start - region threading + if (sourceEntity != null && !Bukkit.isOwnedByCurrentRegion(sourceEntity)) { + throw new IllegalStateException("Cannot send game event asynchronously"); + } + io.papermc.paper.util.TickThread.ensureTickThread(this.world, position.getX(), position.getZ(), "Cannot send game event asynchronously"); + // Folia end - region threading getHandle().gameEvent(sourceEntity != null ? ((CraftEntity) sourceEntity).getHandle(): null, net.minecraft.core.registries.BuiltInRegistries.GAME_EVENT.get(org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(gameEvent.getKey())), org.bukkit.craftbukkit.util.CraftVector.toBlockPos(position)); } // Paper end @@ -2355,7 +2398,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { // Paper start public java.util.concurrent.CompletableFuture getChunkAtAsync(int x, int z, boolean gen, boolean urgent) { warnUnsafeChunk("getting a faraway chunk async", x, z); // Paper - if (Bukkit.isPrimaryThread()) { + if (io.papermc.paper.util.TickThread.isTickThreadFor(this.getHandle(), x, z)) { // Folia - region threading net.minecraft.world.level.chunk.LevelChunk immediate = this.world.getChunkSource().getChunkAtIfLoadedImmediately(x, z); if (immediate != null) { return java.util.concurrent.CompletableFuture.completedFuture(immediate.getBukkitChunk()); @@ -2372,7 +2415,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { 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(() -> { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(this.getHandle(), x, z, () -> { // Folia - region threading net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c; if (chunk != null) addTicket(x, z); // Paper ret.complete(chunk == null ? null : chunk.getBukkitChunk()); diff --git a/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java b/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java index a3d5e319473e2f6316b3ef8edf719296e02d85a1..55abed5fdf8dc699ade5b25b1f1949417dbf7a69 100644 --- a/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java +++ b/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java @@ -46,6 +46,7 @@ public final class CapturedBlockState extends CraftBlockState { for (int k = 0; k < j; ++k) { Bee entitybee = new Bee(EntityType.BEE, generatoraccessseed.getMinecraftWorld()); + entitybee.setPosRaw(blockposition1.getX(), blockposition1.getY(), blockposition1.getZ()); // Folia - region threading - set position so that thread checks do not fail tileentitybeehive.addOccupantWithPresetTicks(entitybee, false, random.nextInt(599)); } diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java index 962c950ca9c7e047a3aec215d4faa73676049d36..2f0ee1fa2a250e1edc2a33e0bb91009a5bb945f1 100644 --- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java @@ -81,6 +81,11 @@ public class CraftBlock implements Block { } public net.minecraft.world.level.block.state.BlockState getNMS() { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); + } + // Folia end - region threading return this.world.getBlockState(position); } @@ -157,6 +162,11 @@ public class CraftBlock implements Block { } private void setData(final byte data, int flag) { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); + } + // Folia end - region threading this.world.setBlock(position, CraftMagicNumbers.getBlock(this.getType(), data), flag); } @@ -198,6 +208,11 @@ public class CraftBlock implements Block { } public static boolean setTypeAndData(LevelAccessor world, BlockPos position, net.minecraft.world.level.block.state.BlockState old, net.minecraft.world.level.block.state.BlockState blockData, boolean applyPhysics) { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); + } + // Folia end - region threading // SPIGOT-611: need to do this to prevent glitchiness. Easier to handle this here (like /setblock) than to fix weirdness in tile entity cleanup if (old.hasBlockEntity() && blockData.getBlock() != old.getBlock()) { // SPIGOT-3725 remove old tile entity if block changes // SPIGOT-4612: faster - just clear tile @@ -340,18 +355,33 @@ public class CraftBlock implements Block { @Override public Biome getBiome() { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); + } + // Folia end - region threading return this.getWorld().getBiome(this.getX(), this.getY(), this.getZ()); } // Paper start @Override public Biome getComputedBiome() { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); + } + // Folia end - region threading return this.getWorld().getComputedBiome(this.getX(), this.getY(), this.getZ()); } // Paper end @Override public void setBiome(Biome bio) { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); + } + // Folia end - region threading this.getWorld().setBiome(this.getX(), this.getY(), this.getZ(), bio); } @@ -422,6 +452,11 @@ public class CraftBlock implements Block { @Override public boolean isBlockFaceIndirectlyPowered(BlockFace face) { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); + } + // Folia end - region threading int power = this.world.getMinecraftWorld().getSignal(position, CraftBlock.blockFaceToNotch(face)); Block relative = this.getRelative(face); @@ -434,6 +469,11 @@ public class CraftBlock implements Block { @Override public int getBlockPower(BlockFace face) { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); + } + // Folia end - region threading int power = 0; net.minecraft.world.level.Level world = this.world.getMinecraftWorld(); int x = this.getX(); @@ -520,6 +560,11 @@ public class CraftBlock implements Block { @Override public boolean breakNaturally(ItemStack item, boolean triggerEffect, boolean dropExperience) { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); + } + // Folia end - region threading // Paper end // Order matters here, need to drop before setting to air so skulls can get their data net.minecraft.world.level.block.state.BlockState iblockdata = this.getNMS(); @@ -563,21 +608,27 @@ public class CraftBlock implements Block { @Override public boolean applyBoneMeal(BlockFace face) { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); + } + // Folia end - region threading Direction direction = CraftBlock.blockFaceToNotch(face); BlockFertilizeEvent event = null; ServerLevel world = this.getCraftWorld().getHandle(); UseOnContext context = new UseOnContext(world, null, InteractionHand.MAIN_HAND, Items.BONE_MEAL.getDefaultInstance(), new BlockHitResult(Vec3.ZERO, direction, this.getPosition(), false)); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading // SPIGOT-6895: Call StructureGrowEvent and BlockFertilizeEvent - world.captureTreeGeneration = true; + worldData.captureTreeGeneration = true; // Folia - region threading InteractionResult result = BoneMealItem.applyBonemeal(context); - world.captureTreeGeneration = false; + worldData.captureTreeGeneration = false; // Folia - region threading - if (world.capturedBlockStates.size() > 0) { - TreeType treeType = SaplingBlock.treeType; - SaplingBlock.treeType = null; - List blocks = new ArrayList<>(world.capturedBlockStates.values()); - world.capturedBlockStates.clear(); + if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading + TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading + SaplingBlock.treeTypeRT.set(null); // Folia - region threading + List blocks = new ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading StructureGrowEvent structureEvent = null; if (treeType != null) { @@ -663,6 +714,11 @@ public class CraftBlock implements Block { @Override public RayTraceResult rayTrace(Location start, Vector direction, double maxDistance, FluidCollisionMode fluidCollisionMode) { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); + } + // Folia end - region threading Validate.notNull(start, "Start location is null!"); Validate.isTrue(this.getWorld().equals(start.getWorld()), "Start location is from different world!"); start.checkFinite(); @@ -704,6 +760,11 @@ public class CraftBlock implements Block { @Override public boolean canPlace(BlockData data) { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); + } + // Folia end - region threading Preconditions.checkArgument(data != null, "Provided block data is null!"); net.minecraft.world.level.block.state.BlockState iblockdata = ((CraftBlockData) data).getState(); net.minecraft.world.level.Level world = this.world.getMinecraftWorld(); @@ -734,6 +795,11 @@ public class CraftBlock implements Block { @Override public float getDestroySpeed(ItemStack itemStack, boolean considerEnchants) { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); + } + // Folia end - region threading net.minecraft.world.item.ItemStack nmsItemStack; if (itemStack instanceof CraftItemStack) { nmsItemStack = ((CraftItemStack) itemStack).handle; @@ -759,6 +825,11 @@ public class CraftBlock implements Block { @Override public void tick() { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); + } + // Folia end - region threading net.minecraft.world.level.block.state.BlockState blockData = this.getNMS(); net.minecraft.server.level.ServerLevel level = this.world.getMinecraftWorld(); @@ -767,6 +838,11 @@ public class CraftBlock implements Block { @Override public void randomTick() { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); + } + // Folia end - region threading net.minecraft.world.level.block.state.BlockState blockData = this.getNMS(); net.minecraft.server.level.ServerLevel level = this.world.getMinecraftWorld(); diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java index a8ab1d3ee81664193be39d2735d6495136e0e310..462493e2caf576f9dbfcffd1d9a8ca696febf83e 100644 --- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java +++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java @@ -206,6 +206,12 @@ public class CraftBlockState implements BlockState { LevelAccessor access = this.getWorldHandle(); CraftBlock block = this.getBlock(); + // Folia start - region threading + if (access instanceof net.minecraft.server.level.ServerLevel serverWorld) { + io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); + } + // Folia end - region threading + if (block.getType() != this.getType()) { if (!force) { return false; @@ -343,6 +349,9 @@ public class CraftBlockState implements BlockState { @Override public java.util.Collection getDrops(org.bukkit.inventory.ItemStack item, org.bukkit.entity.Entity entity) { + // Folia start - region threading + io.papermc.paper.util.TickThread.ensureTickThread(world.getHandle(), position, "Cannot modify world asynchronously"); + // Folia end - region threading net.minecraft.world.item.ItemStack nms = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(item); // Modelled off EntityHuman#hasBlock diff --git a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java index cd4ad8261e56365850068db1d83d6a8454026737..78f7e72f2912dae503c2dab7d1992b652b404ae5 100644 --- a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java +++ b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java @@ -50,7 +50,7 @@ public class ConsoleCommandCompleter implements Completer { return syncEvent.callEvent() ? syncEvent.getCompletions() : com.google.common.collect.ImmutableList.of(); } }; - server.getServer().processQueue.add(syncCompletions); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(syncCompletions); // Folia - region threading try { final List legacyCompletions = syncCompletions.get(); completions.removeIf(it -> !legacyCompletions.contains(it.suggestion())); // remove any suggestions that were removed @@ -98,7 +98,7 @@ public class ConsoleCommandCompleter implements Completer { return tabEvent.isCancelled() ? Collections.EMPTY_LIST : tabEvent.getCompletions(); } }; - server.getServer().processQueue.add(waitable); // Paper - Remove "this." + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(waitable); // Folia - region threading try { List offers = waitable.get(); if (offers == null) { diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java index 57a0dbb23a32123d30c3b3572f4d129be9d97847..652c28a66d45680479a38463525afcf191c55ab4 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java @@ -203,6 +203,16 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { private EntityDamageEvent lastDamageEvent; private final CraftPersistentDataContainer persistentDataContainer = new CraftPersistentDataContainer(CraftEntity.DATA_TYPE_REGISTRY); protected net.kyori.adventure.pointer.Pointers adventure$pointers; // Paper - implement pointers + public final io.papermc.paper.threadedregions.EntityScheduler taskScheduler = new io.papermc.paper.threadedregions.EntityScheduler(this); // Folia - region threading + + // Folia start - region threading API + private final io.papermc.paper.threadedregions.scheduler.FoliaEntityScheduler apiScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaEntityScheduler(this); + + @Override + public final io.papermc.paper.threadedregions.scheduler.EntityScheduler getScheduler() { + return this.apiScheduler; + }; + // Folia end - region threading API public CraftEntity(final CraftServer server, final Entity entity) { this.server = server; @@ -567,6 +577,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { @Override public boolean teleport(Location location, TeleportCause cause, io.papermc.paper.entity.TeleportFlag... flags) { + // Folia start - region threading + if (true) { + throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); + } + // Folia end - region threading // Paper end Preconditions.checkArgument(location != null, "location cannot be null"); location.checkFinite(); @@ -1244,7 +1259,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { } ServerLevel world = ((CraftWorld) this.getWorld()).getHandle(); - ChunkMap.TrackedEntity entityTracker = world.getChunkSource().chunkMap.entityMap.get(this.getEntityId()); + ChunkMap.TrackedEntity entityTracker = this.getHandle().tracker; // Folia - region threading if (entityTracker == null) { return; @@ -1308,30 +1323,43 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { 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(); + // Folia start - region threading 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); + java.util.function.Consumer run = (Entity nmsEntity) -> { + boolean success = nmsEntity.teleportAsync( + ((CraftWorld)locationClone.getWorld()).getHandle(), + new net.minecraft.world.phys.Vec3(locationClone.getX(), locationClone.getY(), locationClone.getZ()), + null, null, net.minecraft.world.phys.Vec3.ZERO, + cause == null ? TeleportCause.UNKNOWN : cause, + Entity.TELEPORT_FLAG_LOAD_CHUNK | Entity.TELEPORT_FLAG_UNMOUNT, // preserve behavior with old API: dismount the entity so it can teleport + (Entity entityTp) -> { + ret.complete(Boolean.TRUE); } - }); - }); + ); + if (!success) { + ret.complete(Boolean.FALSE); + } + }; + if (org.bukkit.Bukkit.isOwnedByCurrentRegion(this)) { + run.accept(this.getHandle()); + return ret; + } + boolean scheduled = this.taskScheduler.schedule( + // success + run, + // retired + (Entity nmsEntity) -> { + ret.complete(Boolean.FALSE); + }, + 1L + ); + + if (!scheduled) { + ret.complete(Boolean.FALSE); + } return ret; + // Folia end - region threading } @Override diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index fec408416aa63e1022802a644bb95131601f0c3b..c6e6ce4a077550e8e86995f18a4deda4b7b369eb 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -564,7 +564,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { @Override public void kickPlayer(String message) { - org.spigotmc.AsyncCatcher.catchOp("player kick"); // Spigot + //org.spigotmc.AsyncCatcher.catchOp("player kick"); // Spigot // Folia - thread-safe now, as it will simply delay the kick if (this.getHandle().connection == null) return; this.getHandle().connection.disconnect(message == null ? "" : message, org.bukkit.event.player.PlayerKickEvent.Cause.PLUGIN); // Paper - kick event cause @@ -1259,6 +1259,11 @@ public class CraftPlayer extends CraftHumanEntity implements Player { @Override public boolean teleport(Location location, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause, io.papermc.paper.entity.TeleportFlag... flags) { + // Folia start - region threading + if (true) { + throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); + } + // Folia end - region threading java.util.Set relativeArguments; java.util.Set allFlags; if (flags.length == 0) { @@ -1779,7 +1784,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { private void unregisterEntity(Entity other) { // Paper end ChunkMap tracker = ((ServerLevel) this.getHandle().level).getChunkSource().chunkMap; - ChunkMap.TrackedEntity entry = tracker.entityMap.get(other.getId()); + ChunkMap.TrackedEntity entry = other.tracker; // Folia - region threading if (entry != null) { entry.removePlayer(this.getHandle()); } @@ -1863,7 +1868,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { this.getHandle().connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(otherPlayer))); } - ChunkMap.TrackedEntity entry = tracker.entityMap.get(other.getId()); + ChunkMap.TrackedEntity entry = other.tracker; // Folia - region threading if (entry != null && !entry.seenBy.contains(this.getHandle().connection)) { entry.updatePlayer(this.getHandle()); } diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java index cdb8ec04f4a19ec3dbedbd5b17a7d1f3afaa238e..49c68ee30436232663b8d4dd15991e19b5f59d09 100644 --- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java @@ -227,8 +227,8 @@ import org.bukkit.potion.PotionEffect; import org.bukkit.event.entity.SpawnerSpawnEvent; // Spigot public class CraftEventFactory { - public static org.bukkit.block.Block blockDamage; // For use in EntityDamageByBlockEvent - public static Entity entityDamage; // For use in EntityDamageByEntityEvent + public static final ThreadLocal blockDamageRT = new ThreadLocal<>(); // For use in EntityDamageByBlockEvent // Folia - region threading + public static final ThreadLocal entityDamageRT = new ThreadLocal<>(); // For use in EntityDamageByEntityEvent // Folia - region threading // helper methods private static boolean canBuild(ServerLevel world, Player player, int x, int z) { @@ -841,7 +841,7 @@ public class CraftEventFactory { return CraftEventFactory.handleBlockSpreadEvent(world, source, target, block, 2); } - public static BlockPos sourceBlockOverride = null; // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. + public static ThreadLocal sourceBlockOverrideRT = new ThreadLocal<>(); // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // Folia - region threading public static boolean handleBlockSpreadEvent(LevelAccessor world, BlockPos source, BlockPos target, net.minecraft.world.level.block.state.BlockState block, int flag) { // Suppress during worldgen if (!(world instanceof Level)) { @@ -852,7 +852,7 @@ public class CraftEventFactory { CraftBlockState state = CraftBlockStates.getBlockState(world, target, flag); state.setData(block); - BlockSpreadEvent event = new BlockSpreadEvent(state.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverride != null ? CraftEventFactory.sourceBlockOverride : source), state); + BlockSpreadEvent event = new BlockSpreadEvent(state.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverrideRT.get() != null ? CraftEventFactory.sourceBlockOverrideRT.get() : source), state); // Folia - region threading Bukkit.getPluginManager().callEvent(event); if (!event.isCancelled()) { @@ -967,8 +967,8 @@ public class CraftEventFactory { private static EntityDamageEvent handleEntityDamageEvent(Entity entity, DamageSource source, Map modifiers, Map> modifierFunctions, boolean cancelled) { if (source.is(DamageTypeTags.IS_EXPLOSION)) { DamageCause damageCause; - Entity damager = CraftEventFactory.entityDamage; - CraftEventFactory.entityDamage = null; + Entity damager = CraftEventFactory.entityDamageRT.get(); // Folia - region threading + CraftEventFactory.entityDamageRT.set(null); // Folia - region threading EntityDamageEvent event; if (damager == null) { event = new EntityDamageByBlockEvent(null, entity.getBukkitEntity(), DamageCause.BLOCK_EXPLOSION, modifiers, modifierFunctions); @@ -1024,13 +1024,13 @@ public class CraftEventFactory { } return event; } else if (source.is(DamageTypes.LAVA)) { - EntityDamageEvent event = (new EntityDamageByBlockEvent(CraftEventFactory.blockDamage, entity.getBukkitEntity(), DamageCause.LAVA, modifiers, modifierFunctions)); + EntityDamageEvent event = (new EntityDamageByBlockEvent(CraftEventFactory.blockDamageRT.get(), entity.getBukkitEntity(), DamageCause.LAVA, modifiers, modifierFunctions)); // Folia - region threading event.setCancelled(cancelled); - Block damager = CraftEventFactory.blockDamage; - CraftEventFactory.blockDamage = null; // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call + Block damager = CraftEventFactory.blockDamageRT.get(); + CraftEventFactory.blockDamageRT.set(null); // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call // Folia - region threading CraftEventFactory.callEvent(event); - CraftEventFactory.blockDamage = damager; // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause + CraftEventFactory.blockDamageRT.set(damager); // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause // Folia - region threading if (!event.isCancelled()) { event.getEntity().setLastDamageCause(event); @@ -1038,9 +1038,9 @@ public class CraftEventFactory { entity.lastDamageCancelled = true; // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Keep track if the event was canceled } return event; - } else if (CraftEventFactory.blockDamage != null) { + } else if (CraftEventFactory.blockDamageRT.get() != null) { // Folia - region threading DamageCause cause = null; - Block damager = CraftEventFactory.blockDamage; + Block damager = CraftEventFactory.blockDamageRT.get(); // Folia - region threading if (source.is(DamageTypes.CACTUS) || source.is(DamageTypes.SWEET_BERRY_BUSH) || source.is(DamageTypes.STALAGMITE) || source.is(DamageTypes.FALLING_STALACTITE) || source.is(DamageTypes.FALLING_ANVIL)) { cause = DamageCause.CONTACT; } else if (source.is(DamageTypes.HOT_FLOOR)) { @@ -1055,9 +1055,9 @@ public class CraftEventFactory { EntityDamageEvent event = new EntityDamageByBlockEvent(damager, entity.getBukkitEntity(), cause, modifiers, modifierFunctions); event.setCancelled(cancelled); - CraftEventFactory.blockDamage = null; // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call + CraftEventFactory.blockDamageRT.set(null); // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call // Folia - region threading CraftEventFactory.callEvent(event); - CraftEventFactory.blockDamage = damager; // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause + CraftEventFactory.blockDamageRT.set(damager); // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause // Folia - region threading if (!event.isCancelled()) { event.getEntity().setLastDamageCause(event); @@ -1065,10 +1065,10 @@ public class CraftEventFactory { entity.lastDamageCancelled = true; // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Keep track if the event was canceled } return event; - } else if (CraftEventFactory.entityDamage != null) { + } else if (CraftEventFactory.entityDamageRT.get() != null) { // Folia - region threading DamageCause cause = null; - CraftEntity damager = CraftEventFactory.entityDamage.getBukkitEntity(); - CraftEventFactory.entityDamage = null; + CraftEntity damager = CraftEventFactory.entityDamageRT.get().getBukkitEntity(); // Folia - region threading + CraftEventFactory.entityDamageRT.set(null); // Folia - region threading if (source.is(DamageTypes.FALLING_STALACTITE) || source.is(DamageTypes.FALLING_BLOCK) || source.is(DamageTypes.FALLING_ANVIL)) { cause = DamageCause.FALLING_BLOCK; } else if (damager instanceof LightningStrike) { diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java index cdefb2025eedea7e204d70d568adaf1c1ec4c03c..9136fb30db749737e9f189d0901024fcad02e402 100644 --- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java @@ -533,6 +533,7 @@ public class CraftScheduler implements BukkitScheduler { } protected CraftTask handle(final CraftTask task, final long delay) { // Paper + if (true) throw new UnsupportedOperationException(); // Folia - region threading // Paper start if (!this.isAsyncScheduler && !task.isSync()) { this.asyncScheduler.handle(task, delay); diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java index 64c50c52c11214740de7903e5592b8b6b2c170b3..9f5e8a21de3785721897cf8cf75b17ce3968cb7e 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java @@ -373,6 +373,12 @@ public final class CraftMagicNumbers implements UnsafeValues { String minimumVersion = MinecraftServer.getServer().server.minimumAPI; int minimumIndex = CraftMagicNumbers.SUPPORTED_API.indexOf(minimumVersion); + // Folia start - block plugins not marked as supported + if (!pdf.isFoliaSupported()) { + throw new InvalidPluginException("Plugin " + pdf.getFullName() + " is not marked as supporting regionised multithreading"); + } + // Folia end - block plugins not marked as supported + if (pdf.getAPIVersion() != null) { int pluginIndex = CraftMagicNumbers.SUPPORTED_API.indexOf(pdf.getAPIVersion()); diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java index e881584d38dc354204479863f004e974a0ac6c07..5b85034aeb2445a980f8d6156cea6a483ce97391 100644 --- a/src/main/java/org/spigotmc/ActivationRange.java +++ b/src/main/java/org/spigotmc/ActivationRange.java @@ -65,26 +65,27 @@ public class ActivationRange private static int checkInactiveWakeup(Entity entity) { Level world = entity.level; + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions SpigotWorldConfig config = world.spigotConfig; - long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; + long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions if (entity.activationType == ActivationType.VILLAGER) { - if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) { - world.wakeupInactiveRemainingVillagers--; + if (inactiveFor > config.wakeUpInactiveVillagersEvery && worldData.wakeupInactiveRemainingVillagers > 0) { // Folia - threaded regions + worldData.wakeupInactiveRemainingVillagers--; // Folia - threaded regions return config.wakeUpInactiveVillagersFor; } } else if (entity.activationType == ActivationType.ANIMAL) { - if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) { - world.wakeupInactiveRemainingAnimals--; + if (inactiveFor > config.wakeUpInactiveAnimalsEvery && worldData.wakeupInactiveRemainingAnimals > 0) { // Folia - threaded regions + worldData.wakeupInactiveRemainingAnimals--; // Folia - threaded regions return config.wakeUpInactiveAnimalsFor; } } else if (entity.activationType == ActivationType.FLYING_MONSTER) { - if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) { - world.wakeupInactiveRemainingFlying--; + if (inactiveFor > config.wakeUpInactiveFlyingEvery && worldData.wakeupInactiveRemainingFlying > 0) { // Folia - threaded regions + worldData.wakeupInactiveRemainingFlying--; // Folia - threaded regions return config.wakeUpInactiveFlyingFor; } } else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) { - if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) { - world.wakeupInactiveRemainingMonsters--; + if (inactiveFor > config.wakeUpInactiveMonstersEvery && worldData.wakeupInactiveRemainingMonsters > 0) { // Folia - threaded regions + worldData.wakeupInactiveRemainingMonsters--; // Folia - threaded regions return config.wakeUpInactiveMonstersFor; } } @@ -174,10 +175,11 @@ public class ActivationRange final int waterActivationRange = world.spigotConfig.waterActivationRange; final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange; final int villagerActivationRange = world.spigotConfig.villagerActivationRange; - world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); - world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); - world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); - world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions + worldData.wakeupInactiveRemainingAnimals = Math.min(worldData.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); // Folia - threaded regions + worldData.wakeupInactiveRemainingVillagers = Math.min(worldData.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); // Folia - threaded regions + worldData.wakeupInactiveRemainingMonsters = Math.min(worldData.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); // Folia - threaded regions + worldData.wakeupInactiveRemainingFlying = Math.min(worldData.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); // Folia - threaded regions final ServerChunkCache chunkProvider = (ServerChunkCache) world.getChunkSource(); // Paper end @@ -191,9 +193,9 @@ public class ActivationRange // Paper end maxRange = Math.min( ( world.spigotConfig.simulationDistance << 4 ) - 8, maxRange ); - for ( Player player : world.players() ) + for ( Player player : world.getLocalPlayers() ) // Folia - region threading { - player.activatedTick = MinecraftServer.currentTick; + player.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading if ( world.spigotConfig.ignoreSpectatorActivation && player.isSpectator() ) { continue; @@ -212,10 +214,11 @@ public class ActivationRange // Paper end // Paper start - java.util.List entities = world.getEntities((Entity)null, maxBB, (e) -> !(e instanceof net.minecraft.world.entity.Marker)); // Don't tick markers + java.util.List entities = new java.util.ArrayList<>(); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later + ((net.minecraft.server.level.ServerLevel)world).getEntityLookup().getEntities((Entity)null, maxBB, entities, (e) -> !(e instanceof net.minecraft.world.entity.Marker)); // Paper - optimise this call // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later for (int i = 0; i < entities.size(); i++) { Entity entity = entities.get(i); - ActivationRange.activateEntity(entity); + if (io.papermc.paper.util.TickThread.isTickThreadFor(entity)) ActivationRange.activateEntity(entity); // Folia - region ticking } // Paper end } @@ -229,16 +232,16 @@ public class ActivationRange */ private static void activateEntity(Entity entity) { - if ( MinecraftServer.currentTick > entity.activatedTick ) + if ( io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() > entity.activatedTick ) // Folia - threaded regions { if ( entity.defaultActivationState ) { - entity.activatedTick = MinecraftServer.currentTick; + entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions return; } if ( entity.activationType.boundingBox.intersects( entity.getBoundingBox() ) ) { - entity.activatedTick = MinecraftServer.currentTick; + entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions } } } @@ -261,10 +264,10 @@ public class ActivationRange if (entity.remainingFireTicks > 0) { return 2; } - if (entity.activatedImmunityTick >= MinecraftServer.currentTick) { + if (entity.activatedImmunityTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) { // Folia - threaded regions return 1; } - long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; + long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions // Paper end // quick checks. if ( (entity.activationType != ActivationType.WATER && entity.wasTouchingWater && entity.isPushedByFluid()) ) // Paper @@ -387,19 +390,19 @@ public class ActivationRange } // Paper end - boolean isActive = entity.activatedTick >= MinecraftServer.currentTick; + boolean isActive = entity.activatedTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions entity.isTemporarilyActive = false; // Paper // Should this entity tick? if ( !isActive ) { - if ( ( MinecraftServer.currentTick - entity.activatedTick - 1 ) % 20 == 0 ) + if ( ( io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick - 1 ) % 20 == 0 ) // Folia - threaded regions { // Check immunities every 20 ticks. // Paper start int immunity = checkEntityImmunities(entity); if (immunity >= 0) { - entity.activatedTick = MinecraftServer.currentTick + immunity; + entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + immunity; // Folia - threaded regions } else { entity.isTemporarilyActive = true; } diff --git a/src/main/java/org/spigotmc/SpigotCommand.java b/src/main/java/org/spigotmc/SpigotCommand.java index 3112a8695639c402e9d18710acbc11cff5611e9c..b0249d9b61819d4a259753150df795a84ce1d4ff 100644 --- a/src/main/java/org/spigotmc/SpigotCommand.java +++ b/src/main/java/org/spigotmc/SpigotCommand.java @@ -29,6 +29,7 @@ public class SpigotCommand extends Command { Command.broadcastCommandMessage(sender, ChatColor.RED + "Please note that this command is not supported and may cause issues."); Command.broadcastCommandMessage(sender, ChatColor.RED + "If you encounter any issues please use the /stop command to restart your server."); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading MinecraftServer console = MinecraftServer.getServer(); org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); for (ServerLevel world : console.getAllLevels()) { @@ -37,6 +38,7 @@ public class SpigotCommand extends Command { console.server.reloadCount++; Command.broadcastCommandMessage(sender, ChatColor.GREEN + "Reload complete."); + }); // Folia - region threading } return true; diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java index 612c3169c3463d702b85975e1db79ae6e47d60d0..6f77134ba451e7bd6bcba1000134ce8a2c762979 100644 --- a/src/main/java/org/spigotmc/SpigotConfig.java +++ b/src/main/java/org/spigotmc/SpigotConfig.java @@ -228,7 +228,7 @@ public class SpigotConfig SpigotConfig.restartOnCrash = SpigotConfig.getBoolean( "settings.restart-on-crash", SpigotConfig.restartOnCrash ); SpigotConfig.restartScript = SpigotConfig.getString( "settings.restart-script", SpigotConfig.restartScript ); SpigotConfig.restartMessage = SpigotConfig.transform( SpigotConfig.getString( "messages.restart", "Server is restarting" ) ); - SpigotConfig.commands.put( "restart", new RestartCommand( "restart" ) ); + //SpigotConfig.commands.put( "restart", new RestartCommand( "restart" ) ); // Folia - region threading // WatchdogThread.doStart( timeoutTime, restartOnCrash ); // Paper - moved to after paper config initialization } @@ -283,7 +283,7 @@ public class SpigotConfig private static void tpsCommand() { - SpigotConfig.commands.put( "tps", new TicksPerSecondCommand( "tps" ) ); + //SpigotConfig.commands.put( "tps", new TicksPerSecondCommand( "tps" ) ); // Folia - region threading } public static int playerSample; diff --git a/src/main/java/org/spigotmc/SpigotWorldConfig.java b/src/main/java/org/spigotmc/SpigotWorldConfig.java index 5503ad6a93d331771a0e92c0da6adedf2ac81aff..a06408a10cb24c203cfc25f25ccd37ac1a587a1a 100644 --- a/src/main/java/org/spigotmc/SpigotWorldConfig.java +++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java @@ -425,7 +425,7 @@ public class SpigotWorldConfig this.otherMultiplier = (float) this.getDouble( "hunger.other-multiplier", 0.0 ); } - public int currentPrimedTnt = 0; + //public int currentPrimedTnt = 0; // Folia - region threading - moved to regionised world data public int maxTntTicksPerTick; private void maxTntPerTick() { if ( SpigotConfig.version < 7 )