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/ImproveReentrantLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/ImproveReentrantLock.java new file mode 100644 index 0000000000000000000000000000000000000000..9df9881396f4a69b51acaae562b12b8ce0a48443 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/ImproveReentrantLock.java @@ -0,0 +1,139 @@ +package ca.spottedleaf.concurrentutil.lock; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import java.lang.invoke.VarHandle; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.AbstractQueuedSynchronizer; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; + +/** + * Implementation of {@link Lock} that should outperform {@link java.util.concurrent.locks.ReentrantLock}. + * The lock is considered a non-fair lock, as specified by {@link java.util.concurrent.locks.ReentrantLock}, + * and additionally does not support the creation of Conditions. + * + *

+ * Specifically, this implementation is careful to avoid synchronisation penalties when multi-acquiring and + * multi-releasing locks from the same thread, and additionally avoids unnecessary synchronisation penalties + * when releasing the lock. + *

+ */ +public class ImproveReentrantLock implements Lock { + + private final InternalLock lock = new InternalLock(); + + private static final class InternalLock extends AbstractQueuedSynchronizer { + + private volatile Thread owner; + private static final VarHandle OWNER_HANDLE = ConcurrentUtil.getVarHandle(InternalLock.class, "owner", Thread.class); + private int count; + + private Thread getOwnerPlain() { + return (Thread)OWNER_HANDLE.get(this); + } + + private Thread getOwnerVolatile() { + return (Thread)OWNER_HANDLE.getVolatile(this); + } + + private void setOwnerRelease(final Thread to) { + OWNER_HANDLE.setRelease(this, to); + } + + private void setOwnerVolatile(final Thread to) { + OWNER_HANDLE.setVolatile(this, to); + } + + private Thread compareAndExchangeOwnerVolatile(final Thread expect, final Thread update) { + return (Thread)OWNER_HANDLE.compareAndExchange(this, expect, update); + } + + @Override + protected final boolean tryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + final Thread owner = this.getOwnerVolatile(); + + // When trying to blind acquire the lock, using just compare and exchange is faster + // than reading the owner field first - but comes at the cost of performing the compare and exchange + // even if the current thread owns the lock + if ((owner == null && null == this.compareAndExchangeOwnerVolatile(null, current)) || owner == current) { + this.count += acquires; + return true; + } + + return false; + } + + @Override + protected final boolean tryRelease(int releases) { + if (this.getOwnerPlain() == Thread.currentThread()) { + final int newCount = this.count -= releases; + if (newCount == 0) { + // When the caller, which is release(), attempts to signal the next node, it will use volatile + // to retrieve the node and status. + // Let's say that we have written this field null as release, and then checked for a next node + // using volatile and then determined there are no waiters. + // While a call to tryAcquire() can fail for another thread since the write may not + // publish yet, once the thread adds itself to the waiters list it will synchronise with + // the write to the field, since the volatile write to put the thread on the waiter list + // will synchronise with the volatile read we did earlier to check for any + // waiters. + this.setOwnerRelease(null); + return true; + } + return false; + } + throw new IllegalMonitorStateException(); + } + } + + /** + * Returns the thread that owns the lock, or returns {@code null} if there is no such thread. + */ + public Thread getLockOwner() { + return this.lock.getOwnerVolatile(); + } + + /** + * Returns whether the current thread owns the lock. + */ + public boolean isHeldByCurrentThread() { + return this.lock.getOwnerPlain() == Thread.currentThread(); + } + + @Override + public void lock() { + this.lock.acquire(1); + } + + @Override + public void lockInterruptibly() throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + this.lock.acquireInterruptibly(1); + } + + @Override + public boolean tryLock() { + return this.lock.tryAcquire(1); + } + + @Override + public boolean tryLock(final long time, final TimeUnit unit) throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + return this.lock.tryAcquire(1) || this.lock.tryAcquireNanos(1, unit.toNanos(time)); + } + + @Override + public void unlock() { + this.lock.release(1); + } + + @Override + public Condition newCondition() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/RBLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/RBLock.java new file mode 100644 index 0000000000000000000000000000000000000000..793a7326141b7d83395585b3d32b0a7e8a6238a7 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/RBLock.java @@ -0,0 +1,303 @@ +package ca.spottedleaf.concurrentutil.lock; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import java.lang.invoke.VarHandle; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.LockSupport; + +// ReentrantBiasedLock +public final class RBLock implements Lock { + + private volatile LockWaiter owner; + private static final VarHandle OWNER_HANDLE = ConcurrentUtil.getVarHandle(RBLock.class, "owner", LockWaiter.class); + + private volatile LockWaiter tail; + private static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(RBLock.class, "tail", LockWaiter.class); + + public RBLock() { + // we can have the initial state as if it was locked by this thread, then unlocked + final LockWaiter dummy = new LockWaiter(null, LockWaiter.STATE_BIASED, null); + this.setOwnerPlain(dummy); + // release ensures correct publishing + this.setTailRelease(dummy); + } + + private LockWaiter getOwnerVolatile() { + return (LockWaiter)OWNER_HANDLE.getVolatile(this); + } + + private void setOwnerPlain(final LockWaiter value) { + OWNER_HANDLE.set(this, value); + } + + private void setOwnerRelease(final LockWaiter value) { + OWNER_HANDLE.setRelease(this, value); + } + + + + private void setTailOpaque(final LockWaiter newTail) { + TAIL_HANDLE.setOpaque(this, newTail); + } + + private void setTailRelease(final LockWaiter newTail) { + TAIL_HANDLE.setRelease(this, newTail); + } + + private LockWaiter getTailOpaque() { + return (LockWaiter)TAIL_HANDLE.getOpaque(this); + } + + + private void appendWaiter(final LockWaiter waiter) { + // Similar to MultiThreadedQueue#appendList + int failures = 0; + + for (LockWaiter currTail = this.getTailOpaque(), curr = currTail;;) { + /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */ + /* It is likely due to a cache miss caused by another write to the next field */ + final LockWaiter next = curr.getNextVolatile(); + + for (int i = 0; i < failures; ++i) { + Thread.onSpinWait(); + } + + if (next == null) { + final LockWaiter compared = curr.compareAndExchangeNextVolatile(null, waiter); + + if (compared == null) { + /* Added */ + /* Avoid CASing on tail more than we need to */ + /* CAS to avoid setting an out-of-date tail */ + if (this.getTailOpaque() == currTail) { + this.setTailOpaque(waiter); + } + return; + } + + ++failures; + curr = compared; + continue; + } + + if (curr == currTail) { + /* Tail is likely not up-to-date */ + curr = next; + } else { + /* Try to update to tail */ + if (currTail == (currTail = this.getTailOpaque())) { + curr = next; + } else { + curr = currTail; + } + } + } + } + + // required that expected is already appended to the wait chain + private boolean tryAcquireBiased(final LockWaiter expected) { + final LockWaiter owner = this.getOwnerVolatile(); + if (owner.getNextVolatile() == expected && owner.getStateVolatile() == LockWaiter.STATE_BIASED) { + this.setOwnerRelease(expected); + return true; + } + return false; + } + + @Override + public void lock() { + final Thread currThread = Thread.currentThread(); + final LockWaiter owner = this.getOwnerVolatile(); + + // try to fast acquire + + final LockWaiter acquireObj; + boolean needAppend = true; + + if (owner.getNextVolatile() != null) { + // unlikely we are able to fast acquire + acquireObj = new LockWaiter(currThread, 1, null); + } else { + // may be able to fast acquire the lock + if (owner.owner == currThread) { + final int oldState = owner.incrementState(); + if (oldState == LockWaiter.STATE_BIASED) { + // in this case, we may not have the lock. + final LockWaiter next = owner.getNextVolatile(); + if (next == null) { + // we win the lock + return; + } else { + // we have incremented the state, which means any tryAcquireBiased() will fail. + // The next waiter may be waiting for us, so we need to re-set our state and then + // try to push the lock to them. + // We cannot simply claim ownership of the lock, since we don't know if the next waiter saw + // the biased state + owner.setStateRelease(LockWaiter.STATE_BIASED); + LockSupport.unpark(next.owner); + + acquireObj = new LockWaiter(currThread, 1, null); + // fall through to slower lock logic + } + } else { + // we already have the lock + return; + } + } else { + acquireObj = new LockWaiter(currThread, 1, null); + if (owner.getStateVolatile() == LockWaiter.STATE_BIASED) { + // we may be able to quickly acquire the lock + if (owner.getNextVolatile() == null && null == owner.compareAndExchangeNextVolatile(null, acquireObj)) { + if (owner.getStateVolatile() == LockWaiter.STATE_BIASED) { + this.setOwnerRelease(acquireObj); + return; + } else { + needAppend = false; + // we failed to acquire, but we can block instead - we did CAS to the next immediate owner + } + } + } // else: fall through to append and wait code + } + } + + if (needAppend) { + this.appendWaiter(acquireObj); // append to end of waiters + } + + // failed to fast acquire, so now we may need to block + final int spinAttempts = 10; + for (int i = 0; i < spinAttempts; ++i) { + for (int k = 0; k <= i; ++i) { + Thread.onSpinWait(); + } + if (this.tryAcquireBiased(acquireObj)) { + // acquired + return; + } + } + + // slow acquire + while (!this.tryAcquireBiased(acquireObj)) { + LockSupport.park(this); + } + } + + /** + * {@inheritDoc} + * @throws IllegalMonitorStateException If the current thread does not own the lock. + */ + @Override + public void unlock() { + final LockWaiter owner = this.getOwnerVolatile(); + + final int oldState; + if (owner.owner != Thread.currentThread() || (oldState = owner.getStatePlain()) <= 0) { + throw new IllegalMonitorStateException(); + } + + owner.setStateRelease(oldState - 1); + + if (oldState != 1) { + return; + } + + final LockWaiter next = owner.getNextVolatile(); + + if (next == null) { + // we can leave the lock in biased state, which will save a CAS + return; + } + + // we have TWO cases: + // waiter saw the lock in biased state + // waiter did not see the lock in biased state + // the problem is that if the waiter saw the lock in the biased state, then it now owns the lock. but if it did not, + // then we still own the lock. + + // However, by unparking always, the waiter will try to acquire the biased lock from us. + LockSupport.unpark(next.owner); + } + + @Override + public void lockInterruptibly() throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean tryLock() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public Condition newCondition() { + throw new UnsupportedOperationException(); + } + + static final class LockWaiter { + + static final int STATE_BIASED = 0; + + private volatile LockWaiter next; + private volatile int state; + private Thread owner; + + private static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LockWaiter.class, "next", LockWaiter.class); + private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(LockWaiter.class, "state", int.class); + + + private LockWaiter compareAndExchangeNextVolatile(final LockWaiter expect, final LockWaiter update) { + return (LockWaiter)NEXT_HANDLE.compareAndExchange((LockWaiter)this, expect, update); + } + + private void setNextPlain(final LockWaiter next) { + NEXT_HANDLE.set((LockWaiter)this, next); + } + + private LockWaiter getNextOpaque() { + return (LockWaiter)NEXT_HANDLE.getOpaque((LockWaiter)this); + } + + private LockWaiter getNextVolatile() { + return (LockWaiter)NEXT_HANDLE.getVolatile((LockWaiter)this); + } + + + + private int getStatePlain() { + return (int)STATE_HANDLE.get((LockWaiter)this); + } + + private int getStateVolatile() { + return (int)STATE_HANDLE.getVolatile((LockWaiter)this); + } + + private void setStatePlain(final int value) { + STATE_HANDLE.set((LockWaiter)this, value); + } + + private void setStateRelease(final int value) { + STATE_HANDLE.setRelease((LockWaiter)this, value); + } + + public LockWaiter(final Thread owner, final int initialState, final LockWaiter next) { + this.owner = owner; + this.setStatePlain(initialState); + this.setNextPlain(next); + } + + public int incrementState() { + final int old = this.getStatePlain(); + // Technically, we DO NOT need release for old != BIASED. But we care about optimising only for x86, + // which is a simple MOV for everything but volatile. + this.setStateRelease(old + 1); + return old; + } + } +} 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..f579ad58ea7db20d6d7b89abbab3a4dfadaaeaee --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java @@ -0,0 +1,534 @@ +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.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; + } + + /** + * 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/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java index 4b002e8b75d117b726b0de274a76d3596fce015b..897cb94abf7b53da8ba7cda5135b6580aa2d9824 100644 --- a/src/main/java/com/destroystokyo/paper/Metrics.java +++ b/src/main/java/com/destroystokyo/paper/Metrics.java @@ -593,7 +593,7 @@ public class Metrics { boolean logFailedRequests = config.getBoolean("logFailedRequests", false); // Only start Metrics, if it's enabled in the config if (config.getBoolean("enabled", true)) { - Metrics metrics = new Metrics("Paper", serverUUID, logFailedRequests, Bukkit.getLogger()); + Metrics metrics = new Metrics("Tuinity", serverUUID, logFailedRequests, Bukkit.getLogger()); // Tuinity - we have our own bstats page metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { String minecraftVersion = Bukkit.getVersion(); @@ -611,7 +611,7 @@ public class Metrics { } else { paperVersion = "unknown"; } - metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> paperVersion)); + metrics.addCustomChart(new Metrics.SimplePie("tuinity_version", () -> paperVersion)); // Tuinity - we have our own bstats page metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { Map> map = new HashMap<>(); diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java index 4f3670b2bdb8b1b252e9f074a6af56a018a8c465..5c1ea572a97b130c3ff77624189b4acf3e9e9ece 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/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java index 309fe1162db195c7c3c94d785d6aa2700e42b08a..27f8c9b1c56cbf9af400a9ae15c2076a2db8b284 100644 --- a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java +++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java @@ -97,7 +97,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() { @@ -177,7 +177,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/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java index 6df1948b1204a7288ecb7238b6fc2a733f7d25b3..6a413abc67aa4dcbab64231be3eb13446d5cc820 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,6 +101,9 @@ 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) { @@ -109,19 +115,19 @@ public final class ChunkSystem { } 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().addEntityTickingChunks(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/RegionisedPlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/system/RegionisedPlayerChunkLoader.java index cb170d1039fd9dadfbc27da0b181c00742e72025..5cccfcf45b3c3cdfdebdf47dc674934441cc0c4c 100644 --- a/src/main/java/io/papermc/paper/chunk/system/RegionisedPlayerChunkLoader.java +++ b/src/main/java/io/papermc/paper/chunk/system/RegionisedPlayerChunkLoader.java @@ -231,14 +231,14 @@ public class RegionisedPlayerChunkLoader { public void tick() { TickThread.ensureTickThread("Cannot tick player chunk loader async"); - for (final ServerPlayer player : this.world.players()) { + for (final ServerPlayer player : this.world.getLocalPlayers()) { // Folia - region threding player.chunkLoader.update(); } } public void tickMidTick() { final long time = System.nanoTime(); - for (final ServerPlayer player : this.world.players()) { + for (final ServerPlayer player : this.world.getLocalPlayers()) { // Folia - region threading player.chunkLoader.midTickUpdate(time); } } 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..687fc3e7ada3da9c5b938b0ffb9e8bcf90c2d1c7 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,6 +394,8 @@ 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; @@ -407,6 +418,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 +835,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..309b45885edc1400ae5a97cac7e5e5a19d73be0c 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.RegionisedServer; +import io.papermc.paper.threadedregions.ThreadedRegioniser; +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,201 @@ 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; + // 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 { + 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<>(); + + // 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<>(); + + 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; + } + ); + } + + // add them all + into.specialCaseUnload.addAll(this.specialCaseUnload); } - final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); + 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 + } - if (saveTickCompare != 0) { - return saveTickCompare; + 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); + } + } + + for (final NewChunkHolder special : this.specialCaseUnload) { + final int regionCoordinateX = CoordinateUtils.getChunkX(special.chunkX) >> chunkToRegionShift; + final int regionCoordinateZ = CoordinateUtils.getChunkZ(special.chunkZ) >> chunkToRegionShift; + + // can never be null, since this chunk holder is loaded + regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)).specialCaseUnload.add(special); + } } + } - final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); - final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); + private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() { + final ThreadedRegioniser.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); - if (coord1 == coord2) { - throw new IllegalStateException("Duplicate chunkholder in auto save queue"); + if (region == null) { + return null; } - return Long.compare(coord1, coord2); - }); + 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().getHolderManagerRegionData(); + } + + // MUST hold ticket lock + private ChunkHolderManager.HolderManagerRegionData getDataFor(final long key) { + return this.getDataFor(CoordinateUtils.getChunkX(key), CoordinateUtils.getChunkZ(key)); + } + + // 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 ThreadedRegioniser.ThreadedRegion region + = this.world.regioniser.getRegionAtUnsynchronised(chunkX, chunkZ); + + if (region == null) { + return null; + } + + return region.getData().getHolderManagerRegionData(); + } + // Folia end - region threading + public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { this.world = world; @@ -129,8 +296,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 +312,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 +336,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 = RegionisedServer.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 = RegionisedServer.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 +377,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 +398,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 +409,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 +447,7 @@ public final class ChunkHolderManager { } } } - if (flush) { + if (last && flush) { // Folia - region threading RegionFileIOThread.flush(); } if (logProgress) { @@ -290,18 +481,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 +501,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 +521,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(); @@ -377,10 +580,27 @@ public final class ChunkHolderManager { 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); + final boolean addToSpecial = holder == null; + if (addToSpecial) { + // 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)); + } + + final ChunkHolderManager.HolderManagerRegionData targetData = this.getDataFor(chunk); + if (addToSpecial) { + // no guarantee checkUnload is called for this chunk holder - by adding to the special case unload, + // the unload chunks call will perform it + targetData.specialCaseUnload.add(holder); + } + // 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 +612,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); } @@ -439,35 +659,43 @@ public final class ChunkHolderManager { return false; } + final ChunkHolderManager.HolderManagerRegionData currRegionData = this.getCurrentRegionData(); // Folia - region threading + this.ticketLock.lock(); try { - final SortedArraySet> ticketsAtChunk = this.tickets.get(chunk); + // Folia start - region threading + final ChunkHolderManager.HolderManagerRegionData targetData = this.getDataFor(chunk); + + final boolean sameRegion = currRegionData == targetData; + + 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; } if (ticketsAtChunk.isEmpty()) { - this.tickets.remove(chunk); + targetData.tickets.remove(chunk); // Folia - region threading } final int newLevel = getTicketLevelAt(ticketsAtChunk); 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 } } } @@ -476,6 +704,13 @@ public final class ChunkHolderManager { this.updateTicketLevel(chunk, newLevel); } + // Folia start - region threading + // if we're not the target region, we should not change the ticket levels while the target region may be ticking + if (!sameRegion && newLevel > level) { + this.addTicketAtLevel(TicketType.UNKNOWN, chunk, level, new ChunkPos(chunk)); + } + // Folia end - region threading + return true; } finally { this.ticketLock.unlock(); @@ -516,24 +751,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 +790,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 +1042,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 ThreadedRegioniser.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,23 +1115,42 @@ 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(); try { this.taskScheduler.schedulingLock.lock(); try { + // Folia start - region threading + for (final NewChunkHolder special : currentData.specialCaseUnload) { + special.checkUnload(); + } + currentData.specialCaseUnload.clear(); + // Folia end - region threading if (this.unloadQueue.isEmpty()) { return; } // 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; @@ -1193,7 +1488,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 +1504,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 +1554,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 +1573,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..93b666893a9755e426701f5c2849fc0fb2026bb7 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().regionisedServer.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().regionisedServer.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().regionisedServer.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 92154550b41b2e1d03deb1271b71bb3baa735e0a..bc97ad0ae019edb52e189e44d0d698973c8792a0 100644 --- a/src/main/java/io/papermc/paper/command/PaperCommand.java +++ b/src/main/java/io/papermc/paper/command/PaperCommand.java @@ -50,7 +50,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 d31b5ed47cffc61c90c926a0cd2005b72ebddfc5..9b9c5bda073914a0588d4a6c8b584e5ce23468d8 100644 --- a/src/main/java/io/papermc/paper/command/PaperCommands.java +++ b/src/main/java/io/papermc/paper/command/PaperCommands.java @@ -17,7 +17,10 @@ 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("tpa", new io.papermc.paper.threadedregions.commands.CommandsTPA()); // Folia - region threading + COMMANDS.put("tpaaccept", new io.papermc.paper.threadedregions.commands.CommandsTPAAccept()); // Folia - region threading + COMMANDS.put("tpadeny", new io.papermc.paper.threadedregions.commands.CommandsTPADeny()); // Folia - region threading + 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..6c24e8567d303db35328fe4f0a7b05df16f3590a 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.RegionisedServer.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..0f641ac581243db55a667ad8bc5d1110206b389e 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.RegionisedServer.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 4532f3a0d74feae0a1249b53e1bfbc18a8808b32..f06681b3e66234b8805e6aa2d26fd535fbdb0aff 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))); 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..70c3accbab4e69268435c6f4fb13d29c7662283d --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java @@ -0,0 +1,112 @@ +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.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(); + + ThreadedRegioniser.ThreadedRegion shuttingDown; + + public RegionShutdownThread(final String name) { + super(name); + this.setUncaughtExceptionHandler((thread, thr) -> { + LOGGER.error("Error shutting down server", thr); + }); + } + + static ThreadedRegioniser.ThreadedRegion getRegion() { + final Thread currentThread = Thread.currentThread(); + if (currentThread instanceof RegionShutdownThread shutdownThread) { + return shutdownThread.shuttingDown; + } + return null; + } + + + static RegionisedWorldData 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 saveRegionChunks(final ThreadedRegioniser.ThreadedRegion region, + final boolean first, final boolean last) { + final ChunkPos center = region.getCenterChunk(); + LOGGER.info("Saving chunks around region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'"); + try { + this.shuttingDown = region; + region.regioniser.world.chunkTaskScheduler.chunkHolderManager.close(true, true, first, 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 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.warn("Scheduler halted"); + } else { + LOGGER.warn("Scheduler did not terminate within 60s, proceeding with shutdown anyways"); + } + + MinecraftServer.getServer().stopServer(); // stop part 1: most logic, kicking players, plugins, etc + 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 ThreadedRegioniser.ThreadedRegion region = regions.get(i); + this.saveRegionChunks(region, i == 0, (i + 1) == len); + } + + if (regions.isEmpty()) { + // still need to halt the chunk system + this.haltWorldNoRegions(world); + } + + this.saveLevelData(world); + } + 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/RegionisedData.java b/src/main/java/io/papermc/paper/threadedregions/RegionisedData.java new file mode 100644 index 0000000000000000000000000000000000000000..3549e5f3359f38b207e189d89595442018c9dfa2 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionisedData.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 RegionisedData} 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 RegionisedData} 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 RegionisedData}'s world. Consider the usages of {@code RegionisedData} 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 RegionisedData entityTickLists =
+ *                 new RegionisedData<>(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 RegionisedData 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 RegionisedData object
+ *             // is not instantiated per world, but rather globally.
+ *             public final RegionisedData tickTimes =
+ *                  new RegionisedData<>(null, () -> new TickTimes(), ...);
+ *         }
+ *         }
+ *     
+ * In general, it is advised that if a RegionisedData object is instantiated per world, that world checking + * is enabled for it by passing the world to the constructor. + *

+ */ +public final class RegionisedData { + + 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 RegionisedData(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 RegionisedData}'s world is not {@code null}, + * and the current ticking region's world does not match this {@code RegionisedData}'s world. + */ + public @Nullable T get() { + final ThreadedRegioniser.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().getOrCreateRegionisedData(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/RegionisedServer.java b/src/main/java/io/papermc/paper/threadedregions/RegionisedServer.java new file mode 100644 index 0000000000000000000000000000000000000000..269c051e20cd07e692c624a873e4ee2b5ae5589a --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionisedServer.java @@ -0,0 +1,366 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; +import com.mojang.authlib.GameProfile; +import com.mojang.logging.LogUtils; +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.network.protocol.status.ServerStatus; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.dedicated.DedicatedServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.network.ServerLoginPacketListenerImpl; +import net.minecraft.server.players.PlayerList; +import net.minecraft.util.Mth; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import org.slf4j.Logger; +import java.util.ArrayList; +import java.util.Arrays; +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 RegionisedServer { + + private static final Logger LOGGER = LogUtils.getLogger(); + private static final RegionisedServer INSTANCE = new RegionisedServer(); + + public final RegionisedTaskQueue taskQueue = new RegionisedTaskQueue(); + + 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 RegionisedServer 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() { + 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 ThreadedRegioniser.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 RegionisedServer server; + + private final AtomicBoolean scheduled = new AtomicBoolean(); + private final AtomicBoolean ticking = new AtomicBoolean(); + + public GlobalTickTickHandle(final RegionisedServer 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; + // 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 ServerStatus status = mcServer.getStatus(); + final PlayerList playerList = mcServer.getPlayerList(); + + final long i = 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 (i - this.lastServerStatus >= 5000000000L) { + this.lastServerStatus = i; + List players = new ArrayList<>(playerList.players); + ServerStatus.Players newPlayers = new ServerStatus.Players(mcServer.getMaxPlayers(), players.size()); + + if (!mcServer.hidesOnlinePlayers()) { + GameProfile[] agameprofile = new GameProfile[Math.min(players.size(), org.spigotmc.SpigotConfig.playerSample)]; // Paper + int j = Mth.nextInt(new LegacyRandomSource(i), 0, players.size() - agameprofile.length); + + for (int k = 0; k < agameprofile.length; ++k) { + ServerPlayer entityplayer = (ServerPlayer) players.get(j + k); + + if (entityplayer.allowsListing()) { + agameprofile[k] = entityplayer.getGameProfile(); + } else { + agameprofile[k] = MinecraftServer.ANONYMOUS_PLAYER_PROFILE; + } + } + + Collections.shuffle(Arrays.asList(agameprofile)); + newPlayers.setSample(agameprofile); + } + // TODO make players field volatile + status.setPlayers(newPlayers); + } + } + + 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/RegionisedTaskQueue.java b/src/main/java/io/papermc/paper/threadedregions/RegionisedTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..c13237edb7323fa747d260375f626a5c9979b004 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionisedTaskQueue.java @@ -0,0 +1,742 @@ +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.io.RegionFileIOThread; +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.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; + +public final class RegionisedTaskQueue { + + 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 ThreadedRegioniser regioniser = this.world.regioniser; + final ThreadedRegioniser.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 ThreadedRegioniser regioniser, + final Long2ReferenceOpenHashMap> into) { + this.tickTaskQueue.split( + false, regioniser, into + ); + this.chunkQueue.split( + true, regioniser, into + ); + } + + public void drainTasks() { + 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); + } + + 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 ThreadedRegioniser 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 ThreadedRegioniser.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/RegionisedWorldData.java b/src/main/java/io/papermc/paper/threadedregions/RegionisedWorldData.java new file mode 100644 index 0000000000000000000000000000000000000000..8317638bdb40764c389a68ced176e6d334eeb599 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionisedWorldData.java @@ -0,0 +1,664 @@ +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.ChunkHolder; +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.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.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.gameevent.GameEvent; +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.phys.Vec3; +import net.minecraft.world.ticks.LevelTicks; +import org.bukkit.craftbukkit.block.CraftBlockState; +import org.bukkit.craftbukkit.util.UnsafeList; +import org.bukkit.entity.SpawnCategory; +import org.slf4j.Logger; + +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.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public final class RegionisedWorldData { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public static final RegionisedData.RegioniserCallback REGION_CALLBACK = new RegionisedData.RegioniserCallback<>() { + @Override + public void merge(final RegionisedWorldData from, final RegionisedWorldData into, final long fromTickOffset) { + // connections + for (final Connection conn : from.connections) { + into.connections.add(conn); + } + // time + final long fromRedstoneTimeOffset = from.redstoneTime - into.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()); + } + for (final ChunkHolder holder : from.needsChangeBroadcasting) { + into.needsChangeBroadcasting.add(holder); + } + // 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 RegionisedWorldData 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 RegionisedWorldData 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 RegionisedWorldData 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 RegionisedWorldData 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 RegionisedWorldData 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 RegionisedWorldData 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 RegionisedWorldData regionisedWorldData : dataSet) { + regionisedWorldData.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); + } + + for (final ChunkHolder holder : from.needsChangeBroadcasting) { + final ChunkPos pos = holder.pos; + + regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) + .needsChangeBroadcasting.add(holder); + } + // 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 RegionisedWorldData 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 RegionisedWorldData regionisedWorldData : dataSet) { + regionisedWorldData.catSpawnerNextTick = from.catSpawnerNextTick; + regionisedWorldData.patrolSpawnerNextTick = from.patrolSpawnerNextTick; + regionisedWorldData.phantomSpawnerNextTick = from.phantomSpawnerNextTick; + regionisedWorldData.wanderingTraderTickDelay = from.wanderingTraderTickDelay; + regionisedWorldData.wanderingTraderSpawnChance = from.wanderingTraderSpawnChance; + regionisedWorldData.wanderingTraderSpawnDelay = from.wanderingTraderSpawnDelay; + regionisedWorldData.villageSiegeState = new VillageSiegeState(); // just re set it, as the spawn pos will be invalid + } + } + }; + + public final ServerLevel world; + + private RegionisedServer.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 ReferenceOpenHashSet needsChangeBroadcasting = new ReferenceOpenHashSet<>(); + + // 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 + + // 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 RegionisedWorldData(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 RegionisedServer.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 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); + } + + 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); + } + + 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); + } + } + } + + 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); + } + } + } + + 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 addEntityTickingChunks(final LevelChunk levelChunk) { + this.entityTickingChunks.add(levelChunk); + } + + public void removeEntityTickingChunk(final LevelChunk levelChunk) { + this.entityTickingChunks.remove(levelChunk); + } + + public IteratorSafeOrderedReferenceSet getEntityTickingChunks() { + return this.entityTickingChunks; + } + + public void addChunkHolderNeedsBroadcasting(final ChunkHolder holder) { + this.needsChangeBroadcasting.add(holder); + } + + public void removeChunkHolderNeedsBroadcasting(final ChunkHolder holder) { + this.needsChangeBroadcasting.remove(holder); + } + + public ReferenceOpenHashSet getNeedsChangeBroadcasting() { + return this.needsChangeBroadcasting; + } +} 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/ThreadedRegioniser.java b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegioniser.java new file mode 100644 index 0000000000000000000000000000000000000000..f05546aa9124d4c0e34005f528483bf516e93c20 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegioniser.java @@ -0,0 +1,1187 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +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.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 java.io.FileReader; +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.Consumer; + +public final class ThreadedRegioniser, S extends ThreadedRegioniser.ThreadedRegionSectionData> { + + 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<>(); + */ + + public ThreadedRegioniser(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); + } + } + */ + + 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 need to create it + + // 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_STEADY_STATE) { + regionOfInterest.state = ThreadedRegion.STATE_NOT_READY; + 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_STEADY_STATE; + if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) { + throw new IllegalStateException("Should not happen on region " + this); + } + this.callbacks.onRegionActive(regionOfInterest); + } + } 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(); + } + } + } 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_NOT_READY; + 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) { + region.state = ThreadedRegion.STATE_STEADY_STATE; + 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_STEADY_STATE; + 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_STEADY_STATE; + 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_NOT_READY = 0; + private static final int STATE_STEADY_STATE = 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 ThreadedRegioniser regioniser; + + private final R data; + + private final ReferenceOpenHashSet> mergeIntoLater = new ReferenceOpenHashSet<>(); + private final ReferenceOpenHashSet> expectingMergeFrom = new ReferenceOpenHashSet<>(); + + public ThreadedRegion(final ThreadedRegioniser regioniser) { + this.regioniser = regioniser; + this.id = REGION_ID_GENERATOR.getAndIncrement(); + this.state = STATE_NOT_READY; + 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 ChunkPos getCenterChunk() { + final LongArrayList sections = this.getOwnedSections(); + + sections.sort(null); + + // note: regions always have at least one section + final long middle = sections.getLong(sections.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_NOT_READY: { + this.state = STATE_DEAD; + this.onRemove(false); + return true; + } + case STATE_STEADY_STATE: { + 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() { + this.regioniser.acquireWriteLock(); + try { + if (this.state != STATE_STEADY_STATE) { + return false; + } + + if (!this.mergeIntoLater.isEmpty() || !this.expectingMergeFrom.isEmpty()) { + throw new IllegalStateException("Region " + this + " should not be steady state"); + } + + 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_STEADY_STATE; + } 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 ThreadedRegioniser 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 ThreadedRegioniser 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 ThreadedRegioniser 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 ThreadedRegioniser regioniser, + final int nonEmptyNeighbours) { + this(sectionX, sectionZ, regioniser); + + this.nonEmptyNeighbours = nonEmptyNeighbours; + } + + 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 ThreadedRegioniser 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..e75aac237764c7a9fa0538ddf8d68b1e14de7d49 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java @@ -0,0 +1,544 @@ +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.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()); + return ret; + } + }); + } + + public int getTotalThreadCount() { + return this.scheduler.getThreads().length; + } + + private static void setTickingRegion(final ThreadedRegioniser.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.currentTickingWorldRegionisedData = region.regioniser.world.worldRegionData.get(); + } else { + tickThreadRunner.currentTickingWorldRegionisedData = 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 ThreadedRegioniser.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 RegionisedData#get()} method. + * If this thread is not a TickThread, then returns {@code null}. + */ + public static RegionisedWorldData getCurrentRegionisedWorldData() { + final Thread currThread = Thread.currentThread(); + if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { + return RegionShutdownThread.getWorldData(); + } + return tickThreadRunner.currentTickingWorldRegionisedData; + } + + /** + * 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); + } + + public void setHasTasks(final RegionScheduleHandle region) { + this.scheduler.notifyTasks(region); + } + + public void init() { + this.scheduler.start(); + } + + 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(); + + LOGGER.error("Region #" + (handle.region == null ? -1L : handle.region.id) + " centered at chunk " + center + " 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 ThreadedRegioniser.ThreadedRegion currentTickingRegion; + private RegionisedWorldData currentTickingWorldRegionisedData; + 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); + } + + 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..c17669c1e98cd954643fa3b988c12b4b6c3b174e --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java @@ -0,0 +1,340 @@ +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.AtomicLong; +import java.util.function.BooleanSupplier; + +public final class TickRegions implements ThreadedRegioniser.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 = (2 * tickThreads) / 3; + } + } else { + tickThreads = config.threads; + } + + scheduler = new TickRegionScheduler(tickThreads); + LOGGER.info("Regionised ticking is enabled with " + tickThreads + " tick threads"); + } + + @Override + public TickRegionData createNewData(final ThreadedRegioniser.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 ThreadedRegioniser.ThreadedRegion region) { + // nothing for now + } + + @Override + public void onRegionDestroy(final ThreadedRegioniser.ThreadedRegion region) { + // nothing for now + } + + @Override + public void onRegionActive(final ThreadedRegioniser.ThreadedRegion region) { + final TickRegionData data = region.getData(); + + data.tickHandle.checkInitialSchedule(); + scheduler.scheduleRegion(data.tickHandle); + } + + @Override + public void onRegionInactive(final ThreadedRegioniser.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 ThreadedRegioniser.ThreadedRegionSectionData {} + + public static final class TickRegionData implements ThreadedRegioniser.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 ThreadedRegioniser.ThreadedRegion region; + public final ServerLevel world; + + // generic regionised data + private final Reference2ReferenceOpenHashMap, Object> regionisedData = new Reference2ReferenceOpenHashMap<>(); + + // tick data + private ConcreteRegionTickHandle tickHandle = new ConcreteRegionTickHandle(this, SchedulerThreadPool.DEADLINE_NOT_SET); + + // queue data + private final RegionisedTaskQueue.RegionTaskQueueData taskQueueData; + + // chunk holder manager data + private final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = new ChunkHolderManager.HolderManagerRegionData(); + + private TickRegionData(final ThreadedRegioniser.ThreadedRegion region) { + this.region = region; + this.world = region.regioniser.world; + this.taskQueueData = new RegionisedTaskQueue.RegionTaskQueueData(this.world.taskQueueRegionData); + } + + public RegionisedTaskQueue.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 getOrCreateRegionisedData(final RegionisedData regionisedData) { + T ret = (T)this.regionisedData.get(regionisedData); + + if (ret != null) { + return ret; + } + + ret = regionisedData.createNewValue(); + this.regionisedData.put(regionisedData, ret); + + return ret; + } + + @Override + public void split(final ThreadedRegioniser 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 ThreadedRegioniser.ThreadedRegion region : regions) { + final TickRegionData data = region.getData(); + data.tickHandle.copyDeadlineAndTickCount(this.tickHandle); + } + + // generic regionised data + for (final Iterator, Object>> dataIterator = this.regionisedData.reference2ReferenceEntrySet().fastIterator(); + dataIterator.hasNext();) { + final Reference2ReferenceMap.Entry, Object> regionDataEntry = dataIterator.next(); + final RegionisedData data = regionDataEntry.getKey(); + final Object from = regionDataEntry.getValue(); + + final ReferenceOpenHashSet dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f); + + for (final ThreadedRegioniser.ThreadedRegion region : regions) { + dataSet.add(region.getData().getOrCreateRegionisedData(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 ThreadedRegioniser.ThreadedRegion region = entry.getValue(); + final Object to = region.getData().getOrCreateRegionisedData(data); + + regionToData.put(entry.getLongKey(), to); + } + + ((RegionisedData)data).getCallback().split(from, shift, regionToData, dataSet); + } + + // chunk holder manager data + { + final ReferenceOpenHashSet dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f); + + for (final ThreadedRegioniser.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 ThreadedRegioniser.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 ThreadedRegioniser.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.regionisedData.reference2ReferenceEntrySet().fastIterator(); + iterator.hasNext();) { + final Reference2ReferenceMap.Entry, Object> entry = iterator.next(); + final RegionisedData regionisedData = entry.getKey(); + final Object from = entry.getValue(); + final Object to = into.getData().getOrCreateRegionisedData(regionisedData); + + ((RegionisedData)regionisedData).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(); + } + + @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 RegionisedTaskQueue.RegionTaskQueueData queue = this.region.taskQueueData; + boolean executeChunkTask = true; + boolean executeTickTask = true; + do { + if (executeTickTask) { + executeTickTask = queue.executeTickTask(); + } + if (executeChunkTask) { + executeChunkTask = queue.executeChunkTask(); + } + } while ((executeChunkTask | executeTickTask) && canContinue.getAsBoolean()); + 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..4889ebf6e3eb5901eeac49900c541d2359d71316 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java @@ -0,0 +1,330 @@ +package io.papermc.paper.threadedregions.commands; + +import io.papermc.paper.threadedregions.ThreadedRegioniser; +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 DecimalFormat TWO_DECIMAL_PLACES = new DecimalFormat("#0.00"); + private static final DecimalFormat ONE_DECIMAL_PLACES = new DecimalFormat("#0.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"); + } + + @Override + public boolean testPermissionSilent(final CommandSender target) { + // TODO for now + return true; + } + + 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.format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) + .append(Component.text("% util at ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.format(mspt), CommandUtil.getColourForMSPT(mspt))) + .append(Component.text(" MSPT at ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.format(tps), CommandUtil.getColourForTPS(tps))) + .append(Component.text(" TPS" + (newline ? "\n" : ""), PRIMARY)) + .build(); + } + + private static boolean executeRegion(final CommandSender sender, final String commandLabel, final String[] args) { + final ThreadedRegioniser.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, 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 long currTime = System.nanoTime(); + + final double minTps; + final double medianTps; + final double maxTps; + long totalTime = 0; + double totalUtil = 0.0; + + final DoubleArrayList tpsByRegion = new DoubleArrayList(); + final List reportsByRegion = new ArrayList<>(); + + final int maxThreadCount = TickRegions.getScheduler().getTotalThreadCount(); + + for (final ThreadedRegioniser.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()); + } + + 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 ThreadedRegioniser.ThreadedRegion region = + pair.left(); + + if (report == null) { + // skip regions with no data + continue; + } + + 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 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.format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) + .append(Component.text("% util at ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.format(mspt), CommandUtil.getColourForMSPT(mspt))) + .append(Component.text(" MSPT at ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.format(tps), CommandUtil.getColourForTPS(tps))) + .append(Component.text(" TPS" + ((i + 1) == len ? "" : "\n"), PRIMARY)) + .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.format(totalUtil * 100.0), CommandUtil.getUtilisationColourRegion(totalUtil / (double)maxThreadCount))) + .append(Component.text("% / ", PRIMARY)) + .append(Component.text(ONE_DECIMAL_PLACES.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.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.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.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/commands/CommandsTPA.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandsTPA.java new file mode 100644 index 0000000000000000000000000000000000000000..f2259d295ce613e41097819482b2084dd9c1fdd9 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandsTPA.java @@ -0,0 +1,138 @@ +package io.papermc.paper.threadedregions.commands; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public final class CommandsTPA extends Command { + + public CommandsTPA() { + super("tpa"); + this.setUsage("/ "); + } + + @Override + public boolean testPermissionSilent(final CommandSender target) { + return true; + } + + @Override + public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { + if (!(sender instanceof CraftPlayer playerSender)) { + sender.sendMessage(commandLabel + " only works for players"); + return true; + } + + if (args.length != 1) { + sender.sendMessage(Component.text("Usage: /" + commandLabel + " ", NamedTextColor.DARK_RED)); + return true; + } + + final ServerPlayer target = CommandUtil.getPlayer(args[0]); + + if (target == null) { + sender.sendMessage( + Component.text() + .append(Component.text("Found no such player ", NamedTextColor.DARK_RED)) + .append(Component.text(args[0], NamedTextColor.RED)) + .build() + ); + return true; + } + + if (target == playerSender.getHandle()) { + sender.sendMessage(Component.text("Cannot tpa to yourself!", NamedTextColor.DARK_RED)); + return true; + } + + final String targetName = target.getGameProfile().getName(); + + if (!target.pendingTpas.add(playerSender.getUniqueId())) { + sender.sendMessage( + Component.text() + .append(Component.text("You already have a tpa request to ", NamedTextColor.DARK_RED)) + .append(Component.text(targetName, NamedTextColor.RED)) + .build() + ); + return true; + } + + sender.sendMessage( + Component.text() + .append(Component.text("Sent tpa request to ", NamedTextColor.GRAY)) + .append(Component.text(targetName, NamedTextColor.RED)) + .build() + ); + target.getBukkitEntity().sendMessage( + Component.text() + .append(Component.text(playerSender.getName(), NamedTextColor.RED)) + .append(Component.text(" has requested to teleport to you!\n", NamedTextColor.GRAY)) + .append( + Component.text() + .append(Component.text("Run or click ", NamedTextColor.GRAY)) + .append(Component.text("/tpaaccept ", NamedTextColor.DARK_GREEN)) + .append(Component.text(playerSender.getName(), NamedTextColor.GREEN)) + .append(Component.text(" to accept\n", NamedTextColor.GRAY)) + .build() + .clickEvent(ClickEvent.runCommand("/tpaaccept " + playerSender.getName())) + .hoverEvent( + HoverEvent.showText( + Component.text() + .append(Component.text("Click to accept tpa request from ", NamedTextColor.DARK_GREEN)) + .append(Component.text(playerSender.getName(), NamedTextColor.GREEN)) + ) + ) + ) + .append( + Component.text() + .append(Component.text("Run or click ", NamedTextColor.GRAY)) + .append(Component.text("/tpaaccept ", NamedTextColor.DARK_RED)) + .append(Component.text(playerSender.getName(), NamedTextColor.RED)) + .append(Component.text(" to deny", NamedTextColor.GRAY)) + .build() + .clickEvent(ClickEvent.runCommand("/tpadeny " + playerSender.getName())) + .hoverEvent( + HoverEvent.showText( + Component.text() + .append(Component.text("Click to reject tpa request from ", NamedTextColor.DARK_RED)) + .append(Component.text(playerSender.getName(), NamedTextColor.RED)) + ) + ) + ) + .build() + ); + + return true; + } + + @Override + public List tabComplete(final CommandSender sender, final String alias, + final String[] args) throws IllegalArgumentException { + if (!(sender instanceof CraftPlayer)) { + return new ArrayList<>(); + } + + final List players = MinecraftServer.getServer().getPlayerList().players; + + final Function playerToName = (final ServerPlayer value) -> { + return value.getGameProfile().getName(); + }; + + if (args.length == 0) { + return CommandUtil.getSortedList(players, playerToName); + } else if (args.length == 1) { + return CommandUtil.getSortedList(players, playerToName, args[0]); + } + + return new ArrayList<>(); + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/commands/CommandsTPAAccept.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandsTPAAccept.java new file mode 100644 index 0000000000000000000000000000000000000000..b648d67f3ade11172af4ed76d6d14de7ca39c5d6 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandsTPAAccept.java @@ -0,0 +1,109 @@ +package io.papermc.paper.threadedregions.commands; + +import io.papermc.paper.threadedregions.TeleportUtils; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.event.player.PlayerTeleportEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public final class CommandsTPAAccept extends Command { + + public CommandsTPAAccept() { + super("tpaaccept"); + this.setUsage("/ "); + } + + @Override + public boolean testPermissionSilent(final CommandSender target) { + return true; + } + + @Override + public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { + if (!(sender instanceof CraftPlayer playerSender)) { + sender.sendMessage(commandLabel + " only works for players"); + return true; + } + + if (args.length != 1) { + sender.sendMessage(Component.text("Usage: /" + commandLabel + " ", NamedTextColor.DARK_RED)); + return true; + } + + final ServerPlayer target = CommandUtil.getPlayer(args[0]); + + if (target == null) { + sender.sendMessage( + Component.text() + .append(Component.text("Found no such player ", NamedTextColor.DARK_RED)) + .append(Component.text(args[0], NamedTextColor.RED)) + .build() + ); + return true; + } + + if (!playerSender.getHandle().pendingTpas.remove(target.getUUID())) { + sender.sendMessage( + Component.text() + .append(Component.text("No tpa request to accept from ", NamedTextColor.DARK_RED)) + .append(Component.text(args[0], NamedTextColor.RED)) + .build() + ); + return true; + } + + sender.sendMessage( + Component.text() + .append(Component.text("Accepted tpa request from ", NamedTextColor.GRAY)) + .append(Component.text(target.getGameProfile().getName(), NamedTextColor.GREEN)) + .build() + ); + target.getBukkitEntity().sendMessage( + Component.text() + .append(Component.text(playerSender.getName(), NamedTextColor.GREEN)) + .append(Component.text(" accepted", NamedTextColor.DARK_GREEN)) + .append(Component.text(" your tpa request!", NamedTextColor.GRAY)) + .build() + ); + + TeleportUtils.teleport( + target, true, playerSender.getHandle(), + Float.valueOf(playerSender.getHandle().getYRot()), Float.valueOf(playerSender.getHandle().getXRot()), + Entity.TELEPORT_FLAG_LOAD_CHUNK | Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, + PlayerTeleportEvent.TeleportCause.COMMAND, null + ); + + return true; + } + + @Override + public List tabComplete(final CommandSender sender, final String alias, + final String[] args) throws IllegalArgumentException { + if (!(sender instanceof CraftPlayer playerSender)) { + return new ArrayList<>(); + } + + final List players = MinecraftServer.getServer().getPlayerList().players; + + // The laziest implementation possible. + final Function playerToName = (final ServerPlayer value) -> { + return playerSender.getHandle().pendingTpas.contains(value.getUUID()) ? value.getGameProfile().getName() : null; + }; + + if (args.length == 0) { + return CommandUtil.getSortedList(players, playerToName); + } else if (args.length == 1) { + return CommandUtil.getSortedList(players, playerToName, args[0]); + } + + return new ArrayList<>(); + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/commands/CommandsTPADeny.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandsTPADeny.java new file mode 100644 index 0000000000000000000000000000000000000000..5bf205d8c0a03ba932be85cc1a63d6cea304b517 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandsTPADeny.java @@ -0,0 +1,99 @@ +package io.papermc.paper.threadedregions.commands; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public final class CommandsTPADeny extends Command { + + public CommandsTPADeny() { + super("tpadeny"); + this.setUsage("/ "); + } + + @Override + public boolean testPermissionSilent(final CommandSender target) { + return true; + } + + @Override + public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { + if (!(sender instanceof CraftPlayer playerSender)) { + sender.sendMessage(commandLabel + " only works for players"); + return true; + } + + if (args.length != 1) { + sender.sendMessage(Component.text("Usage: /" + commandLabel + " ", NamedTextColor.DARK_RED)); + return true; + } + + final ServerPlayer target = CommandUtil.getPlayer(args[0]); + + if (target == null) { + sender.sendMessage( + Component.text() + .append(Component.text("Found no such player ", NamedTextColor.DARK_RED)) + .append(Component.text(args[0], NamedTextColor.RED)) + .build() + ); + return true; + } + + if (!playerSender.getHandle().pendingTpas.remove(target.getUUID())) { + sender.sendMessage( + Component.text() + .append(Component.text("No tpa request to reject from ", NamedTextColor.DARK_RED)) + .append(Component.text(args[0], NamedTextColor.RED)) + .build() + ); + return true; + } + + sender.sendMessage( + Component.text() + .append(Component.text("Rejected tpa request from ", NamedTextColor.GRAY)) + .append(Component.text(target.getGameProfile().getName(), NamedTextColor.GREEN)) + .build() + ); + target.getBukkitEntity().sendMessage( + Component.text() + .append(Component.text(playerSender.getName(), NamedTextColor.RED)) + .append(Component.text(" rejected", NamedTextColor.DARK_RED)) + .append(Component.text(" your tpa request!", NamedTextColor.GRAY)) + .build() + ); + + return true; + } + + @Override + public List tabComplete(final CommandSender sender, final String alias, + final String[] args) throws IllegalArgumentException { + if (!(sender instanceof CraftPlayer playerSender)) { + return new ArrayList<>(); + } + + final List players = MinecraftServer.getServer().getPlayerList().players; + + // The laziest implementation possible. + final Function playerToName = (final ServerPlayer value) -> { + return playerSender.getHandle().pendingTpas.contains(value.getUUID()) ? value.getGameProfile().getName() : null; + }; + + if (args.length == 0) { + return CommandUtil.getSortedList(players, playerToName); + } else if (args.length == 1) { + return CommandUtil.getSortedList(players, playerToName, args[0]); + } + + return new ArrayList<>(); + } +} diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java index e08f4e39db4ee3fed62e37364d17dcc5c5683504..03d239460a2e856c1f59d6bcd95811c8e4e0cf6d 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.RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); + 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.RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); + 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.RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); + 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.RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); + 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.RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); + if (worldData != null) { + worldData.resetCollisionLists(); + } + // Folia end - region threading } } diff --git a/src/main/java/io/papermc/paper/util/CoordinateUtils.java b/src/main/java/io/papermc/paper/util/CoordinateUtils.java index 413e4b6da027876dbbe8eb78f2568a440f431547..d29a4a3bab456df99fbccddc832a9ac2da880f31 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.fastFloor(pos.x); + } + + public static int getBlockY(final Vec3 pos) { + return Mth.fastFloor(pos.y); + } + + public static int getBlockZ(final Vec3 pos) { + return Mth.fastFloor(pos.z); + } + + public static int getChunkX(final Vec3 pos) { + return Mth.fastFloor(pos.x) >> 4; + } + + public static int getChunkY(final Vec3 pos) { + return Mth.fastFloor(pos.y) >> 4; + } + + public static int getChunkZ(final Vec3 pos) { + return Mth.fastFloor(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 6898c704e60d89d53c8ed114e5e12f73ed63605a..594ada3cdec25784c7bd6abb9ad42d3f1e2bd733 100644 --- a/src/main/java/io/papermc/paper/util/MCUtil.java +++ b/src/main/java/io/papermc/paper/util/MCUtil.java @@ -28,6 +28,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; @@ -332,6 +333,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()); } @@ -472,6 +474,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..7de0bd89b13dcb550cf78ceda625f5ab9f9f3599 100644 --- a/src/main/java/io/papermc/paper/util/TickThread.java +++ b/src/main/java/io/papermc/paper/util/TickThread.java @@ -1,8 +1,19 @@ package io.papermc.paper.util; +import io.papermc.paper.threadedregions.RegionShutdownThread; +import io.papermc.paper.threadedregions.RegionisedWorldData; +import io.papermc.paper.threadedregions.ThreadedRegioniser; +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.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.Vec3; import org.bukkit.Bukkit; import java.util.concurrent.atomic.AtomicInteger; @@ -38,6 +49,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()); @@ -77,11 +102,75 @@ 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 ThreadedRegioniser.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + if (region == null) { + return isShutdownThread(); + } + return world.regioniser.getRegionAtUnsynchronised(chunkX, chunkZ) == region; + } + + public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) { + final ThreadedRegioniser.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + if (region == null) { + return isShutdownThread(); + } + + final int minSectionX = (chunkX - radius) >> world.regioniser.sectionChunkShift; + final int maxSectionX = (chunkX + radius) >> world.regioniser.sectionChunkShift; + final int minSectionZ = (chunkZ - radius) >> world.regioniser.sectionChunkShift; + final int maxSectionZ = (chunkZ + radius) >> world.regioniser.sectionChunkShift; + + for (int secZ = minSectionZ; secZ <= maxSectionZ; ++secZ) { + for (int secX = minSectionX; secX <= maxSectionX; ++secX) { + final int lowerLeftCX = secX << world.regioniser.sectionChunkShift; + final int lowerLeftCZ = secZ << world.regioniser.sectionChunkShift; + if (world.regioniser.getRegionAtUnsynchronised(lowerLeftCX, lowerLeftCZ) != region) { + return false; + } + } + } + + return true; } public static boolean isTickThreadFor(final Entity entity) { - return Thread.currentThread() instanceof TickThread; + if (entity == null) { + return true; + } + final ThreadedRegioniser.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + if (region == null) { + return isShutdownThread(); + } + + final Level level = entity.level; + if (level != region.regioniser.world) { + // world mismatch + return false; + } + + final RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); + + // pass through the check if the entity is removed and we own its chunk + return worldData.hasEntity(entity) || (entity.isRemoved() && !(entity instanceof ServerPlayer) && 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/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java index ae5dd08de75a7ed231295f306fd0974da3988249..0674c69e7180c482bcace9797af877e09263e88b 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.RegionisedServer.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 330f6c79417378da855326b4da665f9d240e748d..22f06033a731c3ba1b815842be7a9d575fa820f2 100644 --- a/src/main/java/net/minecraft/commands/Commands.java +++ b/src/main/java/net/minecraft/commands/Commands.java @@ -139,12 +139,12 @@ 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); - DataCommands.register(this.dispatcher); - DataPackCommand.register(this.dispatcher); - DebugCommand.register(this.dispatcher); + //CloneCommands.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO + //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); @@ -154,44 +154,44 @@ 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); 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); } @@ -208,8 +208,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); @@ -417,9 +417,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 58fa7b99dc7a9745afe6faf31c1804e95ed27dbe..28a260dfe6ba9f7e9ff161562dcb87a6314af87c 100644 --- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java @@ -221,7 +221,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); } @@ -276,7 +276,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); } @@ -329,7 +329,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); } @@ -385,7 +385,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); } @@ -459,7 +459,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); } @@ -498,7 +498,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); } @@ -556,7 +556,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); } @@ -628,7 +628,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); } @@ -701,7 +701,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); } @@ -748,7 +748,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); } @@ -809,7 +809,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); } @@ -827,7 +827,8 @@ public interface DispenseItemBehavior { } } - worldserver.captureTreeGeneration = true; + io.papermc.paper.threadedregions.RegionisedWorldData 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)) { @@ -836,13 +837,13 @@ public interface DispenseItemBehavior { worldserver.levelEvent(1505, blockposition, 0); } // CraftBukkit start - worldserver.captureTreeGeneration = false; - if (worldserver.capturedBlockStates.size() > 0) { + worldData.captureTreeGeneration = false; // Folia - region threading + if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading TreeType treeType = SaplingBlock.treeType; SaplingBlock.treeType = null; 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); @@ -877,7 +878,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); } @@ -934,7 +935,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); } @@ -983,7 +984,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); } @@ -1056,7 +1057,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 38c09c65dfa4a7a0c80d36f726c1fd028cbe05f8..5e74408bbdcc9b434447e3d9cf7523ef122ec03e 100644 --- a/src/main/java/net/minecraft/network/Connection.java +++ b/src/main/java/net/minecraft/network/Connection.java @@ -73,7 +73,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 @@ -81,7 +81,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; @@ -177,6 +177,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(); @@ -191,6 +217,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) { @@ -372,13 +399,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(); @@ -496,66 +516,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 @@ -564,21 +576,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 @@ -618,13 +650,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) { @@ -636,6 +676,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 } @@ -784,13 +825,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 27d4aa45e585842c04491839826d405d6f447f0e..e6ef0691588fbb33d47692db4269c56557814c9b 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 @@ -71,7 +72,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 2ee4e5e8d17a3a1e6a342c74b13135df030ffef6..9577b633ecf5ebd1ff5bf79aa6ea61160f59e764 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -291,7 +291,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); public int autosavePeriod; public Commands vanillaCommandDispatcher; @@ -304,12 +304,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 @@ -602,7 +630,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); @@ -614,6 +656,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(); + // 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.setPlayers(new ServerStatus.Players(this.getMaxPlayers(), this.getPlayerCount())); if (!this.hidesOnlinePlayers()) { @@ -1429,9 +1571,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 { @@ -1441,16 +1583,17 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper + // Folia - region threading this.profiler.push(() -> { return worldserver + " " + worldserver.dimension().location(); @@ -1532,7 +1674,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + io.papermc.paper.threadedregions.RegionisedServer.getInstance().invalidateStatus(); + }); + return; + } + // Folia end - region threading this.lastServerStatus = 0L; } @@ -1962,6 +2113,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 @@ -2764,7 +2901,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/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 bed3ffb18398f34077503ba2d7aa6ecc7c0537c2..8651d87632a4f5d0ccd69332a78f9a9969eb638f 100644 --- a/src/main/java/net/minecraft/server/commands/EffectCommands.java +++ b/src/main/java/net/minecraft/server/commands/EffectCommands.java @@ -76,7 +76,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; } } @@ -102,8 +110,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 } } @@ -128,8 +144,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..9ad71bda2f7498ad6e0853a1070c5be2d8016548 100644 --- a/src/main/java/net/minecraft/server/commands/EnchantCommand.java +++ b/src/main/java/net/minecraft/server/commands/EnchantCommand.java @@ -46,6 +46,12 @@ 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()) { @@ -55,18 +61,26 @@ public class EnchantCommand { 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); + } 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 (CommandSyntaxException ex) { + sendMessage(source, ex); } - } else if (targets.size() == 1) { - throw ERROR_NO_ITEM.create(livingEntity.getName().getString()); - } + }, null, 1L); + ++i; + // Folia end - region threading } else if (targets.size() == 1) { throw ERROR_NOT_LIVING_ENTITY.create(entity.getName().getString()); } 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 6c29947dc9259f453782de3c973c1cabb87e3de5..c8e60578f15a358223ed056460d3ea2c57b0cd40 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); @@ -78,29 +84,43 @@ public class FillBiomeCommand { throw ERROR_VOLUME_TOO_LARGE.create(32768, i); } else { ServerLevel serverLevel = source.getLevel(); - List list = new ArrayList<>(); + // 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) -> { + List list = new ArrayList<>(); - for(int j = SectionPos.blockToSectionCoord(boundingBox.minZ()); j <= SectionPos.blockToSectionCoord(boundingBox.maxZ()); ++j) { - for(int k = SectionPos.blockToSectionCoord(boundingBox.minX()); k <= SectionPos.blockToSectionCoord(boundingBox.maxX()); ++k) { - ChunkAccess chunkAccess = serverLevel.getChunk(k, j, ChunkStatus.FULL, false); - if (chunkAccess == null) { - throw ERROR_NOT_LOADED.create(); - } + for(int j = SectionPos.blockToSectionCoord(boundingBox.minZ()); j <= SectionPos.blockToSectionCoord(boundingBox.maxZ()); ++j) { + for(int k = SectionPos.blockToSectionCoord(boundingBox.minX()); k <= SectionPos.blockToSectionCoord(boundingBox.maxX()); ++k) { + ChunkAccess chunkAccess = serverLevel.getChunk(k, j, ChunkStatus.FULL, false); + if (chunkAccess == null) { + sendMessage(source, ERROR_NOT_LOADED.create()); return; + } - list.add(chunkAccess); - } - } + list.add(chunkAccess); + } + } - MutableInt mutableInt = new MutableInt(0); + MutableInt mutableInt = new MutableInt(0); - for(ChunkAccess chunkAccess2 : list) { - chunkAccess2.fillBiomesFromNoise(makeResolver(mutableInt, chunkAccess2, boundingBox, biome, filter), serverLevel.getChunkSource().randomState().sampler()); - chunkAccess2.setUnsaved(true); - serverLevel.getChunkSource().chunkMap.resendChunk(chunkAccess2); - } + for(ChunkAccess chunkAccess2 : list) { + chunkAccess2.fillBiomesFromNoise(makeResolver(mutableInt, chunkAccess2, boundingBox, biome, filter), serverLevel.getChunkSource().randomState().sampler()); + chunkAccess2.setUnsaved(true); + serverLevel.getChunkSource().chunkMap.resendChunk(chunkAccess2); + } - 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(); + source.sendSuccess(Component.translatable("commands.fillbiome.success.count", mutableInt.getValue(), boundingBox.minX(), boundingBox.minY(), boundingBox.minZ(), boundingBox.maxX(), boundingBox.maxY(), boundingBox.maxZ()), true); + } + ); + 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 99fbb24dabe867ed4956a2996543107f58a57193..01360d24522a877bf7c3524f17ec65ef2b514b0c 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(); if (i > 32768) { @@ -64,33 +70,50 @@ public class FillCommand { } else { List list = Lists.newArrayList(); ServerLevel serverLevel = source.getLevel(); - int j = 0; - for(BlockPos blockPos : BlockPos.betweenClosed(range.minX(), range.minY(), range.minZ(), range.maxX(), range.maxY(), range.maxZ())) { - if (filter == null || filter.test(new BlockInWorld(serverLevel, blockPos, true))) { - BlockInput blockInput = mode.filter.filter(range, blockPos, block, serverLevel); - if (blockInput != null) { - BlockEntity blockEntity = serverLevel.getBlockEntity(blockPos); - Clearable.tryClear(blockEntity); - if (blockInput.place(serverLevel, blockPos, 2)) { - list.add(blockPos.immutable()); - ++j; + // 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) -> { + int j = 0; + + for(BlockPos blockPos : BlockPos.betweenClosed(range.minX(), range.minY(), range.minZ(), range.maxX(), range.maxY(), range.maxZ())) { + if (filter == null || filter.test(new BlockInWorld(serverLevel, blockPos, true))) { + BlockInput blockInput = mode.filter.filter(range, blockPos, block, serverLevel); + if (blockInput != null) { + BlockEntity blockEntity = serverLevel.getBlockEntity(blockPos); + Clearable.tryClear(blockEntity); + if (blockInput.place(serverLevel, blockPos, 2)) { + list.add(blockPos.immutable()); + ++j; + } + } } } - } - } - for(BlockPos blockPos2 : list) { - Block block2 = serverLevel.getBlockState(blockPos2).getBlock(); - serverLevel.blockUpdated(blockPos2, block2); - } + for(BlockPos blockPos2 : list) { + Block block2 = serverLevel.getBlockState(blockPos2).getBlock(); + serverLevel.blockUpdated(blockPos2, block2); + } + + if (j == 0) { + sendMessage(source, ERROR_FAILED.create()); return; // Folia - region threading + } else { + source.sendSuccess(Component.translatable("commands.fill.success", j), true); + return; // Folia - region threading + } + } + ); - if (j == 0) { - throw ERROR_FAILED.create(); - } else { - source.sendSuccess(Component.translatable("commands.fill.success", j), true); - return j; - } + 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..3f165dbca5ce094ad39e46ecc2fa2bb9e80968ce 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.RegionisedServer.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.RegionisedServer.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.RegionisedServer.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.RegionisedServer.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 06e3a868e922f1b7a586d0ca28f64a67ae463b68..8f4a7b6ed27e97c22153dadf837e521a75bb6940 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.setOwner(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 6835072c6b30ee0b79c43e05526fd6d605bf7139..0a6baec737ef847fc84723176c7f267d3999ad4c 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.RegionisedServer.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.RegionisedServer.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.RegionisedServer.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..2e53969ae222c13a7ef034f96a7014f924960481 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.RegionisedServer.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 1e41de9523c5fa3b9cfced798a5c35a24ec9d349..aa2c3d3161d01c87cd88e3311907e6559e81aa4e 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 ade2626bc63f986a53277378cdc19f5366f9372f..b2081239f13d3a001bbfa467933518ec400baea7 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.RegionisedServer.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 { source.sendSuccess(Component.translatable("commands.summon.success", entity.getDisplayName()), true); diff --git a/src/main/java/net/minecraft/server/commands/TeleportCommand.java b/src/main/java/net/minecraft/server/commands/TeleportCommand.java index 027ca5b67c544048815ddef4bb36d0a8fc3d038c..f7981cc27aa62cf0935d6ce027cd73c50b837c04 100644 --- a/src/main/java/net/minecraft/server/commands/TeleportCommand.java +++ b/src/main/java/net/minecraft/server/commands/TeleportCommand.java @@ -78,7 +78,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(ClientboundPlayerPositionPacket.RelativeArgument.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) { @@ -154,6 +154,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 if (target instanceof ServerPlayer) { ChunkPos chunkcoordintpair = new ChunkPos(new BlockPos(x, y, z)); diff --git a/src/main/java/net/minecraft/server/commands/TimeCommand.java b/src/main/java/net/minecraft/server/commands/TimeCommand.java index f0a7a8df3caa2ea765bb0a87cfede71d0995d276..00f63992885c16ea01384fc00e1325f389cc1a0f 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.RegionisedServer.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.RegionisedServer.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.RegionisedServer.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 71fd7887a4fa174d3f74c4bbe24497b156cbd3c8..b8e1054cd5c906fd425fe5987c10db963cb32c62 100644 --- a/src/main/java/net/minecraft/server/commands/WeatherCommand.java +++ b/src/main/java/net/minecraft/server/commands/WeatherCommand.java @@ -28,20 +28,26 @@ public class WeatherCommand { } private static int setClear(CommandSourceStack source, int duration) { + io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { // Folia - region threading source.getLevel().setWeatherParameters(duration, 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.RegionisedServer.getInstance().addTask(() -> { // Folia - region threading source.getLevel().setWeatherParameters(0, 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.RegionisedServer.getInstance().addTask(() -> { // Folia - region threading source.getLevel().setWeatherParameters(0, 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 51b3db0b6c2cede95b584268e035c0fb36d38094..48718c37e96821576f0d6bf0e510cd5806a23d4c 100644 --- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java @@ -436,9 +436,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 @@ -741,6 +741,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 0b9cb85c063f913ad9245bafb8587d2f06c0ac6e..179e142e7012eebbe636f65804f5ac6b8fb72abe 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.getCurrentWorldData().addChunkHolderNeedsBroadcasting(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.getCurrentWorldData().removeChunkHolderNeedsBroadcasting(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 } @@ -303,7 +303,7 @@ public class ChunkHolder { private void addToBroadcastMap() { org.spigotmc.AsyncCatcher.catchOp("ChunkHolder update"); - this.chunkMap.needsChangeBroadcasting.add(this); + this.chunkMap.level.getCurrentWorldData().addChunkHolderNeedsBroadcasting(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 870f4d6fae8c14502b4653f246a2df9e345ccca3..e23d752fcf6fea08d3ec114b065ada9d7e634a80 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -146,21 +146,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 @@ -174,8 +174,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) { @@ -184,11 +184,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); @@ -200,40 +196,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 } @@ -242,21 +227,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 @@ -294,8 +272,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(); @@ -340,96 +318,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 } @@ -457,28 +357,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) { @@ -747,6 +626,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 @@ -1007,6 +892,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; @@ -1052,7 +969,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(); @@ -1081,25 +998,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 } @@ -1118,43 +1028,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 @@ -1177,9 +1051,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 @@ -1192,27 +1066,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 } } @@ -1226,16 +1098,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(); @@ -1245,25 +1117,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 @@ -1274,51 +1139,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); @@ -1327,7 +1153,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); @@ -1504,41 +1330,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; @@ -1585,6 +1377,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 @@ -1600,10 +1414,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..06ad3857f04ec073ae753b6569c5ae2ce7719ede 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -61,7 +61,7 @@ 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; @@ -72,62 +72,33 @@ public class ServerChunkCache extends ChunkSource { @VisibleForDebug private NaturalSpawner.SpawnState lastSpawnState; // Paper start - final com.destroystokyo.paper.util.concurrent.WeakSeqLock loadedChunkMapSeqLock = new com.destroystokyo.paper.util.concurrent.WeakSeqLock(); - final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap loadedChunkMap = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(8192, 0.5f); + // 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 +113,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 +264,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 +338,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 +434,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 +611,11 @@ public class ServerChunkCache extends ChunkSource { } private void tickChunks() { + io.papermc.paper.threadedregions.RegionisedWorldData regionisedWorldData = 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 +623,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); + regionisedWorldData.mobSpawnMap.remove(player); // Folia - region threading continue; } @@ -685,8 +640,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 + regionisedWorldData.mobSpawnMap.remove(player); // Folia - region threading continue; } @@ -694,7 +650,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); + regionisedWorldData.mobSpawnMap.addOrUpdate(player, chunkX, chunkZ, range); // Folia - region threading player.lastEntitySpawnRadiusSquared = (double)((range << 4) * (range << 4)); // used in anyPlayerCloseEnoughForSpawning player.playerNaturallySpawnedEvent = event; } @@ -704,23 +660,16 @@ 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, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap)) : null; + // Folia end - threaded regions - revert per-player mob caps this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings this.lastSpawnState = spawnercreature_d; @@ -731,17 +680,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) && !regionisedWorldData.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 = regionisedWorldData.getEntityTickingChunks().iterator(); // Folia - region threading } else { - iterator1 = this.entityTickingChunks.unsafeIterator(); - List shuffled = Lists.newArrayListWithCapacity(this.entityTickingChunks.size()); + iterator1 = regionisedWorldData.getEntityTickingChunks().unsafeIterator(); // Folia - region threading + List shuffled = Lists.newArrayListWithCapacity(regionisedWorldData.getEntityTickingChunks().size()); // Folia - region threading while (iterator1.hasNext()) { shuffled.add(iterator1.next()); } @@ -791,14 +740,14 @@ 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(); + if (!regionisedWorldData.getNeedsChangeBroadcasting().isEmpty()) { // Folia - region threading + ReferenceOpenHashSet copy = regionisedWorldData.getNeedsChangeBroadcasting().clone(); // Folia - region threading + regionisedWorldData.getNeedsChangeBroadcasting().clear(); // Folia - region threading for (ChunkHolder holder : copy) { holder.broadcastChanges(holder.getFullChunkNowUnchecked()); // LevelChunks are NEVER unloaded if (holder.needsBroadcastChanges()) { // I DON'T want to KNOW what DUMB plugins might be doing. - this.chunkMap.needsChangeBroadcasting.add(holder); + regionisedWorldData.getNeedsChangeBroadcasting().add(holder); // Folia - region threading } } } @@ -806,8 +755,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<>(regionisedWorldData.getLocalPlayers().size()); // Folia - region threading + for (ServerPlayer player : regionisedWorldData.getLocalPlayers()) { // Folia - region threading net.minecraft.server.network.ServerGamePacketListenerImpl connection = player.connection; if (connection != null) { connection.connection.disableAutomaticFlush(); @@ -880,14 +829,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.RegionisedServer.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) { @@ -992,8 +946,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 +990,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.getCurrentRegionisedWorldData().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 714637cdd9dcdbffa344b19e77944fb3c7541ff7..61fd4eea649fab254b3b2c0f160257e01d0873ed 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -192,35 +192,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()); @@ -267,50 +266,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); @@ -322,11 +335,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; @@ -446,81 +479,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; - } + // Folia - region threading - @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); - } + // Folia - region threading - @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); - - List ret = new java.util.ArrayList<>(); - - 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.RegionisedPlayerChunkLoader playerChunkLoader = new io.papermc.paper.chunk.system.RegionisedPlayerChunkLoader(this); @@ -565,6 +533,29 @@ 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.ThreadedRegioniser regioniser; + { + this.regioniser = new io.papermc.paper.threadedregions.ThreadedRegioniser<>( + 3*9, + (2.0 / 3.0), + 1, + 1, + io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(), + this, + this.tickRegions + ); + } + public final io.papermc.paper.threadedregions.RegionisedTaskQueue.WorldRegionTaskData taskQueueRegionData = new io.papermc.paper.threadedregions.RegionisedTaskQueue.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; + + // 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) { // Holder holder = worlddimension.type(); // CraftBukkit - decompile error @@ -574,13 +565,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 +610,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,8 +638,15 @@ 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.RegionisedServer.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); this.serverLevelData.setRainTime(rainDuration); @@ -666,55 +664,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.RegionisedWorldData regionisedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking + // Folia - region threading ProfilerFiller gameprofilerfiller = this.getProfiler(); - this.handlingTick = true; + regionisedWorldData.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 (region == null) this.tickSleep(); // Folia - region threading - if (!event.isCancelled()) { - this.wakeUpAllPlayers(); - } - // CraftBukkit end - if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) { - this.resetWeatherCycle(); - } - } - - 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 = regionisedWorldData.getRedstoneGameTime(); // Folia - region threading gameprofilerfiller.push("blockTicks"); - this.blockTicks.tick(j, 65536, this::tickBlock); + regionisedWorldData.getBlockLevelTicks().tick(j, 65536, this::tickBlock); // Folia - region ticking gameprofilerfiller.popPush("fluidTicks"); - this.fluidTicks.tick(j, 65536, this::tickFluid); + regionisedWorldData.getFluidLevelTicks().tick(j, 65536, this::tickFluid); // Folia - region ticking gameprofilerfiller.pop(); } timings.scheduledBlocks.stopTiming(); // Paper @@ -731,7 +705,7 @@ public class ServerLevel extends Level implements WorldGenLevel { timings.doSounds.startTiming(); // Spigot this.runBlockEvents(); timings.doSounds.stopTiming(); // Spigot - this.handlingTick = false; + regionisedWorldData.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 +717,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) -> { + regionisedWorldData.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 +771,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 +806,12 @@ public class ServerLevel extends Level implements WorldGenLevel { protected void tickTime() { if (this.tickTime) { - long i = this.levelData.getGameTime() + 1L; + io.papermc.paper.threadedregions.RegionisedWorldData regionisedWorldData = this.getCurrentWorldData(); // Folia - region threading + long i = regionisedWorldData.getRedstoneGameTime() + 1L; // Folia - region threading - this.serverLevelData.setGameTime(i); - this.serverLevelData.getScheduledEvents().tick(this.server, i); - if (this.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { + regionisedWorldData.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 +840,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 +864,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 @@ -941,7 +959,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; } @@ -955,7 +973,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) } @@ -1009,7 +1027,7 @@ public class ServerLevel extends Level implements WorldGenLevel { } public boolean isHandlingTick() { - return this.handlingTick; + return this.getCurrentWorldData().isHandlingTick(); // Folia - regionised ticking } public boolean canSleepThroughNights() { @@ -1041,6 +1059,14 @@ public class ServerLevel extends Level implements WorldGenLevel { } public void updateSleepingPlayerList() { + // Folia start - region threading + if (!io.papermc.paper.threadedregions.RegionisedServer.isGlobalTickThread()) { + io.papermc.paper.threadedregions.RegionisedServer.getInstance().addTask(() -> { + ServerLevel.this.updateSleepingPlayerList(); + }); + return; + } + // Folia end - region threading if (!this.players.isEmpty() && this.sleepStatus.update(this.players)) { this.announceSleepStatus(); } @@ -1052,7 +1078,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()) { @@ -1138,23 +1164,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 @@ -1218,7 +1245,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); @@ -1251,7 +1278,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 @@ -1274,7 +1310,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 @@ -1291,7 +1327,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(); @@ -1379,7 +1424,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; @@ -1387,12 +1440,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(); } @@ -1447,6 +1495,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); @@ -1548,8 +1609,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 @@ -1688,7 +1749,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")); @@ -1701,7 +1762,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 @@ -1724,7 +1785,7 @@ public class ServerLevel extends Level implements WorldGenLevel { } try { - this.isUpdatingNavigations = true; + //this.isUpdatingNavigations = true; // Folia - region threading iterator = list.iterator(); while (iterator.hasNext()) { @@ -1733,7 +1794,7 @@ public class ServerLevel extends Level implements WorldGenLevel { navigationabstract1.recomputePath(); } } finally { - this.isUpdatingNavigations = false; + //this.isUpdatingNavigations = false; // Folia - region threading } } @@ -1742,23 +1803,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 @@ -1784,7 +1845,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(); @@ -1799,25 +1860,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.RegionisedWorldData 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) { @@ -1828,12 +1892,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 @@ -1857,7 +1921,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 @@ -1910,7 +1974,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 @@ -1918,6 +1989,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 @@ -2082,7 +2208,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 +2236,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.RegionisedServer.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 +2255,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.RegionisedServer.getInstance().taskQueue.queueChunkTask( + this, blockposition1.getX() >> 4, blockposition1.getZ() >> 4, run + ); + // Folia end - region threading }); } } @@ -2171,7 +2307,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 +2321,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 +2467,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 +2480,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 +2489,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 +2532,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 +2565,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 +2583,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 +2604,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 +2633,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 +2681,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 +2718,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 +2729,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 } } @@ -2666,7 +2805,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 +2816,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 869daafbc236b3ff63f878e5fe28427fde75afe5..ecd5b4542f95717e830fe3d845e79090aa341c2b 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -181,7 +181,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; @@ -242,11 +242,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; @@ -311,6 +307,9 @@ public class ServerPlayer extends Player { }); } + // Folia start - region threading + // Folia end - region threading + public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile) { super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile); this.chatVisibility = ChatVisiblity.FULL; @@ -408,7 +407,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. @@ -450,11 +449,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) { @@ -468,33 +467,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; } @@ -1147,6 +1189,338 @@ 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.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) -> { + // reset player if needed + if (!alive) { + ServerPlayer.this.reset(); + } + + // 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) -> { + // 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.isPassenger()); + 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.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(), treeNode.parent == null + ); + 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(), treeNode.parent == null + ); + 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) { @@ -2098,6 +2472,12 @@ public class ServerPlayer extends Player { if (entity1 == entity) return; // new spec target is the current spec target + // Folia start - region threading + if (!io.papermc.paper.util.TickThread.isTickThreadFor(entity)) { + return; + } + // Folia end - region threading + if (entity == this) { com.destroystokyo.paper.event.player.PlayerStopSpectatingEntityEvent playerStopSpectatingEntityEvent = new com.destroystokyo.paper.event.player.PlayerStopSpectatingEntityEvent(this.getBukkitEntity(), entity1.getBukkitEntity()); @@ -2132,7 +2512,7 @@ public class ServerPlayer extends Player { this.getBukkitEntity().teleport(new Location(entity.getCommandSenderWorld().getWorld(), entity.getX(), entity.getY(), entity.getZ(), this.getYRot(), this.getXRot()), TeleportCause.SPECTATE); // Correctly handle cross-world entities from api calls by using CB teleport // Make sure we're tracking the entity before sending - ChunkMap.TrackedEntity tracker = ((ServerLevel)entity.level).getChunkSource().chunkMap.entityMap.get(entity.getId()); + ChunkMap.TrackedEntity tracker = entity.tracker; // Folia - region threading if (tracker != null) { // dumb plugins... tracker.updatePlayer(this); } @@ -2567,7 +2947,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 58b093bb1de78ee3b3b2ea364aa50474883f443a..147c9baaf73d0f8c315477ee32236bc163e1736c 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java @@ -123,7 +123,7 @@ public class ServerPlayerGameMode { } public void tick() { - this.gameTicks = MinecraftServer.currentTick; // CraftBukkit; + ++this.gameTicks; // CraftBukkit; // Folia - region threading BlockState iblockdata; if (this.hasDelayedDestroy) { @@ -408,7 +408,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); @@ -436,8 +436,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..eef501b0558680e5563b0a15a93bd3ab217b91d8 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.RegionisedServer.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.RegionisedServer.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.RegionisedServer.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..cf38de369a57e30a29dfa13e116f950b0dbf5904 100644 --- a/src/main/java/net/minecraft/server/level/TicketType.java +++ b/src/main/java/net/minecraft/server/level/TicketType.java @@ -35,6 +35,12 @@ 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); + // 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 abcc3266d18f34d160eac87fdea153dce24c60b8..7cf0619883577a0f21ed75ba70ece90d5c316c21 100644 --- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java +++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java @@ -155,10 +155,13 @@ public class ServerConnectionListener { // Paper end // ServerConnectionListener.this.connections.add((Connection) object); // CraftBukkit - decompile error - pending.add((Connection) object); // Paper + // Folia - connection fixes - move down channel.pipeline().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.RegionisedServer.getInstance().addConnection((Connection)object); + // Folia end - regionised threading } }).group((EventLoopGroup) lazyinitvar.get()).localAddress(address)).option(ChannelOption.AUTO_READ, false).bind().syncUninterruptibly()); // CraftBukkit // Paper } @@ -217,7 +220,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 3472f7f9b98d6d9c9f6465872803ef17fa67486d..e8e2d8e481ff798dc73bfdfe956cd7d9cabe4403 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -320,10 +320,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 @@ -340,8 +340,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; @@ -393,22 +425,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(); @@ -441,6 +458,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 @@ -477,24 +507,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; } @@ -525,7 +539,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) { @@ -608,9 +621,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; @@ -864,13 +878,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 @@ -895,7 +909,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) -> { @@ -906,7 +920,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()); @@ -1215,7 +1229,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; @@ -1238,17 +1252,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(); @@ -1435,9 +1449,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); @@ -1829,9 +1844,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.RegionisedServer.getCurrentTick()) { this.dropCount = 0; - this.lastDropTick = MinecraftServer.currentTick; + this.lastDropTick = io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick(); } else { // Else we increment the drop count and check the amount. this.dropCount++; @@ -2056,7 +2071,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; } } @@ -2117,6 +2132,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 @@ -2201,9 +2218,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()); @@ -2237,17 +2254,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 } } @@ -2321,9 +2338,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 if (this.player.isRemoved() || this.player.getChatVisibility() == ChatVisiblity.HIDDEN) { // CraftBukkit - dead men tell no tales this.send(new ClientboundSystemChatPacket(Component.translatable("chat.disabled.options").withStyle(ChatFormatting.RED), false)); @@ -2396,7 +2413,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()); @@ -2474,6 +2491,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<>() { @@ -2534,6 +2552,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 @@ -2889,6 +2908,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); @@ -2897,6 +2922,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 @@ -3249,7 +3286,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; } } @@ -3391,7 +3428,13 @@ 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); + }); } private void updateSignText(ServerboundSignUpdatePacket packet, List signText) { diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java index a25306fe8a35bb70a490e6a0c01d0340bbc0d781..805557d4fedd234a593ccf2655399a2b87ee6b60 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.fastFloor(loc.getX()) >> 4; + int chunkZ = net.minecraft.util.Mth.fastFloor(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.RegionisedServer.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/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java index 3c9d08c37a44a60bc70387d8d0dbd0a39ea98a26..a0267f2e110bacd30f33978414fd2aff2dc84ab1 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java @@ -138,7 +138,7 @@ public abstract class PlayerList { private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); 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; @@ -160,9 +160,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 @@ -184,7 +231,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(); @@ -234,8 +281,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.RegionisedServer.getCurrentTick(); + // Folia end - region threading - rewrite login process player.setLevel(worldserver1); String s1 = "local"; @@ -246,7 +313,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(); @@ -265,6 +332,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); @@ -282,7 +350,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; @@ -334,8 +402,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); @@ -451,7 +518,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 @@ -542,7 +609,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.RegionisedServer.getCurrentTick(); // Folia - region threading this.playerIo.save(player); ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit @@ -582,7 +649,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) { @@ -622,6 +689,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 @@ -640,8 +708,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); @@ -666,19 +733,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 @@ -717,7 +778,7 @@ public abstract class PlayerList { 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 } } @@ -967,10 +1028,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()); @@ -996,18 +1057,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 } } @@ -1051,8 +1111,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); @@ -1063,10 +1122,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; @@ -1085,7 +1146,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 } } @@ -1095,7 +1158,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 + } } @@ -1156,8 +1222,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())) { @@ -1187,9 +1252,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.RegionisedServer.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; } @@ -1309,6 +1377,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 ichatbasecomponent = PaperAdventure.asVanilla(this.server.server.shutdownMessage()); // Paper - Adventure + // CraftBukkit end + + player.connection.send(new net.minecraft.network.protocol.game.ClientboundDisconnectPacket(ichatbasecomponent), net.minecraft.network.PacketSendListener.thenRun(() -> { + player.connection.disconnect(ichatbasecomponent); + })); + } + if (true) { + return; + } + // Folia end - region threading // Paper end // CraftBukkit start - disconnect safely for (ServerPlayer player : this.players) { @@ -1318,7 +1400,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 ba96589aab8be0a90144f73f7779769146c7a37e..08dc87506686434d2e53e748ead9d05c35f470cd 100644 --- a/src/main/java/net/minecraft/world/damagesource/CombatTracker.java +++ b/src/main/java/net/minecraft/world/damagesource/CombatTracker.java @@ -34,7 +34,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)) { @@ -85,6 +85,7 @@ public class CombatTracker { CombatEntry combatEntry2 = this.entries.get(this.entries.size() - 1); Component component = combatEntry2.getAttackerName(); Entity entity = combatEntry2.getSource().getEntity(); + if (!io.papermc.paper.util.TickThread.isTickThreadFor(entity)) entity = null; // Folia - region threading - not safe to access other entity data Component component4; if (combatEntry != null && combatEntry2.getSource() == DamageSource.FALL) { Component component2 = combatEntry.getAttackerName(); @@ -126,6 +127,7 @@ public class CombatTracker { float g = 0.0F; for(CombatEntry combatEntry : this.entries) { + if (!io.papermc.paper.util.TickThread.isTickThreadFor(combatEntry.getSource().getEntity())) { continue; } // Folia - region threading - skip entities we do not own if (combatEntry.getSource().getEntity() instanceof Player && (player == null || combatEntry.getDamage() > g)) { g = combatEntry.getDamage(); player = (Player)combatEntry.getSource().getEntity(); diff --git a/src/main/java/net/minecraft/world/damagesource/EntityDamageSource.java b/src/main/java/net/minecraft/world/damagesource/EntityDamageSource.java index b53e04854f960682fae94bb2ef5a12e4274fcebf..01469deddce99a736e661ce9bca8e20e88bb6c1e 100644 --- a/src/main/java/net/minecraft/world/damagesource/EntityDamageSource.java +++ b/src/main/java/net/minecraft/world/damagesource/EntityDamageSource.java @@ -35,7 +35,7 @@ public class EntityDamageSource extends DamageSource { public Component getLocalizedDeathMessage(LivingEntity entity) { Entity var4 = this.entity; ItemStack var10000; - if (var4 instanceof LivingEntity livingEntity) { + if (var4 instanceof LivingEntity livingEntity && io.papermc.paper.util.TickThread.isTickThreadFor(livingEntity)) { // Folia - region threading var10000 = livingEntity.getMainHandItem(); } else { var10000 = ItemStack.EMPTY; diff --git a/src/main/java/net/minecraft/world/damagesource/IndirectEntityDamageSource.java b/src/main/java/net/minecraft/world/damagesource/IndirectEntityDamageSource.java index 6b5fd3e2e19c2d3d694df94f90fce0d310a1a86c..a7a48cf40db1e31ab03e0f42028b617b4f87c8ad 100644 --- a/src/main/java/net/minecraft/world/damagesource/IndirectEntityDamageSource.java +++ b/src/main/java/net/minecraft/world/damagesource/IndirectEntityDamageSource.java @@ -34,7 +34,7 @@ public class IndirectEntityDamageSource extends EntityDamageSource { Entity entity1 = this.cause; ItemStack itemstack; - if (entity1 instanceof LivingEntity) { + if (entity1 instanceof LivingEntity && io.papermc.paper.util.TickThread.isTickThreadFor(entity1)) { // Folia - region threading LivingEntity entityliving1 = (LivingEntity) entity1; itemstack = entityliving1.getMainHandItem(); diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java index 1eaab1f6923e6aa34b643293347348e5cc19af3c..87e20dc4e787e7b875d97b6cdb6b3ce497f795e8 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(); @@ -488,28 +500,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; @@ -541,6 +532,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(); @@ -656,6 +666,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); } @@ -780,6 +795,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(); @@ -802,7 +823,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(); } @@ -903,11 +924,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(DamageSource.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 } @@ -1015,8 +1036,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.RegionisedServer.getCurrentTick() + 20); // Paper + this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick() + 20); // Paper movement = this.limitPistonMovement(movement); if (movement.equals(Vec3.ZERO)) { return; @@ -3071,6 +3092,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()); } @@ -3186,9 +3212,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(DamageSource.LIGHTNING_BOLT, 5.0F)) { - CraftEventFactory.entityDamage = null; + CraftEventFactory.entityDamageRT.set(null); // Folia - region threading return; } // CraftBukkit end @@ -3361,6 +3387,662 @@ 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; + + 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(); + + Runnable scheduleEntityJoin = () -> { + io.papermc.paper.threadedregions.RegionisedServer.getInstance().taskQueue.queueTickTaskQueue( + destination, + io.papermc.paper.util.CoordinateUtils.getChunkX(pos), io.papermc.paper.util.CoordinateUtils.getChunkZ(pos), + () -> { + 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.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) { + Entity copy = this.getType().create(destination); + copy.restoreFrom(this); + copy.transform(pos, yaw, pitch, speedDirectionUpdate); + + this.removeAfterChangingDimensions(); + + 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(pos))) { + return false; + } + + if ((teleportFlags & TELEPORT_FLAG_TELEPORT_PASSENGERS) != 0L) { + if (this.isPassenger()) { + return false; + } + } else { + if (this.isVehicle() || this.isPassenger()) { + return false; + } + } + + this.getBukkitEntity(); // force bukkit entity to be created before TPing + if (!this.canTeleportAsync()) { + return false; + } + for (Entity entity : this.getIndirectPassengers()) { + entity.getBukkitEntity(); // force bukkit entity to be created before TPing + if (!entity.canTeleportAsync()) { + 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); + } + + // 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 this.level.getBlockStateIfLoaded(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, + 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; + } + + // no portal found - create one + destination.loadChunksAsync( + targetPos, portalCreateRadius + 32, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH, + (chunks2) -> { + // 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; + } + + // 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(); + } + + ca.spottedleaf.concurrentutil.completable.Completable portalInfoCompletable + = new ca.spottedleaf.concurrentutil.completable.Completable<>(); + + portalInfoCompletable.addWaiter((PortalInfo info, Throwable throwable) -> { + // 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 @@ -3859,17 +4541,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 @@ -4341,7 +5019,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 @@ -4362,7 +5041,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 @@ -4461,7 +5140,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 42eb78830855d7282b7f3f1bdbe85e632d489784..1e36d889edb6b68d52eae9eee3c13802496af864 100644 --- a/src/main/java/net/minecraft/world/entity/LivingEntity.java +++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java @@ -469,7 +469,7 @@ public abstract class LivingEntity extends Entity { if (this.isDeadOrDying() && this.level.shouldTickDeath(this)) { this.tickDeath(); - } + } else { this.broadcastedDeath = false; } // Folia - region threading if (this.lastHurtByPlayerTime > 0) { --this.lastHurtByPlayerTime; @@ -620,11 +620,14 @@ public abstract class LivingEntity extends Entity { return false; } + 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 } } @@ -841,9 +844,9 @@ public abstract class LivingEntity extends Entity { } 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 @@ -2264,7 +2267,7 @@ public abstract class LivingEntity extends Entity { @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() { @@ -3432,7 +3435,7 @@ public abstract class LivingEntity extends Entity { 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()); @@ -4096,7 +4099,7 @@ public abstract class LivingEntity extends Entity { BlockPos blockposition = new BlockPos(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 49b983064ea810382b6112f5dc7f93ba4e5710bd..ee24904679e37007c38d3eb7095b406f345444f6 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 { 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); @@ -826,12 +834,7 @@ public abstract class Mob extends LivingEntity { 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); 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 11a101e8ff05fbda5e84018358be02014ca01854..8cfa70e9b07e0f993d172d3e4d3804490a4d9fd5 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.tamable.isOrderedToSit() ? false : this.tamable.distanceToSqr((Entity) this.owner) > (double) (this.stopDistance * this.stopDistance)); + return this.navigation.isDone() ? false : (this.tamable.isOrderedToSit() ? false : (this.owner.level == this.level && this.tamable.distanceToSqr((Entity) this.owner) > (double) (this.stopDistance * this.stopDistance))); // Folia - region threading - check level } @Override @@ -93,7 +93,7 @@ public class FollowOwnerGoal extends Goal { if (--this.timeToRecalcPath <= 0) { this.timeToRecalcPath = this.adjustedTickDelay(10); if (!this.tamable.isLeashed() && !this.tamable.isPassenger()) { - 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); @@ -105,6 +105,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); @@ -135,7 +140,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 97257b450e848f53fdb9b5b7affa57b03ea5f459..5485eba13ae544a3e5b7ff5e416c369db8f9c7bc 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 @@ -79,11 +79,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 { @@ -198,7 +198,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 @@ -210,7 +210,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..394d73b10bc53310d936d1ad568a77bf852ef9d6 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.RegionisedWorldData 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.RegionisedWorldData.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.RegionisedWorldData 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.RegionisedWorldData 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 0114c1cf3b6b0500149a77ebc190cb7fa2832184..1189465e79005c99204f873ca7768171218d6399 100644 --- a/src/main/java/net/minecraft/world/entity/animal/Cat.java +++ b/src/main/java/net/minecraft/world/entity/animal/Cat.java @@ -366,7 +366,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 25503678e7d049a8b3172cfad8a5606958c32302..13d60258c6c491a7d0ba5cc93934f0c9b2abd35b 100644 --- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java +++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java @@ -336,9 +336,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(DamageSource.LIGHTNING_BOLT, 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 428523feaa4f30260e32ba03937e88200246c693..5722f1dd949c8ef59379bf4499ec2d77a40df847 100644 --- a/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java +++ b/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java @@ -290,8 +290,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 eacb8a407fe99af2c13f23c12b5544696bda8890..723d5f44c59a8073040669549d9cab88b45e9a3e 100644 --- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java +++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java @@ -292,9 +292,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 f0ccdfbd7d7be8c6e302609accf8fe9cac8885c4..d07060fb35df66e77ebdd404444c58564fdb9402 100644 --- a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java +++ b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java @@ -50,7 +50,7 @@ public class ItemEntity extends Entity { @Nullable private UUID owner; 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 @@ -116,13 +116,11 @@ public class ItemEntity extends Entity { 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(); @@ -176,11 +174,11 @@ public class ItemEntity extends Entity { 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) { @@ -207,13 +205,14 @@ public class ItemEntity extends Entity { // 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 @@ -514,14 +513,20 @@ public class ItemEntity extends Entity { 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 bedee2c93bd0aff148f93dcf111e0fc3d9bce4a0..718701a39d0c5369600119330cec7f8015fd95b0 100644 --- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java @@ -59,7 +59,7 @@ public class PrimedTnt extends Entity { @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)); } @@ -101,7 +101,7 @@ public class PrimedTnt extends Entity { */ // 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..1f1e3d6e5e94b985a5c929ab266a996471432923 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.RegionisedWorldData 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 18eac340386a396c9850f53f30d20a41c1437788..81efaadf67f7bcc6097c19c05ba2fdb6b34d8218 100644 --- a/src/main/java/net/minecraft/world/entity/npc/Villager.java +++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java @@ -710,6 +710,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler ServerLevel worldserver = minecraftserver.getLevel(globalpos.dimension()); if (worldserver != null) { + io.papermc.paper.threadedregions.RegionisedServer.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); @@ -718,6 +720,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 0ae8e9134a3671cdf2a480cd4dd6598653e261ab..d9d832b7978d03417912408564f6e21bb5e52dc3 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.RegionisedWorldData 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.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 //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 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/EvokerFangs.java b/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java index c7265a650a5d6bdc42d41c5c90cad401d7f1c99d..c95d80ee142dc056874af6baf2d058cc932985e9 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java +++ b/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java @@ -128,9 +128,9 @@ public class EvokerFangs extends Entity { 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(DamageSource.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 5406925cd66f46ab8744123c670d72cea7bfc3a1..d0fa197283a3bf14ead356e832500430ecae3f86 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java +++ b/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java @@ -130,6 +130,10 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier { }); } + if (this.attachedToEntity != null && !io.papermc.paper.util.TickThread.isTickThreadFor(this.attachedToEntity)) { // Folia start - region threading + this.attachedToEntity = null; + } + // Folia end - region threading if (this.attachedToEntity != null) { if (this.attachedToEntity.isFallFlying()) { @@ -241,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(DamageSource.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; @@ -270,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(DamageSource.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/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java index 66476b33cede1e44db5ec166a0cea81f82ffe47a..26a17e3098317f6f623cdcab59dceb9d213c7f63 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java @@ -52,8 +52,19 @@ public abstract class Projectile extends Entity { } + // 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 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) { @@ -273,6 +284,6 @@ public abstract class Projectile extends Entity { 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 00ac1cdc4734cc57f15433c5c6e7a3a545739d33..39b0034b7c612759fed87b6a5fff1819583f3f85 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/ThrownEnderpearl.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java index f224ebbc0efefddede43d87f0300c014077b9931..2627610b77e779722bb33eeb1096d862aa9639d2 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java @@ -44,6 +44,62 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { entityHitResult.getEntity().hurt(DamageSource.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 = EntityType.ENDERMITE.create(world); + + if (entityendermite != null) { + entityendermite.moveTo(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()); + world.addFreshEntity(entityendermite, CreatureSpawnEvent.SpawnReason.ENDER_PEARL); + } + } + + // damage player + player.hurt(DamageSource.FALL, 5.0F); + } + } + ); + }, + null, + 1L + ); + } + // Folia end - region threading + @Override protected void onHit(HitResult hitResult) { super.onHit(hitResult); @@ -53,6 +109,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) { @@ -84,9 +154,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(DamageSource.FALL, 5.0F); - CraftEventFactory.entityDamage = null; + CraftEventFactory.entityDamageRT.set(null); // Folia - region threading } // CraftBukkit end } @@ -112,6 +182,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 08b18428e867baf14f551beb72e3875b0c420639..941e74fe6f66a1e7d1a909e2dcf494eba0704058 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 e5ccbaf72f29731f1d1aa939b9297b644a408cd4..1792655d2f0357b388b3c83886cac4bc109e1aa9 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 feb89eb69994bdd1d2f95d2b9992e69251b2bee7..39cdb5c0080613662eaefc4f94d17fa1bd25ed30 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/MinecartHopper.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java index 70f1916185b79bbb9f033f4ef8119d7b17a13ef8..54d55e8827f4ab286fca722f199aac42cddab8d2 100644 --- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java +++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java @@ -155,7 +155,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.RegionisedServer.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 9c8604376228c02f8bbd9a15673fbdf5097e7cb2..40410ce889ef18344291f04d29938b4d1d3c9766 100644 --- a/src/main/java/net/minecraft/world/item/ArmorItem.java +++ b/src/main/java/net/minecraft/world/item/ArmorItem.java @@ -63,7 +63,7 @@ public class ArmorItem extends Item implements Wearable { 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 6860096cb8c0deecc9c1d87543d1128fb95fd2d4..00cc67322d7de29c30b54aa7da62cc44d6469a1d 100644 --- a/src/main/java/net/minecraft/world/item/ItemStack.java +++ b/src/main/java/net/minecraft/world/item/ItemStack.java @@ -333,6 +333,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); @@ -344,12 +345,13 @@ public final class ItemStack { CompoundTag oldData = this.getTagClone(); int oldCount = this.getCount(); ServerLevel world = (ServerLevel) itemactioncontext.getLevel(); + io.papermc.paper.threadedregions.RegionisedWorldData 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(); @@ -358,14 +360,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(); + 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; @@ -392,12 +394,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 @@ -408,13 +410,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(); @@ -429,7 +431,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()); } @@ -490,8 +492,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..df4632a6ddef8744df160163c99bdcdd6225dd97 100644 --- a/src/main/java/net/minecraft/world/level/BaseCommandBlock.java +++ b/src/main/java/net/minecraft/world/level/BaseCommandBlock.java @@ -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"); 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 a213f4098859858a73ddd601bbe8c7511972e0d5..07aa859ccfd3283097c172672c5d80130187cd4c 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) { @@ -259,7 +259,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; } @@ -503,17 +503,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 60003ff929f7ac6b34f9230c53ccbd54dc9e176b..54f50326beaef3985277ff941e40415a671f31fb 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -116,10 +116,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; @@ -129,7 +129,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(); @@ -141,7 +141,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { private final WorldBorder worldBorder; private final BiomeManager biomeManager; private final ResourceKey dimension; - 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; @@ -150,20 +150,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; @@ -177,9 +167,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 @@ -223,7 +213,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); @@ -239,7 +229,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; @@ -270,6 +260,24 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public abstract ResourceKey getTypeKey(); + // Folia start - region ticking + public final io.papermc.paper.threadedregions.RegionisedData worldRegionData + = new io.papermc.paper.threadedregions.RegionisedData<>( + (ServerLevel)this, () -> new io.papermc.paper.threadedregions.RegionisedWorldData((ServerLevel)Level.this), + io.papermc.paper.threadedregions.RegionisedWorldData.REGION_CALLBACK + ); + public volatile io.papermc.paper.threadedregions.RegionisedServer.WorldLevelData tickData; + + public io.papermc.paper.threadedregions.RegionisedWorldData getCurrentWorldData() { + return io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); + } + + @Override + public List getLocalPlayers() { + return this.getCurrentWorldData().getLocalPlayers(); + } + // Folia end - region ticking + protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, 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 @@ -313,7 +321,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 // CraftBukkit start this.getWorldBorder().world = (ServerLevel) this; // From PlayerList.setPlayerFileData @@ -452,8 +460,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { @Nullable public final BlockState getBlockStateIfLoaded(BlockPos blockposition) { // CraftBukkit start - tree generation - if (captureTreeGeneration) { - CraftBlockState previous = capturedBlockStates.get(blockposition); + if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading + CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(blockposition); // Folia - region threading if (previous != null) { return previous.getHandle(); } @@ -514,16 +522,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable { @Override public boolean setBlock(BlockPos pos, BlockState state, int flags, int maxUpdateDepth) { + io.papermc.paper.threadedregions.RegionisedWorldData 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.setData(state); return true; @@ -539,10 +548,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 @@ -552,8 +561,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; @@ -596,7 +605,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 { @@ -645,7 +654,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); @@ -659,7 +668,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 @@ -738,7 +747,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 @@ -763,11 +772,24 @@ public abstract class Level implements LevelAccessor, AutoCloseable { return this.getChunkSource().getLightEngine(); } + // Folia start - region threading + @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(); } @@ -858,7 +880,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() { @@ -866,11 +888,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.RegionisedWorldData regionisedWorldData = ((ServerLevel)this).getCurrentWorldData(); // Folia - regionised ticking + regionisedWorldData.seTtickingBlockEntities(true); // Folia - regionised ticking + regionisedWorldData.pushPendingTickingBlockEntities(); // Folia - regionised ticking + List blockEntityTickers = regionisedWorldData.getBlockEntityTickers(); // Folia - regionised ticking timings.tileEntityPending.stopTiming(); // Spigot timings.tileEntityTick.startTiming(); // Spigot @@ -879,9 +900,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"); @@ -898,19 +918,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 + regionisedWorldData.seTtickingBlockEntities(false); // Folia - regionised ticking + //co.aikar.timings.TimingHistory.tileEntityTicks += this.blockEntityTickers.size(); // Paper // Folia - region threading gameprofilerfiller.pop(); - spigotConfig.currentPrimedTnt = 0; // Spigot + regionisedWorldData.currentPrimedTnt = 0; // Spigot // Folia - region threading } public void guardEntityTick(Consumer tickConsumer, T entity) { @@ -1008,7 +1028,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public BlockEntity getBlockEntity(BlockPos blockposition, boolean validate) { // 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 @@ -1021,8 +1041,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 @@ -1226,13 +1246,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.RegionisedWorldData 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.RegionisedWorldData 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; @@ -1438,8 +1475,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 @@ -1470,7 +1506,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { @Override public long nextSubTickCount() { - return (long) (this.subTickCount++); + return this.subTickCount.getAndIncrement(); // Folia - region threading } public static enum ExplosionInteraction { 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 01b21f520ef1c834b9bafc3de85c1fa4fcf539d6..99bc2e30e3a35929de7cff65bf0a69f25d93d5c2 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 bad7031426ae6c750ae4376beb238186e7d65270..070d8fe9e016ad03be2c1fb5f22379f80ad3f155 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 7b71073027f4cf79736546500ededdfbb83d968e..6c6b7b94e875dce36619461e3994b148148e7f8a 100644 --- a/src/main/java/net/minecraft/world/level/block/Block.java +++ b/src/main/java/net/minecraft/world/level/block/Block.java @@ -398,8 +398,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 1ec242205b82a5a1f10deb2312795cc5dc157a76..ae08011006851493ad315f2490a4374877b2a02d 100644 --- a/src/main/java/net/minecraft/world/level/block/CactusBlock.java +++ b/src/main/java/net/minecraft/world/level/block/CactusBlock.java @@ -118,9 +118,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(DamageSource.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 a4c44cb59dee29cf227dbb51bfc1576d89dfb2e3..3d3f85e10c56dc95a0b6e576bd4b8d33a9daa8a3 100644 --- a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java +++ b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java @@ -95,9 +95,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(DamageSource.IN_FIRE, (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 16504b8be08064e61b013fa943f692816612cbd0..076c6209725bac9268c19c7bfec7b5a94f2ea9a1 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 e234ae13fe9793db237adb6f6216fa32638cfc4f..9447bc16dcd7ecfa941081197d5e4c34f78c79d4 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/HoneyBlock.java b/src/main/java/net/minecraft/world/level/block/HoneyBlock.java index 683f24251baf8ef3bef8f32ba83dc7f0e8ed7d70..b0352a9f05ad1ec49314d505dc99ed7b29ed09c3 100644 --- a/src/main/java/net/minecraft/world/level/block/HoneyBlock.java +++ b/src/main/java/net/minecraft/world/level/block/HoneyBlock.java @@ -81,7 +81,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 d3540a4daaa8021ae009bfd4d9ef4f1172ab4c56..336a4797d114ccfad319086c68e3546bdf3f6fe1 100644 --- a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java +++ b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java @@ -29,9 +29,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(DamageSource.HOT_FLOOR, 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/PointedDripstoneBlock.java b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java index e78fdd317d59cfca6a28deb6e0bd02aabe91e930..c2cb07426d22ff0c14dfa24cc2ead785eaaf1903 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, DamageSource.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..1c23d52b77fea5f9817b482e2904083237ee58c4 100644 --- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java @@ -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; } @@ -286,7 +286,7 @@ public class RedStoneWireBlock extends Block { int k = worldIn.getBestNeighborSignal(pos1); this.shouldSignal = true; - 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; @@ -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 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..01d8895fc04830d6f691852aaadf21d3ede75808 100644 --- a/src/main/java/net/minecraft/world/level/block/SaplingBlock.java +++ b/src/main/java/net/minecraft/world/level/block/SaplingBlock.java @@ -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.RegionisedWorldData 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) { + worldData.captureTreeGeneration = false; // Folia - region threading + if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading TreeType treeType = SaplingBlock.treeType; SaplingBlock.treeType = null; 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..49a8d37b9c77dbc869c03e9f495efeeef606fa4a 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.RegionisedServer.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 c926cd3ebb916115a608e86b389ffe7e15d48cd7..5375f79c6d5e05f4202a6363eaf50e6e6548a789 100644 --- a/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java +++ b/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java @@ -86,9 +86,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(DamageSource.SWEET_BERRY_BUSH, 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 928625b5ab054ffa412be8a438f58291cc7a3cc0..bbc051be1c3a2697e33cc316e328760b838af243 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 58986bc0677c5ea1ad54d7d6d4efa5c2ea233aea..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 IGNORE_TILE_UPDATES = false; // 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 (IGNORE_TILE_UPDATES) 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 55006724ccec9f3de828ec18693728e9741ff65f..9e806ba83240916d422c4a736a9cfd61e73108e4 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 @@ -54,7 +54,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; @@ -171,11 +171,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/ConduitBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java index 05eab04e4aec4151018f25b59f92ddbbb4c09f87..2a6e3eb2ec74e8b1a356b5a62ec5f8521da00380 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 @@ -88,7 +88,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) { @@ -106,7 +106,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) { @@ -236,11 +236,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(DamageSource.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 ccad692aba2ed77259f6814d88f01b91ed9d229b..955f6560f4c6032d375927987c9bb62561ed5034 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 @@ -189,12 +189,11 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen return false; } // Paper start - Optimize Hoppers - private static boolean skipPullModeEventFire = false; - private static boolean skipPushModeEventFire = false; - public static boolean skipHopperEvents = false; + // Folia - region threading - moved to RegionisedWorldData private static boolean hopperPush(Level level, BlockPos pos, Container destination, Direction enumdirection, HopperBlockEntity hopper) { - skipPushModeEventFire = skipHopperEvents; + io.papermc.paper.threadedregions.RegionisedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading + worldData.skipPushModeEventFire = worldData.skipHopperEvents; // Folia - region threading boolean foundItem = false; for (int i = 0; i < hopper.getContainerSize(); ++i) { ItemStack item = hopper.getItem(i); @@ -209,7 +208,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 itemstack = callPushMoveEvent(destination, itemstack, hopper); if (itemstack == null) { // cancelled origItemStack.setCount(origCount); @@ -238,12 +237,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen } private static boolean hopperPull(Level level, Hopper ihopper, Container iinventory, ItemStack origItemStack, int i) { + io.papermc.paper.threadedregions.RegionisedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading ItemStack itemstack = origItemStack; final int origCount = origItemStack.getCount(); final int moved = Math.min(level.spigotConfig.hopperAmount, origCount); itemstack.setCount(moved); - if (!skipPullModeEventFire) { + if (!worldData.skipPullModeEventFire) { // Folia - region threading itemstack = callPullMoveEvent(ihopper, iinventory, itemstack); if (itemstack == null) { // cancelled origItemStack.setCount(origCount); @@ -262,9 +262,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen if (!origItemStack.isEmpty()) { origItemStack.setCount(origCount - moved + remaining); } - IGNORE_TILE_UPDATES = true; + IGNORE_TILE_UPDATES.set(true); // Folia - region threading iinventory.setItem(i, origItemStack); - IGNORE_TILE_UPDATES = false; + IGNORE_TILE_UPDATES.set(false); // Folia - region threading iinventory.setChanged(); return true; } @@ -278,12 +278,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen } private static ItemStack callPushMoveEvent(Container iinventory, ItemStack itemstack, HopperBlockEntity hopper) { + io.papermc.paper.threadedregions.RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); // Folia - region threading Inventory destinationInventory = getInventory(iinventory); InventoryMoveItemEvent event = new InventoryMoveItemEvent(hopper.getOwner(false).getInventory(), CraftItemStack.asCraftMirror(itemstack), destinationInventory, true); boolean result = event.callEvent(); if (!event.calledGetItem && !event.calledSetItem) { - skipPushModeEventFire = true; + worldData.skipPushModeEventFire = true; // Folia - region threading } if (!result) { cooldownHopper(hopper); @@ -298,6 +299,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen } private static ItemStack callPullMoveEvent(Hopper hopper, Container iinventory, ItemStack itemstack) { + io.papermc.paper.threadedregions.RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); // Folia - region threading Inventory sourceInventory = getInventory(iinventory); Inventory destination = getInventory(hopper); @@ -306,7 +308,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen CraftItemStack.asCraftMirror(itemstack), destination, false); boolean result = event.callEvent(); if (!event.calledGetItem && !event.calledSetItem) { - skipPullModeEventFire = true; + worldData.skipPullModeEventFire = true; // Folia - region threading } if (!result) { cooldownHopper(hopper); @@ -447,13 +449,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen // Paper end public static boolean suckInItems(Level world, Hopper hopper) { + io.papermc.paper.threadedregions.RegionisedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionisedWorldData(); // 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(iinventory, item, i, enumdirection)) { @@ -592,9 +595,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen stack = stack.split(to.getMaxStackSize()); } // Spigot end - IGNORE_TILE_UPDATES = true; // Paper + IGNORE_TILE_UPDATES.set(true); // Paper // Folia - region threading to.setItem(slot, stack); - IGNORE_TILE_UPDATES = 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 163e63e3c538c7c1c75ed634896db9d8c00416f3..740cb2858a3f78298895a463fb0fac9e88a9a4a0 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 f80545f80948db27d1fbde77d0505c916eb504ed..3b656e7d5e8b75f8f415d5f43ed5c91da963731b 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.RegionisedServer.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,125 @@ 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(vec3d.x + 0.5D, 75.0D, 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.RegionisedServer.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/piston/PistonMovingBlockEntity.java b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java index 221c5d080d55326e458c1182823d6b49224ef498..29a27534e6c97b262229b51e4ea0345502020647 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.RegionisedServer.getCurrentTick() + 10); // Folia - region threading + entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, io.papermc.paper.threadedregions.RegionisedServer.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 7a12a4da4864306ec6589ca81368e84718825047..e3ef5ccad7f8a8138b1372f419680409ddeab291 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 7e9c388179c75a233d9b179ea1e00428ac65ee99..df83966cb2be368aaee95f3b8563e01ab807d816 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java @@ -308,7 +308,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); @@ -319,7 +319,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 e776eb8afef978938da084f9ae29d611181b43fe..d270f6b5937e167f18c3f358c99a9f6f3cde9c7a 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; } @@ -1213,6 +1171,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(); @@ -1249,6 +1214,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 256642f2e2aa66f7e8c00cae91a75060a8817c9c..9fb91e3648db3ad79bb6d1fe79894a13ddc07cbb 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 @@ -687,7 +687,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..3ff5e74a2aae72eebe6730a4df15b17c1c8ff43a 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; } 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..b2a9cd719c4968a1cde8f0b30f46f01d5872fbc9 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.RegionisedWorldData 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 1c3718d9244513d9fc795dceb564a81375734557..f445e1db1538fb9eda4b2f81f62748dc57fda24a 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.RegionisedWorldData 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 4761aa772bc34dd66547dd4dd561c2e04c3229ad..ac4648773c9d6316db4915d515991219589fa473 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 @@ -51,8 +51,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; @@ -71,7 +164,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 { @@ -79,9 +172,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; @@ -194,17 +287,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 c8cdcf40e45f5c6270f9b124f0333643266e2858..b370ba924b03cc0a36666ee6ed26be06a6affaa7 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())); compoundTag.putInt("DataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); @@ -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 b2845ed8d28627178589da3d2224cd9edd29c31e..5121569f389ee4a50273432a9a272a936542fa12 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, (int) 0, (int) 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 2da78bc43af715fe399eac1d83b3bf6e8fb8afac..433a9302f496a297172c02f3fe0404174cc7a8f1 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 7f1ac2cb29eb84833c0895442d611dfa0504527e..c79cfebc65fd04994735dabcf5bb6e6cc714aca8 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.RegionisedWorldData 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 2ea3778ee1348e5d06b15a2c5dc5d9bd4136dbe3..c4c2a393ee2d5fbcdbf3abb4a49f1bfae2d2c618 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -879,6 +879,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.RegionisedServer.ensureGlobalTickThread("May not dispatch server commands async"); + // Folia end - region threading if (sender instanceof Conversable) { Conversable conversable = (Conversable) sender; @@ -898,12 +901,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.RegionisedServer.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.RegionisedServer.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; @@ -2913,7 +2948,7 @@ public final class CraftServer implements Server { @Override public int getCurrentTick() { - return net.minecraft.server.MinecraftServer.currentTick; + return (int)io.papermc.paper.threadedregions.RegionisedServer.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 d33476ffa49d7f6388bb227f8a57cf115a74698f..ddb59118551449b4c8855cdeaabedb08af148fff 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 @@ -788,13 +788,14 @@ 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.threadedregions.RegionisedWorldData 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 +803,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; } } @@ -878,7 +879,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public long getGameTime() { - return world.levelData.getGameTime(); + return this.getHandle().getGameTime(); // Folia - region threading } @Override @@ -1853,7 +1854,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); } @@ -2356,7 +2357,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.RegionisedServer.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/CraftBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java index 350cbf64c17938021002d5fd67176c44b398231e..e54713a530e18344a7c7d1c400147fc33d64967f 100644 --- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java @@ -568,16 +568,17 @@ public class CraftBlock implements Block { 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.RegionisedWorldData 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) { + if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading TreeType treeType = SaplingBlock.treeType; SaplingBlock.treeType = null; - List blocks = new ArrayList<>(world.capturedBlockStates.values()); - world.capturedBlockStates.clear(); + List blocks = new ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading StructureGrowEvent structureEvent = null; if (treeType != null) { diff --git a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java index cd4ad8261e56365850068db1d83d6a8454026737..c098ae9f057a3dcc77c61555feb870452e947ae7 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.RegionisedServer.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.RegionisedServer.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 78f53ee557276de85f0431ebcb146445b1f4fb92..ab6db7c5193a7c4b3f9433c6997dd24c76a84904 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java @@ -200,6 +200,7 @@ 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 public CraftEntity(final CraftServer server, final Entity entity) { this.server = server; @@ -556,6 +557,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { @Override public boolean teleport(Location location, TeleportCause cause, boolean ignorePassengers, boolean dismount) { + // 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(); @@ -1206,7 +1212,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; @@ -1270,30 +1276,38 @@ 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; + boolean scheduled = this.taskScheduler.schedule( + // success + (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 entityTp) -> { + ret.complete(Boolean.TRUE); } - net.minecraft.server.MinecraftServer.LOGGER.error("Failed to teleport entity " + CraftEntity.this, throwable); - ret.completeExceptionally(throwable); + ); + if (!success) { + ret.complete(Boolean.FALSE); } - }); - }); + }, + // 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 0351eb67bac6ce257f820af60aa3bba9f45da687..88006751825515966dcea1f779ac5452c8ddd964 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -1702,7 +1702,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()); } @@ -1765,7 +1765,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 6a52ae70b5f7fd9953b6b2605cae722f606e7fec..2a6fe4a3fdba9d0027a2e445b694afc80a18053c 100644 --- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java @@ -228,8 +228,8 @@ import org.bukkit.event.entity.SpawnerSpawnEvent; // Spigot public class CraftEventFactory { public static final DamageSource MELTING = CraftDamageSource.copyOf(DamageSource.ON_FIRE); public static final DamageSource POISON = CraftDamageSource.copyOf(DamageSource.MAGIC); - 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 + public static final ThreadLocal entityDamageRT = new ThreadLocal<>(); // For use in EntityDamageByEntityEvent // helper methods private static boolean canBuild(ServerLevel world, Player player, int x, int z) { @@ -842,7 +842,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)) { @@ -853,7 +853,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()) { @@ -968,8 +968,8 @@ public class CraftEventFactory { private static EntityDamageEvent handleEntityDamageEvent(Entity entity, DamageSource source, Map modifiers, Map> modifierFunctions, boolean cancelled) { if (source.isExplosion()) { 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); @@ -1022,13 +1022,13 @@ public class CraftEventFactory { } return event; } else if (source == DamageSource.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); @@ -1036,9 +1036,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 == DamageSource.CACTUS || source == DamageSource.SWEET_BERRY_BUSH || source == DamageSource.STALAGMITE || "fallingStalactite".equals(source.msgId) || "anvil".equals(source.msgId)) { cause = DamageCause.CONTACT; } else if (source == DamageSource.HOT_FLOOR) { @@ -1053,9 +1053,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); @@ -1063,10 +1063,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(); + CraftEventFactory.entityDamageRT.set(null); // Folia - region threading if ("fallingStalactite".equals(source.msgId) || "fallingBlock".equals(source.msgId) || "anvil".equals(source.msgId)) { 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/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java index e881584d38dc354204479863f004e974a0ac6c07..7d99ba41a3178f5321403eb7749f0a4b898ad0a9 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.RegionisedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions SpigotWorldConfig config = world.spigotConfig; - long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; + long inactiveFor = io.papermc.paper.threadedregions.RegionisedServer.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.RegionisedWorldData 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.RegionisedServer.getCurrentTick(); // Folia - region threading if ( world.spigotConfig.ignoreSpectatorActivation && player.isSpectator() ) { continue; @@ -215,7 +217,7 @@ public class ActivationRange java.util.List entities = world.getEntities((Entity)null, maxBB, (e) -> !(e instanceof net.minecraft.world.entity.Marker)); // Don't tick markers 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 +231,16 @@ public class ActivationRange */ private static void activateEntity(Entity entity) { - if ( MinecraftServer.currentTick > entity.activatedTick ) + if ( io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick() > entity.activatedTick ) // Folia - threaded regions { if ( entity.defaultActivationState ) { - entity.activatedTick = MinecraftServer.currentTick; + entity.activatedTick = io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick(); // Folia - threaded regions return; } if ( entity.activationType.boundingBox.intersects( entity.getBoundingBox() ) ) { - entity.activatedTick = MinecraftServer.currentTick; + entity.activatedTick = io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick(); // Folia - threaded regions } } } @@ -261,10 +263,10 @@ public class ActivationRange if (entity.remainingFireTicks > 0) { return 2; } - if (entity.activatedImmunityTick >= MinecraftServer.currentTick) { + if (entity.activatedImmunityTick >= io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick()) { // Folia - threaded regions return 1; } - long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; + long inactiveFor = io.papermc.paper.threadedregions.RegionisedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions // Paper end // quick checks. if ( (entity.activationType != ActivationType.WATER && entity.wasTouchingWater && entity.isPushedByFluid()) ) // Paper @@ -387,19 +389,19 @@ public class ActivationRange } // Paper end - boolean isActive = entity.activatedTick >= MinecraftServer.currentTick; + boolean isActive = entity.activatedTick >= io.papermc.paper.threadedregions.RegionisedServer.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.RegionisedServer.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.RegionisedServer.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..72976bdb3db5d0066599272fab1055b2e20c5b26 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.RegionisedServer.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 )