mirror of
https://github.com/PaperMC/Folia.git
synced 2024-11-19 11:35:18 +01:00
c435aaae96
This is to mirror behavior of RegionizedData's world check.
23765 lines
1.2 MiB
23765 lines
1.2 MiB
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
|
|
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<E> implements Queue<E> {
|
|
}
|
|
}
|
|
|
|
+ /**
|
|
+ * 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<E> tail = this.getTailOpaque();;) {
|
|
+ LinkedNode<E> next = tail.getNextVolatile();
|
|
+ if (next == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (next == tail) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ tail = next;
|
|
+ }
|
|
+ }
|
|
+
|
|
/**
|
|
* Atomically removes the head from this queue if it exists, otherwise prevents additions to this queue if no
|
|
* head is removed.
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/AreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/AreaLock.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..6a155b779914828a0d4199bdfcb0d6fca25e1581
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/AreaLock.java
|
|
@@ -0,0 +1,146 @@
|
|
+package ca.spottedleaf.concurrentutil.lock;
|
|
+
|
|
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
|
|
+import java.util.ArrayList;
|
|
+import java.util.List;
|
|
+import java.util.concurrent.locks.LockSupport;
|
|
+
|
|
+public final class AreaLock {
|
|
+
|
|
+ private final int coordinateShift;
|
|
+
|
|
+ private final Long2ReferenceOpenHashMap<Node> nodesByPosition = new Long2ReferenceOpenHashMap<>(1024, 0.10f);
|
|
+
|
|
+ public AreaLock(final int coordinateShift) {
|
|
+ this.coordinateShift = coordinateShift;
|
|
+ }
|
|
+
|
|
+ private static long key(final int x, final int z) {
|
|
+ return ((long)z << 32) | (x & 0xFFFFFFFFL);
|
|
+ }
|
|
+
|
|
+ public Node lock(final int x, final int z, final int radius) {
|
|
+ final Thread thread = Thread.currentThread();
|
|
+ final int minX = (x - radius) >> this.coordinateShift;
|
|
+ final int minZ = (z - radius) >> this.coordinateShift;
|
|
+ final int maxX = (x + radius) >> this.coordinateShift;
|
|
+ final int maxZ = (z + radius) >> this.coordinateShift;
|
|
+
|
|
+ final Node node = new Node(x, z, radius, thread);
|
|
+
|
|
+ synchronized (this) {
|
|
+ ReferenceOpenHashSet<Node> parents = null;
|
|
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
|
|
+ for (int currX = minX; currX <= maxX; ++currX) {
|
|
+ final Node dependency = this.nodesByPosition.put(key(currX, currZ), node);
|
|
+ if (dependency == null) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (parents == null) {
|
|
+ parents = new ReferenceOpenHashSet<>();
|
|
+ }
|
|
+
|
|
+ if (parents.add(dependency)) {
|
|
+ // added a dependency, so we need to add as a child to the dependency
|
|
+ if (dependency.children == null) {
|
|
+ dependency.children = new ArrayList<>();
|
|
+ }
|
|
+ dependency.children.add(node);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (parents == null) {
|
|
+ // no dependencies, so we can just return immediately
|
|
+ return node;
|
|
+ } // else: we need to lock
|
|
+
|
|
+ node.parents = parents;
|
|
+ }
|
|
+
|
|
+ while (!node.unlocked) {
|
|
+ LockSupport.park(node);
|
|
+ }
|
|
+
|
|
+ return node;
|
|
+ }
|
|
+
|
|
+ public void unlock(final Node node) {
|
|
+ List<Node> toUnpark = null;
|
|
+
|
|
+ final int x = node.x;
|
|
+ final int z = node.z;
|
|
+ final int radius = node.radius;
|
|
+
|
|
+ final int minX = (x - radius) >> this.coordinateShift;
|
|
+ final int minZ = (z - radius) >> this.coordinateShift;
|
|
+ final int maxX = (x + radius) >> this.coordinateShift;
|
|
+ final int maxZ = (z + radius) >> this.coordinateShift;
|
|
+
|
|
+ synchronized (this) {
|
|
+ final List<Node> children = node.children;
|
|
+ if (children != null) {
|
|
+ // try to unlock children
|
|
+ for (int i = 0, len = children.size(); i < len; ++i) {
|
|
+ final Node child = children.get(i);
|
|
+ if (!child.parents.remove(node)) {
|
|
+ throw new IllegalStateException();
|
|
+ }
|
|
+ if (child.parents.isEmpty()) {
|
|
+ // we can unlock, as it now has no dependencies in front
|
|
+ child.parents = null;
|
|
+ if (toUnpark == null) {
|
|
+ toUnpark = new ArrayList<>();
|
|
+ toUnpark.add(child);
|
|
+ } else {
|
|
+ toUnpark.add(child);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // remove node from dependency map
|
|
+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
|
|
+ for (int currX = minX; currX <= maxX; ++currX) {
|
|
+ // node: we only remove if we match, as a mismatch indicates a child node which of course has not
|
|
+ // yet been unlocked
|
|
+ this.nodesByPosition.remove(key(currX, currZ), node);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (toUnpark == null) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // we move the unpark / unlock logic here because we want to avoid performing work while holding the lock
|
|
+
|
|
+ for (int i = 0, len = toUnpark.size(); i < len; ++i) {
|
|
+ final Node toUnlock = toUnpark.get(i);
|
|
+ toUnlock.unlocked = true; // must be volatile and before unpark()
|
|
+ LockSupport.unpark(toUnlock.thread);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static final class Node {
|
|
+
|
|
+ public final int x;
|
|
+ public final int z;
|
|
+ public final int radius;
|
|
+ public final Thread thread;
|
|
+
|
|
+ private List<Node> children;
|
|
+ private ReferenceOpenHashSet<Node> parents;
|
|
+
|
|
+ private volatile boolean unlocked;
|
|
+
|
|
+ public Node(final int x, final int z, final int radius, final Thread thread) {
|
|
+ this.x = x;
|
|
+ this.z = z;
|
|
+ this.radius = radius;
|
|
+ this.thread = thread;
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..7869cc177c95e26dd9e1d3db5b50e996956edb24
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRInt2IntHashTable.java
|
|
@@ -0,0 +1,664 @@
|
|
+package ca.spottedleaf.concurrentutil.map;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.ArrayUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+import io.papermc.paper.util.IntegerUtil;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.Arrays;
|
|
+import java.util.function.Consumer;
|
|
+import java.util.function.IntConsumer;
|
|
+
|
|
+public class SWMRInt2IntHashTable {
|
|
+
|
|
+ protected int size;
|
|
+
|
|
+ protected TableEntry[] table;
|
|
+
|
|
+ protected final float loadFactor;
|
|
+
|
|
+ protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRInt2IntHashTable.class, "size", int.class);
|
|
+ protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRInt2IntHashTable.class, "table", TableEntry[].class);
|
|
+
|
|
+ /* size */
|
|
+
|
|
+ protected final int getSizePlain() {
|
|
+ return (int)SIZE_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ protected final int getSizeOpaque() {
|
|
+ return (int)SIZE_HANDLE.getOpaque(this);
|
|
+ }
|
|
+
|
|
+ protected final int getSizeAcquire() {
|
|
+ return (int)SIZE_HANDLE.getAcquire(this);
|
|
+ }
|
|
+
|
|
+ protected final void setSizePlain(final int value) {
|
|
+ SIZE_HANDLE.set(this, value);
|
|
+ }
|
|
+
|
|
+ protected final void setSizeOpaque(final int value) {
|
|
+ SIZE_HANDLE.setOpaque(this, value);
|
|
+ }
|
|
+
|
|
+ protected final void setSizeRelease(final int value) {
|
|
+ SIZE_HANDLE.setRelease(this, value);
|
|
+ }
|
|
+
|
|
+ /* table */
|
|
+
|
|
+ protected final TableEntry[] getTablePlain() {
|
|
+ //noinspection unchecked
|
|
+ return (TableEntry[])TABLE_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ protected final TableEntry[] getTableAcquire() {
|
|
+ //noinspection unchecked
|
|
+ return (TableEntry[])TABLE_HANDLE.getAcquire(this);
|
|
+ }
|
|
+
|
|
+ protected final void setTablePlain(final TableEntry[] table) {
|
|
+ TABLE_HANDLE.set(this, table);
|
|
+ }
|
|
+
|
|
+ protected final void setTableRelease(final TableEntry[] table) {
|
|
+ TABLE_HANDLE.setRelease(this, table);
|
|
+ }
|
|
+
|
|
+ protected static final int DEFAULT_CAPACITY = 16;
|
|
+ protected static final float DEFAULT_LOAD_FACTOR = 0.75f;
|
|
+ protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1;
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}.
|
|
+ */
|
|
+ public SWMRInt2IntHashTable() {
|
|
+ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with the specified capacity and load factor of {@code 0.75f}.
|
|
+ * @param capacity specified initial capacity, > 0
|
|
+ */
|
|
+ public SWMRInt2IntHashTable(final int capacity) {
|
|
+ this(capacity, DEFAULT_LOAD_FACTOR);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with the specified capacity and load factor.
|
|
+ * @param capacity specified capacity, > 0
|
|
+ * @param loadFactor specified load factor, > 0 && finite
|
|
+ */
|
|
+ public SWMRInt2IntHashTable(final int capacity, final float loadFactor) {
|
|
+ final int tableSize = getCapacityFor(capacity);
|
|
+
|
|
+ if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) {
|
|
+ throw new IllegalArgumentException("Invalid load factor: " + loadFactor);
|
|
+ }
|
|
+
|
|
+ //noinspection unchecked
|
|
+ final TableEntry[] table = new TableEntry[tableSize];
|
|
+ this.setTablePlain(table);
|
|
+
|
|
+ if (tableSize == MAXIMUM_CAPACITY) {
|
|
+ this.threshold = -1;
|
|
+ } else {
|
|
+ this.threshold = getTargetCapacity(tableSize, loadFactor);
|
|
+ }
|
|
+
|
|
+ this.loadFactor = loadFactor;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and
|
|
+ * with a load factor of {@code 0.75f}.
|
|
+ * All of the specified map's entries are copied into this map.
|
|
+ * @param other The specified map.
|
|
+ */
|
|
+ public SWMRInt2IntHashTable(final SWMRInt2IntHashTable other) {
|
|
+ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and
|
|
+ * with a load factor of {@code 0.75f}.
|
|
+ * All of the specified map's entries are copied into this map.
|
|
+ * @param capacity specified capacity, > 0
|
|
+ * @param other The specified map.
|
|
+ */
|
|
+ public SWMRInt2IntHashTable(final int capacity, final SWMRInt2IntHashTable other) {
|
|
+ this(capacity, DEFAULT_LOAD_FACTOR, other);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and
|
|
+ * with the specified load factor.
|
|
+ * All of the specified map's entries are copied into this map.
|
|
+ * @param capacity specified capacity, > 0
|
|
+ * @param loadFactor specified load factor, > 0 && finite
|
|
+ * @param other The specified map.
|
|
+ */
|
|
+ public SWMRInt2IntHashTable(final int capacity, final float loadFactor, final SWMRInt2IntHashTable other) {
|
|
+ this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor);
|
|
+ this.putAll(other);
|
|
+ }
|
|
+
|
|
+ public final float getLoadFactor() {
|
|
+ return this.loadFactor;
|
|
+ }
|
|
+
|
|
+ protected static int getCapacityFor(final int capacity) {
|
|
+ if (capacity <= 0) {
|
|
+ throw new IllegalArgumentException("Invalid capacity: " + capacity);
|
|
+ }
|
|
+ if (capacity >= MAXIMUM_CAPACITY) {
|
|
+ return MAXIMUM_CAPACITY;
|
|
+ }
|
|
+ return IntegerUtil.roundCeilLog2(capacity);
|
|
+ }
|
|
+
|
|
+ /** Callers must still use acquire when reading the value of the entry. */
|
|
+ protected final TableEntry getEntryForOpaque(final int key) {
|
|
+ final int hash = SWMRInt2IntHashTable.getHash(key);
|
|
+ final TableEntry[] table = this.getTableAcquire();
|
|
+
|
|
+ for (TableEntry curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) {
|
|
+ if (key == curr.key) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ protected final TableEntry getEntryForPlain(final int key) {
|
|
+ final int hash = SWMRInt2IntHashTable.getHash(key);
|
|
+ final TableEntry[] table = this.getTablePlain();
|
|
+
|
|
+ for (TableEntry curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) {
|
|
+ if (key == curr.key) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ /* MT-Safe */
|
|
+
|
|
+ /** must be deterministic given a key */
|
|
+ protected static int getHash(final int key) {
|
|
+ return it.unimi.dsi.fastutil.HashCommon.mix(key);
|
|
+ }
|
|
+
|
|
+ // rets -1 if capacity*loadFactor is too large
|
|
+ protected static int getTargetCapacity(final int capacity, final float loadFactor) {
|
|
+ final double ret = (double)capacity * (double)loadFactor;
|
|
+ if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) {
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ return (int)ret;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean equals(final Object obj) {
|
|
+ if (this == obj) {
|
|
+ return true;
|
|
+ }
|
|
+ /* Make no attempt to deal with concurrent modifications */
|
|
+ if (!(obj instanceof SWMRInt2IntHashTable)) {
|
|
+ return false;
|
|
+ }
|
|
+ final SWMRInt2IntHashTable other = (SWMRInt2IntHashTable)obj;
|
|
+
|
|
+ if (this.size() != other.size()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final TableEntry[] table = this.getTableAcquire();
|
|
+
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ final int value = curr.getValueAcquire();
|
|
+
|
|
+ final int otherValue = other.get(curr.key);
|
|
+ if (value != otherValue) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public int hashCode() {
|
|
+ /* Make no attempt to deal with concurrent modifications */
|
|
+ int hash = 0;
|
|
+ final TableEntry[] table = this.getTableAcquire();
|
|
+
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ hash += curr.hashCode();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return hash;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ final StringBuilder builder = new StringBuilder(64);
|
|
+ builder.append("SingleWriterMultiReaderHashMap:{");
|
|
+
|
|
+ this.forEach((final int key, final int value) -> {
|
|
+ builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}");
|
|
+ });
|
|
+
|
|
+ return builder.append('}').toString();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public SWMRInt2IntHashTable clone() {
|
|
+ return new SWMRInt2IntHashTable(this.getTableAcquire().length, this.loadFactor, this);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ public void forEach(final Consumer<? super SWMRInt2IntHashTable.TableEntry> 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}
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ */
|
|
+ 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<V> {
|
|
return null;
|
|
}
|
|
|
|
+ protected final V remove(final long key, final int hash, final V expect) {
|
|
+ final TableEntry<V>[] table = this.getTablePlain();
|
|
+ final int index = (table.length - 1) & hash;
|
|
+
|
|
+ final TableEntry<V> 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<V> 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<V> {
|
|
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<SchedulableTick> 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<SchedulableTick> awaiting = new LinkedSortedSet<>(TICK_COMPARATOR_BY_TIME);
|
|
+ private final PriorityQueue<SchedulableTick> 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}.
|
|
+ * <p>
|
|
+ * A tickable task is expected to run on a fixed interval, which is determined by
|
|
+ * the {@link SchedulerThreadPool}.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ */
|
|
+ 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<SchedulableTick> 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.
|
|
+ * <p>
|
|
+ * It is the callee's responsibility to invoke {@link #setScheduledStart(long)} to adjust the start of
|
|
+ * the next tick.
|
|
+ * </p>
|
|
+ * @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.
|
|
+ * <p>
|
|
+ * stateTarget = null
|
|
+ * </p>
|
|
+ */
|
|
+ private static final int STATE_IDLE = 0;
|
|
+
|
|
+ /**
|
|
+ * The runner is waiting to tick a task, as it has no intermediate tasks to execute.
|
|
+ * <p>
|
|
+ * stateTarget = the task awaiting tick
|
|
+ * </p>
|
|
+ */
|
|
+ private static final int STATE_AWAITING_TICK = 1;
|
|
+
|
|
+ /**
|
|
+ * The runner is executing a tick for one of the tasks that was in its runqueue.
|
|
+ * <p>
|
|
+ * stateTarget = the task being ticked
|
|
+ * </p>
|
|
+ */
|
|
+ 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<String, ProfilerEntry> 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<String, Map<String, Integer>> 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 cab91880a08c6fdc545804911d295e0f24f4d983..8f2bff24256f0946f38e1d8b0ae9c8c921b0a2db 100644
|
|
--- a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
|
|
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
|
|
@@ -179,11 +179,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
|
|
return;
|
|
}
|
|
|
|
- if (!Bukkit.isPrimaryThread()) {
|
|
- // Plugins?
|
|
- MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo));
|
|
- return;
|
|
- }
|
|
+ // Folia - region threading
|
|
|
|
LevelChunk chunk = chunkPacketInfo.getChunk();
|
|
int x = chunk.getPos().x;
|
|
diff --git a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java
|
|
index 22a2547810d0c029f29685faddf7ac21cde2df0b..e36b4053eb2676e934b8c9c401bf58cfa7dd969c 100644
|
|
--- a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java
|
|
+++ b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java
|
|
@@ -832,14 +832,14 @@ public class RedstoneWireTurbo {
|
|
j = getMaxCurrentStrength(upd, j);
|
|
int l = 0;
|
|
|
|
- wire.shouldSignal = false;
|
|
+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading
|
|
// Unfortunately, World.isBlockIndirectlyGettingPowered is complicated,
|
|
// and I'm not ready to try to replicate even more functionality from
|
|
// elsewhere in Minecraft into this accelerator. So sadly, we must
|
|
// suffer the performance hit of this very expensive call. If there
|
|
// is consistency to what this call returns, we may be able to cache it.
|
|
final int k = worldIn.getBestNeighborSignal(upd.self);
|
|
- wire.shouldSignal = true;
|
|
+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading
|
|
|
|
// The variable 'k' holds the maximum redstone power value of any adjacent blocks.
|
|
// If 'k' has the highest level of all neighbors, then the power level of this
|
|
diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
|
|
index 4d9bc4a62ebae0f3707900503576c64733de639f..9ba89a4bf4ae4e42418eca18f9d8a109e6c56a14 100644
|
|
--- a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
|
|
+++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
|
|
@@ -98,7 +98,7 @@ public final class ChatProcessor {
|
|
final CraftPlayer player = this.player.getBukkitEntity();
|
|
final AsyncPlayerChatEvent ae = new AsyncPlayerChatEvent(this.async, player, this.craftbukkit$originalMessage, new LazyPlayerSet(this.server));
|
|
this.post(ae);
|
|
- if (listenersOnSyncEvent) {
|
|
+ if (false && listenersOnSyncEvent) { // Folia - region threading
|
|
final PlayerChatEvent se = new PlayerChatEvent(player, ae.getMessage(), ae.getFormat(), ae.getRecipients());
|
|
se.setCancelled(ae.isCancelled()); // propagate cancelled state
|
|
this.queueIfAsyncOrRunImmediately(new Waitable<Void>() {
|
|
@@ -178,7 +178,7 @@ public final class ChatProcessor {
|
|
ae.setCancelled(cancelled); // propagate cancelled state
|
|
this.post(ae);
|
|
final boolean listenersOnSyncEvent = canYouHearMe(ChatEvent.getHandlerList());
|
|
- if (listenersOnSyncEvent) {
|
|
+ if (false && listenersOnSyncEvent) { // Folia - region threading
|
|
this.queueIfAsyncOrRunImmediately(new Waitable<Void>() {
|
|
@Override
|
|
protected Void evaluate() {
|
|
diff --git a/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java
|
|
index 3c17001bcd3862a76a22df488bff80a0ff4d1b83..b2fffaa862df045bacb346f3cbe7eb963e104e47 100644
|
|
--- a/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java
|
|
+++ b/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java
|
|
@@ -23,35 +23,42 @@ public class ClickCallbackProviderImpl implements ClickCallback.Provider {
|
|
|
|
public static final class CallbackManager {
|
|
|
|
- private final Map<UUID, StoredCallback> callbacks = new HashMap<>();
|
|
- private final Queue<StoredCallback> queue = new ConcurrentLinkedQueue<>();
|
|
+ private final java.util.concurrent.ConcurrentHashMap<UUID, StoredCallback> callbacks = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading
|
|
+ // Folia - region threading
|
|
|
|
private CallbackManager() {
|
|
}
|
|
|
|
public UUID addCallback(final @NotNull ClickCallback<Audience> callback, final ClickCallback.@NotNull Options options) {
|
|
final UUID id = UUID.randomUUID();
|
|
- this.queue.add(new StoredCallback(callback, options, id));
|
|
+ final StoredCallback scb = new StoredCallback(callback, options, id); // Folia - region threading
|
|
+ this.callbacks.put(scb.id(), scb); // Folia - region threading
|
|
return id;
|
|
}
|
|
|
|
public void handleQueue(final int currentTick) {
|
|
// Evict expired entries
|
|
if (currentTick % 100 == 0) {
|
|
- this.callbacks.values().removeIf(callback -> !callback.valid());
|
|
+ this.callbacks.values().removeIf(StoredCallback::expired); // Folia - region threading - don't read uses field
|
|
}
|
|
|
|
- // Add entries from queue
|
|
- StoredCallback callback;
|
|
- while ((callback = this.queue.poll()) != null) {
|
|
- this.callbacks.put(callback.id(), callback);
|
|
- }
|
|
+ // Folia - region threading
|
|
}
|
|
|
|
public void runCallback(final @NotNull Audience audience, final UUID id) {
|
|
- final StoredCallback callback = this.callbacks.get(id);
|
|
- if (callback != null && callback.valid()) { //TODO Message if expired/invalid?
|
|
- callback.takeUse();
|
|
+ // Folia start - region threading
|
|
+ final StoredCallback[] use = new StoredCallback[1];
|
|
+ this.callbacks.computeIfPresent(id, (final UUID keyInMap, final StoredCallback value) -> {
|
|
+ if (!value.valid()) {
|
|
+ return null;
|
|
+ }
|
|
+ use[0] = value;
|
|
+ value.takeUse();
|
|
+ return value.valid() ? value : null;
|
|
+ });
|
|
+ final StoredCallback callback = use[0];
|
|
+ if (callback != null) { //TODO Message if expired/invalid?
|
|
+ // Folia end - region threading
|
|
callback.callback.accept(audience);
|
|
}
|
|
}
|
|
diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
|
|
index 61d03808c8d1ab822d9b2f31fab0de14089a3b15..9051a556fea7ee35014db7bdd75b5476f672f5a9 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/RegionizedPlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java
|
|
index 99dc2038aa4fb0f70dc3670e451018ff541d3a85..5b4025178c1e476ed5dd0808cc33bf1ec7c08b66 100644
|
|
--- a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java
|
|
+++ b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java
|
|
@@ -232,17 +232,19 @@ public class RegionizedPlayerChunkLoader {
|
|
public void tick() {
|
|
TickThread.ensureTickThread("Cannot tick player chunk loader async");
|
|
long currTime = System.nanoTime();
|
|
- for (final ServerPlayer player : this.world.players()) {
|
|
+ for (final ServerPlayer player : this.world.getLocalPlayers()) { // Folia - region threding
|
|
player.chunkLoader.update();
|
|
player.chunkLoader.midTickUpdate(currTime);
|
|
}
|
|
}
|
|
|
|
- public void tickMidTick() {
|
|
+ public boolean tickMidTick() { // Folia - region threading - report whether tickets were added
|
|
final long time = System.nanoTime();
|
|
- for (final ServerPlayer player : this.world.players()) {
|
|
- player.chunkLoader.midTickUpdate(time);
|
|
+ boolean ret = false; // Folia - region threading - report whether tickets were added
|
|
+ for (final ServerPlayer player : this.world.getLocalPlayers()) { // Folia - region threading
|
|
+ ret |= player.chunkLoader.midTickUpdate(time);
|
|
}
|
|
+ return ret; // Folia - region threading - report whether tickets were added
|
|
}
|
|
|
|
private static long[] generateBFSOrder(final int radius) {
|
|
@@ -384,13 +386,13 @@ public class RegionizedPlayerChunkLoader {
|
|
this.player = player;
|
|
}
|
|
|
|
- private void flushDelayedTicketOps() {
|
|
+ private boolean flushDelayedTicketOps() { // Folia - region threading - report whether tickets were added
|
|
if (this.delayedTicketOps.isEmpty()) {
|
|
- return;
|
|
+ return false; // Folia - region threading - report whether tickets were added
|
|
}
|
|
this.world.chunkTaskScheduler.chunkHolderManager.pushDelayedTicketUpdates(this.delayedTicketOps);
|
|
this.delayedTicketOps.clear();
|
|
- this.world.chunkTaskScheduler.chunkHolderManager.tryDrainTicketUpdates();
|
|
+ return this.world.chunkTaskScheduler.chunkHolderManager.tryDrainTicketUpdates() == Boolean.TRUE; // Folia - region threading - report whether tickets were added
|
|
}
|
|
|
|
private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation<?, ?> op) {
|
|
@@ -607,7 +609,7 @@ public class RegionizedPlayerChunkLoader {
|
|
return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance;
|
|
}
|
|
|
|
- void midTickUpdate(final long time) {
|
|
+ boolean midTickUpdate(final long time) { // Folia - region threading - report whether tickets were added
|
|
TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async");
|
|
// update rate limits
|
|
this.chunkSendCounter.update(time);
|
|
@@ -799,7 +801,7 @@ public class RegionizedPlayerChunkLoader {
|
|
this.chunkSendCounter.addTime(time, sendSlots);
|
|
}
|
|
|
|
- this.flushDelayedTicketOps();
|
|
+ return this.flushDelayedTicketOps(); // Folia - region threading - report whether tickets were added
|
|
// we assume propagate ticket levels happens after this call
|
|
}
|
|
|
|
diff --git a/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java
|
|
index 61c170555c8854b102c640b0b6a615f9f732edbf..515cc130a411f218ed20628eb918be9d770b9939 100644
|
|
--- a/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java
|
|
+++ b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java
|
|
@@ -187,7 +187,12 @@ public final class EntityLookup implements LevelEntityGetter<Entity> {
|
|
|
|
@Override
|
|
public Iterable<Entity> 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<Entity> {
|
|
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<Entity> {
|
|
}
|
|
|
|
if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) {
|
|
+ synchronized (this.accessibleEntities) { // Folia - region threading
|
|
this.accessibleEntities.remove(entity);
|
|
+ } // Folia - region threading
|
|
EntityLookup.this.worldCallback.onTrackingEnd(entity);
|
|
}
|
|
}
|
|
@@ -385,11 +394,26 @@ public final class EntityLookup implements LevelEntityGetter<Entity> {
|
|
|
|
entity.setLevelCallback(new EntityCallback(entity));
|
|
|
|
+ this.world.getCurrentWorldData().addEntity(entity); // Folia - region threading
|
|
+
|
|
this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ // only appropriate to use when in shutdown, as this performs no logic hooks to properly add to world
|
|
+ public boolean addEntityForShutdownTeleportComplete(final Entity entity) {
|
|
+ final BlockPos pos = entity.blockPosition();
|
|
+ final int sectionX = pos.getX() >> 4;
|
|
+ final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection);
|
|
+ final int sectionZ = pos.getZ() >> 4;
|
|
+ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ);
|
|
+
|
|
+ return slices.addEntity(entity, sectionY);
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
private void removeEntity(final Entity entity) {
|
|
final int sectionX = entity.sectionX;
|
|
final int sectionY = entity.sectionY;
|
|
@@ -407,6 +431,7 @@ public final class EntityLookup implements LevelEntityGetter<Entity> {
|
|
LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")");
|
|
}
|
|
}
|
|
+
|
|
entity.sectionX = entity.sectionY = entity.sectionZ = Integer.MIN_VALUE;
|
|
|
|
this.entityByLock.writeLock();
|
|
@@ -823,6 +848,9 @@ public final class EntityLookup implements LevelEntityGetter<Entity> {
|
|
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..0d1896d09d419c78501bbccca97424dd1545230b 100644
|
|
--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
|
|
+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
|
|
@@ -3,7 +3,6 @@ package io.papermc.paper.chunk.system.scheduling;
|
|
import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
|
|
import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
|
|
import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
|
|
-import co.aikar.timings.Timing;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.gson.JsonArray;
|
|
import com.google.gson.JsonObject;
|
|
@@ -19,10 +18,12 @@ import it.unimi.dsi.fastutil.longs.Long2IntMap;
|
|
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
|
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
|
|
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
|
import it.unimi.dsi.fastutil.longs.LongIterator;
|
|
import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
|
|
import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
|
|
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
|
|
import net.minecraft.nbt.CompoundTag;
|
|
import io.papermc.paper.chunk.system.ChunkSystem;
|
|
import net.minecraft.server.MinecraftServer;
|
|
@@ -34,8 +35,6 @@ import net.minecraft.server.level.TicketType;
|
|
import net.minecraft.util.SortedArraySet;
|
|
import net.minecraft.util.Unit;
|
|
import net.minecraft.world.level.ChunkPos;
|
|
-import net.minecraft.world.level.chunk.ChunkAccess;
|
|
-import net.minecraft.world.level.chunk.ChunkStatus;
|
|
import org.bukkit.plugin.Plugin;
|
|
import org.slf4j.Logger;
|
|
import java.io.IOException;
|
|
@@ -54,6 +53,13 @@ import java.util.concurrent.locks.LockSupport;
|
|
import java.util.concurrent.locks.ReentrantLock;
|
|
import java.util.function.Predicate;
|
|
|
|
+// Folia start - region threading
|
|
+import io.papermc.paper.threadedregions.RegionizedServer;
|
|
+import io.papermc.paper.threadedregions.ThreadedRegionizer;
|
|
+import io.papermc.paper.threadedregions.TickRegionScheduler;
|
|
+import io.papermc.paper.threadedregions.TickRegions;
|
|
+// Folia end - region threading
|
|
+
|
|
public final class ChunkHolderManager {
|
|
|
|
private static final Logger LOGGER = LogUtils.getClassLogger();
|
|
@@ -63,40 +69,198 @@ public final class ChunkHolderManager {
|
|
public static final int ENTITY_TICKING_TICKET_LEVEL = 31;
|
|
public static final int MAX_TICKET_LEVEL = ChunkMap.MAX_CHUNK_DISTANCE; // inclusive
|
|
|
|
- private static final long NO_TIMEOUT_MARKER = -1L;
|
|
+ // Folia start - region threading
|
|
+ private static final long NO_TIMEOUT_MARKER = Long.MIN_VALUE;
|
|
+ private static final long PROBE_MARKER = Long.MIN_VALUE + 1;
|
|
+ // special region threading fields
|
|
+ // this field contains chunk holders that were created in addTicketAtLevel
|
|
+ // because the chunk holders were created without a reliable unload hook (i.e creation for entity/poi loading,
|
|
+ // which always check for unload after their tasks finish) we need to do that ourselves later
|
|
+ private final ReferenceOpenHashSet<NewChunkHolder> specialCaseUnload = new ReferenceOpenHashSet<>();
|
|
+ // Folia end - region threading
|
|
|
|
- final ReentrantLock ticketLock = new ReentrantLock();
|
|
+ public final ReentrantLock ticketLock = new ReentrantLock(); // Folia - region threading
|
|
|
|
private final SWMRLong2ObjectHashTable<NewChunkHolder> chunkHolders = new SWMRLong2ObjectHashTable<>(16384, 0.25f);
|
|
- private final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> 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<Long2IntOpenHashMap> removeTickToChunkExpireTicketCount = new Long2ObjectOpenHashMap<>();
|
|
+ // Folia - region threading
|
|
private final ServerLevel world;
|
|
private final ChunkTaskScheduler taskScheduler;
|
|
- private long currentTick;
|
|
|
|
- private final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>();
|
|
- private final ObjectRBTreeSet<NewChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
|
|
- if (c1 == c2) {
|
|
- return 0;
|
|
+ // Folia start - region threading
|
|
+ public static final class HolderManagerRegionData {
|
|
+ /*
|
|
+ * This region data is a bit of a mess, because it is part global state and part region state.
|
|
+ * Typically for region state we do not need to worry about threading concerns because it is only
|
|
+ * accessed by the current region when ticking. But since this contains state (
|
|
+ * tickets, and removeTickToChunkExpireTicketCount) that can be written to by any thread holding the
|
|
+ * ticket lock, the merge logic is complicated as merging only holds the region lock. So, Folia has modified
|
|
+ * the add and remove ticket functions to acquire the region lock if the current region does not own the target
|
|
+ * position.
|
|
+ */
|
|
+ private final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>();
|
|
+ private final ObjectRBTreeSet<NewChunkHolder> 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<SortedArraySet<Ticket<?>>> 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<Long2IntOpenHashMap> removeTickToChunkExpireTicketCount = new Long2ObjectOpenHashMap<>();
|
|
+
|
|
+ public void merge(final HolderManagerRegionData into, final long tickOffset) {
|
|
+ // Order doesn't really matter for the pending full update...
|
|
+ into.pendingFullLoadUpdate.addAll(this.pendingFullLoadUpdate);
|
|
+
|
|
+ // We need to copy the set to iterate over, because modifying the field used in compareTo while iterating
|
|
+ // will destroy the result from compareTo (However, the set is not destroyed _after_ iteration because a constant
|
|
+ // addition to every entry will not affect compareTo).
|
|
+ for (final NewChunkHolder holder : new ArrayList<>(this.autoSaveQueue)) {
|
|
+ holder.lastAutoSave += tickOffset;
|
|
+ into.autoSaveQueue.add(holder);
|
|
+ }
|
|
+
|
|
+ final long chunkManagerTickOffset = into.currentTick - this.currentTick;
|
|
+ for (final Iterator<Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator();
|
|
+ iterator.hasNext();) {
|
|
+ final Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> entry = iterator.next();
|
|
+ final SortedArraySet<Ticket<?>> oldTickets = entry.getValue();
|
|
+ final SortedArraySet<Ticket<?>> 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<Long2ObjectMap.Entry<Long2IntOpenHashMap>> iterator = this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet().fastIterator();
|
|
+ iterator.hasNext();) {
|
|
+ final Long2ObjectMap.Entry<Long2IntOpenHashMap> entry = iterator.next();
|
|
+ into.removeTickToChunkExpireTicketCount.merge(
|
|
+ (long)(entry.getLongKey() + chunkManagerTickOffset), entry.getValue(),
|
|
+ (final Long2IntOpenHashMap t, final Long2IntOpenHashMap f) -> {
|
|
+ for (final Iterator<Long2IntMap.Entry> itr = f.long2IntEntrySet().fastIterator(); itr.hasNext();) {
|
|
+ final Long2IntMap.Entry e = itr.next();
|
|
+ t.addTo(e.getLongKey(), e.getIntValue());
|
|
+ }
|
|
+ return t;
|
|
+ }
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void split(final int chunkToRegionShift, final Long2ReferenceOpenHashMap<HolderManagerRegionData> regionToData,
|
|
+ final ReferenceOpenHashSet<HolderManagerRegionData> dataSet) {
|
|
+ for (final NewChunkHolder fullLoadUpdate : this.pendingFullLoadUpdate) {
|
|
+ final int regionCoordinateX = fullLoadUpdate.chunkX >> chunkToRegionShift;
|
|
+ final int regionCoordinateZ = fullLoadUpdate.chunkZ >> chunkToRegionShift;
|
|
+
|
|
+ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ));
|
|
+ if (data != null) {
|
|
+ data.pendingFullLoadUpdate.add(fullLoadUpdate);
|
|
+ } // else: fullLoadUpdate is an unloaded chunk holder
|
|
+ }
|
|
+
|
|
+ for (final NewChunkHolder autoSave : this.autoSaveQueue) {
|
|
+ final int regionCoordinateX = autoSave.chunkX >> chunkToRegionShift;
|
|
+ final int regionCoordinateZ = autoSave.chunkZ >> chunkToRegionShift;
|
|
+
|
|
+ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ));
|
|
+ if (data != null) {
|
|
+ data.autoSaveQueue.add(autoSave);
|
|
+ } // else: autoSave is an unloaded chunk holder
|
|
+ }
|
|
+ for (final HolderManagerRegionData data : dataSet) {
|
|
+ data.currentTick = this.currentTick;
|
|
+ }
|
|
+ for (final Iterator<Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator();
|
|
+ iterator.hasNext();) {
|
|
+ final Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> 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<Long2ObjectMap.Entry<Long2IntOpenHashMap>> iterator = this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet().fastIterator();
|
|
+ iterator.hasNext();) {
|
|
+ final Long2ObjectMap.Entry<Long2IntOpenHashMap> entry = iterator.next();
|
|
+ final long tick = entry.getLongKey();
|
|
+ final Long2IntOpenHashMap chunkToCount = entry.getValue();
|
|
+
|
|
+ for (final Iterator<Long2IntMap.Entry> itr = chunkToCount.long2IntEntrySet().fastIterator(); itr.hasNext();) {
|
|
+ final Long2IntMap.Entry e = itr.next();
|
|
+ final long chunkKey = e.getLongKey();
|
|
+ final int regionCoordinateX = CoordinateUtils.getChunkX(chunkKey) >> chunkToRegionShift;
|
|
+ final int regionCoordinateZ = CoordinateUtils.getChunkZ(chunkKey) >> chunkToRegionShift;
|
|
+ final int count = e.getIntValue();
|
|
+
|
|
+ // can never be null, since a chunk holder exists if the ticket set is not empty
|
|
+ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ));
|
|
+
|
|
+ data.removeTickToChunkExpireTicketCount.computeIfAbsent(tick, (final long keyInMap) -> {
|
|
+ return new Long2IntOpenHashMap();
|
|
+ }).put(chunkKey, count);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() {
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region =
|
|
+ TickRegionScheduler.getCurrentRegion();
|
|
+
|
|
+ if (region == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (this.world != null && this.world != region.getData().world) {
|
|
+ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey());
|
|
}
|
|
|
|
- final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
|
|
+ return region.getData().getHolderManagerRegionData();
|
|
+ }
|
|
+
|
|
+ // MUST hold ticket lock
|
|
+ private ChunkHolderManager.HolderManagerRegionData getDataFor(final long key) {
|
|
+ return this.getDataFor(CoordinateUtils.getChunkX(key), CoordinateUtils.getChunkZ(key));
|
|
+ }
|
|
|
|
- if (saveTickCompare != 0) {
|
|
- return saveTickCompare;
|
|
+ // MUST hold ticket lock
|
|
+ private ChunkHolderManager.HolderManagerRegionData getDataFor(final int chunkX, final int chunkZ) {
|
|
+ if (!this.ticketLock.isHeldByCurrentThread()) {
|
|
+ throw new IllegalStateException("Must hold ticket level lock");
|
|
}
|
|
|
|
- final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
|
|
- final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region
|
|
+ = this.world.regioniser.getRegionAtUnsynchronised(chunkX, chunkZ);
|
|
|
|
- if (coord1 == coord2) {
|
|
- throw new IllegalStateException("Duplicate chunkholder in auto save queue");
|
|
+ if (region == null) {
|
|
+ return null;
|
|
}
|
|
|
|
- return Long.compare(coord1, coord2);
|
|
- });
|
|
+ return region.getData().getHolderManagerRegionData();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
|
|
public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
|
|
this.world = world;
|
|
@@ -129,8 +293,13 @@ public final class ChunkHolderManager {
|
|
}
|
|
|
|
public void close(final boolean save, final boolean halt) {
|
|
+ // Folia start - region threading
|
|
+ this.close(save, halt, true, true, true);
|
|
+ }
|
|
+ public void close(final boolean save, final boolean halt, final boolean first, final boolean last, final boolean checkRegions) {
|
|
+ // Folia end - region threading
|
|
TickThread.ensureTickThread("Closing world off-main");
|
|
- if (halt) {
|
|
+ if (first && halt) { // Folia - region threading
|
|
LOGGER.info("Waiting 60s for chunk system to halt for world '" + this.world.getWorld().getName() + "'");
|
|
if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) {
|
|
LOGGER.warn("Failed to halt world generation/loading tasks for world '" + this.world.getWorld().getName() + "'");
|
|
@@ -140,9 +309,10 @@ public final class ChunkHolderManager {
|
|
}
|
|
|
|
if (save) {
|
|
- this.saveAllChunks(true, true, true);
|
|
+ this.saveAllChunksRegionised(true, true, true, first, last, checkRegions); // Folia - region threading
|
|
}
|
|
|
|
+ if (last) { // Folia - region threading
|
|
if (this.world.chunkDataControllerNew.hasTasks() || this.world.entityDataControllerNew.hasTasks() || this.world.poiDataControllerNew.hasTasks()) {
|
|
RegionFileIOThread.flush();
|
|
}
|
|
@@ -163,27 +333,34 @@ public final class ChunkHolderManager {
|
|
} catch (final IOException ex) {
|
|
LOGGER.error("Failed to close poi regionfile cache for world '" + this.world.getWorld().getName() + "'", ex);
|
|
}
|
|
+ } // Folia - region threading
|
|
}
|
|
|
|
void ensureInAutosave(final NewChunkHolder holder) {
|
|
- if (!this.autoSaveQueue.contains(holder)) {
|
|
- holder.lastAutoSave = MinecraftServer.currentTick;
|
|
- this.autoSaveQueue.add(holder);
|
|
+ // Folia start - region threading
|
|
+ final HolderManagerRegionData regionData = this.getCurrentRegionData();
|
|
+ if (!regionData.autoSaveQueue.contains(holder)) {
|
|
+ holder.lastAutoSave = RegionizedServer.getCurrentTick();
|
|
+ // Folia end - region threading
|
|
+ regionData.autoSaveQueue.add(holder);
|
|
}
|
|
}
|
|
|
|
public void autoSave() {
|
|
final List<NewChunkHolder> reschedule = new ArrayList<>();
|
|
- final long currentTick = MinecraftServer.currentTickLong;
|
|
+ final long currentTick = RegionizedServer.getCurrentTick();
|
|
final long maxSaveTime = currentTick - this.world.paperConfig().chunks.autoSaveInterval.value();
|
|
- for (int autoSaved = 0; autoSaved < this.world.paperConfig().chunks.maxAutoSaveChunksPerTick && !this.autoSaveQueue.isEmpty();) {
|
|
- final NewChunkHolder holder = this.autoSaveQueue.first();
|
|
+ // Folia start - region threading
|
|
+ final HolderManagerRegionData regionData = this.getCurrentRegionData();
|
|
+ for (int autoSaved = 0; autoSaved < this.world.paperConfig().chunks.maxAutoSaveChunksPerTick && !regionData.autoSaveQueue.isEmpty();) {
|
|
+ // Folia end - region threading
|
|
+ final NewChunkHolder holder = regionData.autoSaveQueue.first();
|
|
|
|
if (holder.lastAutoSave > maxSaveTime) {
|
|
break;
|
|
}
|
|
|
|
- this.autoSaveQueue.remove(holder);
|
|
+ regionData.autoSaveQueue.remove(holder);
|
|
|
|
holder.lastAutoSave = currentTick;
|
|
if (holder.save(false, false) != null) {
|
|
@@ -197,15 +374,20 @@ public final class ChunkHolderManager {
|
|
|
|
for (final NewChunkHolder holder : reschedule) {
|
|
if (holder.getChunkStatus().isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) {
|
|
- this.autoSaveQueue.add(holder);
|
|
+ regionData.autoSaveQueue.add(holder);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) {
|
|
+ // Folia start - region threading
|
|
+ this.saveAllChunksRegionised(flush, shutdown, logProgress, true, true, true);
|
|
+ }
|
|
+ public void saveAllChunksRegionised(final boolean flush, final boolean shutdown, final boolean logProgress, final boolean first, final boolean last, final boolean checkRegion) {
|
|
+ // Folia end - region threading
|
|
final List<NewChunkHolder> holders = this.getChunkHolders();
|
|
|
|
- if (logProgress) {
|
|
+ if (first && logProgress) { // Folia - region threading
|
|
LOGGER.info("Saving all chunkholders for world '" + this.world.getWorld().getName() + "'");
|
|
}
|
|
|
|
@@ -213,7 +395,7 @@ public final class ChunkHolderManager {
|
|
|
|
int saved = 0;
|
|
|
|
- long start = System.nanoTime();
|
|
+ final long start = System.nanoTime();
|
|
long lastLog = start;
|
|
boolean needsFlush = false;
|
|
final int flushInterval = 50;
|
|
@@ -224,6 +406,12 @@ public final class ChunkHolderManager {
|
|
|
|
for (int i = 0, len = holders.size(); i < len; ++i) {
|
|
final NewChunkHolder holder = holders.get(i);
|
|
+ // Folia start - region threading
|
|
+ if (!checkRegion && !TickThread.isTickThreadFor(this.world, holder.chunkX, holder.chunkZ)) {
|
|
+ // skip holders that would fail the thread check
|
|
+ continue;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
try {
|
|
final NewChunkHolder.SaveStat saveStat = holder.save(shutdown, false);
|
|
if (saveStat != null) {
|
|
@@ -256,7 +444,7 @@ public final class ChunkHolderManager {
|
|
}
|
|
}
|
|
}
|
|
- if (flush) {
|
|
+ if (last && flush) { // Folia - region threading
|
|
RegionFileIOThread.flush();
|
|
}
|
|
if (logProgress) {
|
|
@@ -290,18 +478,16 @@ public final class ChunkHolderManager {
|
|
}
|
|
|
|
public boolean hasTickets() {
|
|
- this.ticketLock.lock();
|
|
- try {
|
|
- return !this.tickets.isEmpty();
|
|
- } finally {
|
|
- this.ticketLock.unlock();
|
|
- }
|
|
+ return !this.getTicketsCopy().isEmpty(); // Folia - region threading
|
|
}
|
|
|
|
public String getTicketDebugString(final long coordinate) {
|
|
this.ticketLock.lock();
|
|
try {
|
|
- final SortedArraySet<Ticket<?>> tickets = this.tickets.get(coordinate);
|
|
+ // Folia start - region threading
|
|
+ final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = this.getDataFor(coordinate);
|
|
+ final SortedArraySet<Ticket<?>> tickets = holderManagerRegionData == null ? null : holderManagerRegionData.tickets.get(coordinate);
|
|
+ // Folia end - region threading
|
|
|
|
return tickets != null ? tickets.first().toString() : "no_ticket";
|
|
} finally {
|
|
@@ -312,7 +498,17 @@ public final class ChunkHolderManager {
|
|
public Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> getTicketsCopy() {
|
|
this.ticketLock.lock();
|
|
try {
|
|
- return this.tickets.clone();
|
|
+ // Folia start - region threading
|
|
+ Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> ret = new Long2ObjectOpenHashMap<>();
|
|
+ this.world.regioniser.computeForAllRegions((region) -> {
|
|
+ for (final LongIterator iterator = region.getData().getHolderManagerRegionData().tickets.keySet().longIterator(); iterator.hasNext();) {
|
|
+ final long chunk = iterator.nextLong();
|
|
+
|
|
+ ret.put(chunk, region.getData().getHolderManagerRegionData().tickets.get(chunk));
|
|
+ }
|
|
+ });
|
|
+ return ret;
|
|
+ // Folia end - region threading
|
|
} finally {
|
|
this.ticketLock.unlock();
|
|
}
|
|
@@ -322,7 +518,11 @@ public final class ChunkHolderManager {
|
|
ImmutableList.Builder<Plugin> ret;
|
|
this.ticketLock.lock();
|
|
try {
|
|
- SortedArraySet<Ticket<?>> 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<Ticket<?>> tickets = holderManagerRegionData == null ? null : holderManagerRegionData.tickets.get(coordinate);
|
|
+ // Folia end - region threading
|
|
|
|
if (tickets == null) {
|
|
return Collections.emptyList();
|
|
@@ -375,12 +575,37 @@ public final class ChunkHolderManager {
|
|
return false;
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> currRegion = TickRegionScheduler.getCurrentRegion();
|
|
+ final boolean lock = currRegion == null || this.world.regioniser.getRegionAtUnsynchronised(
|
|
+ CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)
|
|
+ ) != currRegion;
|
|
+ // Folia end - region threading
|
|
+
|
|
this.ticketLock.lock();
|
|
try {
|
|
- final long removeTick = removeDelay == 0 ? NO_TIMEOUT_MARKER : this.currentTick + removeDelay;
|
|
+ // Folia start - region threading
|
|
+ NewChunkHolder holder = this.chunkHolders.get(chunk);
|
|
+ if (holder == null) {
|
|
+ // we need to guarantee that a chunk holder exists for each ticket
|
|
+ // this must be executed before retrieving the holder manager data for a target chunk, to ensure the
|
|
+ // region will exist
|
|
+ this.chunkHolders.put(chunk, holder = this.createChunkHolder(chunk));
|
|
+ this.specialCaseUnload.add(holder);
|
|
+ }
|
|
+
|
|
+ if (lock) {
|
|
+ // we just need to prevent merging, so we only need the read lock
|
|
+ // additionally, this will prevent deadlock in the remove all tickets function by using the read lock
|
|
+ this.world.regioniser.acquireReadLock();
|
|
+ }
|
|
+ try {
|
|
+ final ChunkHolderManager.HolderManagerRegionData targetData = lock ? this.getDataFor(chunk) : currRegion.getData().getHolderManagerRegionData();
|
|
+ // Folia end - region threading
|
|
+ final long removeTick = removeDelay == 0 ? NO_TIMEOUT_MARKER : targetData.currentTick + removeDelay; // Folia - region threading
|
|
final Ticket<T> ticket = new Ticket<>(type, level, identifier, removeTick);
|
|
|
|
- final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> {
|
|
+ final SortedArraySet<Ticket<?>> ticketsAtChunk = targetData.tickets.computeIfAbsent(chunk, (final long keyInMap) -> { // Folia - region threading
|
|
return SortedArraySet.create(4);
|
|
});
|
|
|
|
@@ -392,25 +617,25 @@ public final class ChunkHolderManager {
|
|
final long oldRemovalTick = current.removalTick;
|
|
if (removeTick != oldRemovalTick) {
|
|
if (oldRemovalTick != NO_TIMEOUT_MARKER) {
|
|
- final Long2IntOpenHashMap removeCounts = this.removeTickToChunkExpireTicketCount.get(oldRemovalTick);
|
|
+ final Long2IntOpenHashMap removeCounts = targetData.removeTickToChunkExpireTicketCount.get(oldRemovalTick); // Folia - region threading
|
|
final int prevCount = removeCounts.addTo(chunk, -1);
|
|
|
|
if (prevCount == 1) {
|
|
removeCounts.remove(chunk);
|
|
if (removeCounts.isEmpty()) {
|
|
- this.removeTickToChunkExpireTicketCount.remove(oldRemovalTick);
|
|
+ targetData.removeTickToChunkExpireTicketCount.remove(oldRemovalTick); // Folia - region threading
|
|
}
|
|
}
|
|
}
|
|
if (removeTick != NO_TIMEOUT_MARKER) {
|
|
- this.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> {
|
|
+ targetData.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> { // Folia - region threading
|
|
return new Long2IntOpenHashMap();
|
|
}).addTo(chunk, 1);
|
|
}
|
|
}
|
|
} else {
|
|
if (removeTick != NO_TIMEOUT_MARKER) {
|
|
- this.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> {
|
|
+ targetData.removeTickToChunkExpireTicketCount.computeIfAbsent(removeTick, (final long keyInMap) -> { // Folia - region threading
|
|
return new Long2IntOpenHashMap();
|
|
}).addTo(chunk, 1);
|
|
}
|
|
@@ -421,6 +646,11 @@ public final class ChunkHolderManager {
|
|
}
|
|
|
|
return current == ticket;
|
|
+ } finally { // Folia start - region threading
|
|
+ if (lock) {
|
|
+ this.world.regioniser.releaseReadLock();
|
|
+ }
|
|
+ } // Folia end - region threading
|
|
} finally {
|
|
this.ticketLock.unlock();
|
|
}
|
|
@@ -439,35 +669,70 @@ public final class ChunkHolderManager {
|
|
return false;
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> currRegion = TickRegionScheduler.getCurrentRegion();
|
|
+ final boolean lock = currRegion == null || this.world.regioniser.getRegionAtUnsynchronised(
|
|
+ CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)
|
|
+ ) != currRegion;
|
|
+ // Folia end - region threading
|
|
+
|
|
this.ticketLock.lock();
|
|
try {
|
|
- final SortedArraySet<Ticket<?>> ticketsAtChunk = this.tickets.get(chunk);
|
|
+ // Folia start - region threading
|
|
+ if (lock) {
|
|
+ // we just need to prevent merging, so we only need the read lock
|
|
+ // additionally, this will prevent deadlock in the remove all tickets function by using the read lock
|
|
+ this.world.regioniser.acquireReadLock();
|
|
+ }
|
|
+ try {
|
|
+ final ChunkHolderManager.HolderManagerRegionData targetData = lock ? this.getDataFor(chunk) : currRegion.getData().getHolderManagerRegionData();
|
|
+ // Folia end - region threading
|
|
+
|
|
+ final SortedArraySet<Ticket<?>> ticketsAtChunk = targetData == null ? null : targetData.tickets.get(chunk);
|
|
+ // Folia end - region threading
|
|
if (ticketsAtChunk == null) {
|
|
return false;
|
|
}
|
|
|
|
final int oldLevel = getTicketLevelAt(ticketsAtChunk);
|
|
- final Ticket<T> ticket = (Ticket<T>)ticketsAtChunk.removeAndGet(new Ticket<>(type, level, identifier, -2L));
|
|
+ final Ticket<T> ticket = (Ticket<T>)ticketsAtChunk.removeAndGet(new Ticket<>(type, level, identifier, PROBE_MARKER)); // Folia - region threading
|
|
|
|
if (ticket == null) {
|
|
return false;
|
|
}
|
|
|
|
+ int newLevel = getTicketLevelAt(ticketsAtChunk); // Folia - region threading - moved up from below
|
|
+ // Folia start - region threading
|
|
+ // we should not change the ticket levels while the target region may be ticking
|
|
+ if (newLevel > level) {
|
|
+ final long unknownRemoveTick = targetData.currentTick + Math.max(0, TicketType.UNKNOWN.timeout);
|
|
+ final Ticket<ChunkPos> unknownTicket = new Ticket<>(TicketType.UNKNOWN, level, new ChunkPos(chunk), unknownRemoveTick);
|
|
+ if (ticketsAtChunk.add(unknownTicket)) {
|
|
+ targetData.removeTickToChunkExpireTicketCount.computeIfAbsent(unknownRemoveTick, (final long keyInMap) -> {
|
|
+ return new Long2IntOpenHashMap();
|
|
+ }).addTo(chunk, 1);
|
|
+ } else {
|
|
+ throw new IllegalStateException("Should have been able to add " + unknownTicket + " to " + ticketsAtChunk);
|
|
+ }
|
|
+ newLevel = level;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
if (ticketsAtChunk.isEmpty()) {
|
|
- this.tickets.remove(chunk);
|
|
+ targetData.tickets.remove(chunk); // Folia - region threading
|
|
}
|
|
|
|
- final int newLevel = getTicketLevelAt(ticketsAtChunk);
|
|
+ // Folia - region threading - move up
|
|
|
|
final long removeTick = ticket.removalTick;
|
|
if (removeTick != NO_TIMEOUT_MARKER) {
|
|
- final Long2IntOpenHashMap removeCounts = this.removeTickToChunkExpireTicketCount.get(removeTick);
|
|
+ final Long2IntOpenHashMap removeCounts = targetData.removeTickToChunkExpireTicketCount.get(removeTick); // Folia - region threading
|
|
final int currCount = removeCounts.addTo(chunk, -1);
|
|
|
|
if (currCount == 1) {
|
|
removeCounts.remove(chunk);
|
|
if (removeCounts.isEmpty()) {
|
|
- this.removeTickToChunkExpireTicketCount.remove(removeTick);
|
|
+ targetData.removeTickToChunkExpireTicketCount.remove(removeTick); // Folia - region threading
|
|
}
|
|
}
|
|
}
|
|
@@ -477,6 +742,11 @@ public final class ChunkHolderManager {
|
|
}
|
|
|
|
return true;
|
|
+ } finally { // Folia start - region threading
|
|
+ if (lock) {
|
|
+ this.world.regioniser.releaseReadLock();
|
|
+ }
|
|
+ } // Folia end - region threading
|
|
} finally {
|
|
this.ticketLock.unlock();
|
|
}
|
|
@@ -516,24 +786,33 @@ public final class ChunkHolderManager {
|
|
|
|
this.ticketLock.lock();
|
|
try {
|
|
- for (final LongIterator iterator = new LongArrayList(this.tickets.keySet()).longIterator(); iterator.hasNext();) {
|
|
- final long chunk = iterator.nextLong();
|
|
+ // Folia start - region threading
|
|
+ this.world.regioniser.computeForAllRegions((region) -> {
|
|
+ for (final LongIterator iterator = new LongArrayList(region.getData().getHolderManagerRegionData().tickets.keySet()).longIterator(); iterator.hasNext();) {
|
|
+ final long chunk = iterator.nextLong();
|
|
|
|
- this.removeTicketAtLevel(ticketType, chunk, ticketLevel, ticketIdentifier);
|
|
- }
|
|
+ this.removeTicketAtLevel(ticketType, chunk, ticketLevel, ticketIdentifier);
|
|
+ }
|
|
+ });
|
|
+ // Folia end - region threading
|
|
} finally {
|
|
this.ticketLock.unlock();
|
|
}
|
|
}
|
|
|
|
public void tick() {
|
|
- TickThread.ensureTickThread("Cannot tick ticket manager off-main");
|
|
+ // Folia start - region threading
|
|
+ final ChunkHolderManager.HolderManagerRegionData data = this.getCurrentRegionData();
|
|
+ if (data == null) {
|
|
+ throw new IllegalStateException("Not running tick() while on a region");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
|
|
this.ticketLock.lock();
|
|
try {
|
|
- final long tick = ++this.currentTick;
|
|
+ final long tick = ++data.currentTick; // Folia - region threading
|
|
|
|
- final Long2IntOpenHashMap toRemove = this.removeTickToChunkExpireTicketCount.remove(tick);
|
|
+ final Long2IntOpenHashMap toRemove = data.removeTickToChunkExpireTicketCount.remove(tick); // Folia - region threading
|
|
|
|
if (toRemove == null) {
|
|
return;
|
|
@@ -546,10 +825,10 @@ public final class ChunkHolderManager {
|
|
for (final LongIterator iterator = toRemove.keySet().longIterator(); iterator.hasNext();) {
|
|
final long chunk = iterator.nextLong();
|
|
|
|
- final SortedArraySet<Ticket<?>> tickets = this.tickets.get(chunk);
|
|
+ final SortedArraySet<Ticket<?>> tickets = data.tickets.get(chunk); // Folia - region threading
|
|
tickets.removeIf(expireNow);
|
|
if (tickets.isEmpty()) {
|
|
- this.tickets.remove(chunk);
|
|
+ data.tickets.remove(chunk); // Folia - region threading
|
|
this.ticketLevelPropagator.removeSource(chunk);
|
|
} else {
|
|
this.ticketLevelPropagator.setSource(chunk, convertBetweenTicketLevels(tickets.first().getTicketLevel()));
|
|
@@ -798,30 +1077,62 @@ public final class ChunkHolderManager {
|
|
if (changedFullStatus.isEmpty()) {
|
|
return;
|
|
}
|
|
- if (!TickThread.isTickThread()) {
|
|
- this.taskScheduler.scheduleChunkTask(() -> {
|
|
- final ArrayDeque<NewChunkHolder> 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<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
|
|
- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
|
|
- pendingFullLoadUpdate.add(changedFullStatus.get(i));
|
|
+ final Long2ObjectOpenHashMap<List<NewChunkHolder>> sectionToUpdates = new Long2ObjectOpenHashMap<>();
|
|
+ final List<NewChunkHolder> thisRegionHolders = new ArrayList<>();
|
|
+
|
|
+ final int regionShift = this.world.regioniser.sectionChunkShift;
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> 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<Long2ObjectMap.Entry<List<NewChunkHolder>>> iterator = sectionToUpdates.long2ObjectEntrySet().fastIterator();
|
|
+ iterator.hasNext();) {
|
|
+ final Long2ObjectMap.Entry<List<NewChunkHolder>> entry = iterator.next();
|
|
+ final long sectionKey = entry.getLongKey();
|
|
+
|
|
+ final int chunkX = CoordinateUtils.getChunkX(sectionKey) << regionShift;
|
|
+ final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift;
|
|
+
|
|
+ final List<NewChunkHolder> 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<NewChunkHolder> unloadQueue = new ReferenceLinkedOpenHashSet<>();
|
|
|
|
+ /*
|
|
+ * Note: Only called on chunk holders that the current ticking region owns
|
|
+ */
|
|
private void removeChunkHolder(final NewChunkHolder holder) {
|
|
holder.killed = true;
|
|
holder.vanillaChunkHolder.onChunkRemove();
|
|
- this.autoSaveQueue.remove(holder);
|
|
+ // Folia - region threading
|
|
ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder);
|
|
+ this.getCurrentRegionData().autoSaveQueue.remove(holder); // Folia - region threading
|
|
this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ));
|
|
}
|
|
|
|
@@ -839,6 +1150,8 @@ public final class ChunkHolderManager {
|
|
throw new IllegalStateException("Cannot hold scheduling lock while calling processUnloads");
|
|
}
|
|
|
|
+ final ChunkHolderManager.HolderManagerRegionData currentData = this.getCurrentRegionData(); // Folia - region threading
|
|
+
|
|
final List<NewChunkHolder.UnloadState> unloadQueue;
|
|
final List<ChunkProgressionTask> scheduleList = new ArrayList<>();
|
|
this.ticketLock.lock();
|
|
@@ -851,11 +1164,22 @@ public final class ChunkHolderManager {
|
|
// in order to ensure all chunks in the unload queue do not have a pending ticket level update,
|
|
// process them now
|
|
this.processTicketUpdates(false, false, scheduleList);
|
|
- unloadQueue = new ArrayList<>((int)(this.unloadQueue.size() * 0.05) + 1);
|
|
|
|
- final int unloadCount = Math.max(50, (int)(this.unloadQueue.size() * 0.05));
|
|
- for (int i = 0; i < unloadCount && !this.unloadQueue.isEmpty(); ++i) {
|
|
- final NewChunkHolder chunkHolder = this.unloadQueue.removeFirst();
|
|
+ // Folia start - region threading
|
|
+ final ArrayDeque<NewChunkHolder> 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;
|
|
@@ -1168,6 +1492,17 @@ public final class ChunkHolderManager {
|
|
}
|
|
|
|
this.ticketLevelUpdates.clear();
|
|
+
|
|
+ // Folia start - region threading
|
|
+ // it is possible that a special case new chunk holder had its ticket removed before it was propagated,
|
|
+ // which means checkUnload was never invoked. By checking unload here, we ensure that either the
|
|
+ // ticket level was propagated (in which case, a later depropagation would check again) or that
|
|
+ // we called checkUnload for it.
|
|
+ for (final NewChunkHolder special : this.specialCaseUnload) {
|
|
+ special.checkUnload();
|
|
+ }
|
|
+ this.specialCaseUnload.clear();
|
|
+ // Folia end - region threading
|
|
}
|
|
}
|
|
} finally {
|
|
@@ -1193,7 +1528,12 @@ public final class ChunkHolderManager {
|
|
|
|
// only call on tick thread
|
|
protected final boolean processPendingFullUpdate() {
|
|
- final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
|
|
+ final HolderManagerRegionData data = this.getCurrentRegionData();
|
|
+ if (data == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = data.pendingFullLoadUpdate;
|
|
|
|
boolean ret = false;
|
|
|
|
@@ -1204,9 +1544,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 +1594,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 +1613,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<Long2IntOpenHashMap> 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<Long2IntOpenHashMap> 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<SortedArraySet<Ticket<?>>> coordinateTickets : this.tickets.long2ObjectEntrySet()) {
|
|
- final long coordinate = coordinateTickets.getLongKey();
|
|
- final SortedArraySet<Ticket<?>> tickets = coordinateTickets.getValue();
|
|
+ for (final Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> coordinateTickets : regionData.getHolderManagerRegionData().tickets.long2ObjectEntrySet()) {
|
|
+ final long coordinate = coordinateTickets.getLongKey();
|
|
+ final SortedArraySet<Ticket<?>> tickets = coordinateTickets.getValue();
|
|
|
|
- final JsonObject coordinateJson = new JsonObject();
|
|
- allTicketsJson.add(coordinateJson);
|
|
+ final JsonObject coordinateJson = new JsonObject();
|
|
+ allTicketsJson.add(coordinateJson);
|
|
|
|
- coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate)));
|
|
- coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate)));
|
|
+ coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate)));
|
|
+ coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate)));
|
|
|
|
- final JsonArray ticketsSerialized = new JsonArray();
|
|
- coordinateJson.add("tickets", ticketsSerialized);
|
|
+ final JsonArray ticketsSerialized = new JsonArray();
|
|
+ coordinateJson.add("tickets", ticketsSerialized);
|
|
|
|
- for (final Ticket<?> ticket : tickets) {
|
|
- final JsonObject ticketSerialized = new JsonObject();
|
|
- ticketsSerialized.add(ticketSerialized);
|
|
+ for (final Ticket<?> ticket : tickets) {
|
|
+ final JsonObject ticketSerialized = new JsonObject();
|
|
+ ticketsSerialized.add(ticketSerialized);
|
|
|
|
- ticketSerialized.addProperty("type", ticket.getType().toString());
|
|
- ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel()));
|
|
- ticketSerialized.addProperty("identifier", Objects.toString(ticket.key));
|
|
- ticketSerialized.addProperty("remove_tick", Long.valueOf(ticket.removalTick));
|
|
+ ticketSerialized.addProperty("type", ticket.getType().toString());
|
|
+ ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel()));
|
|
+ ticketSerialized.addProperty("identifier", Objects.toString(ticket.key));
|
|
+ ticketSerialized.addProperty("remove_tick", Long.valueOf(ticket.removalTick));
|
|
+ }
|
|
}
|
|
- }
|
|
+ });
|
|
+ // Folia end - region threading
|
|
|
|
return ret;
|
|
}
|
|
diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java
|
|
index 84cc9397237fa0c17aa1012dfb5683c90eb6d3b8..25db30284e3bab9ebad1ca7320db66116057e599 100644
|
|
--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java
|
|
+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java
|
|
@@ -113,7 +113,7 @@ public final class ChunkTaskScheduler {
|
|
public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor;
|
|
public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor;
|
|
|
|
- private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue();
|
|
+ // Folia - regionised ticking
|
|
|
|
final ReentrantLock schedulingLock = new ReentrantLock();
|
|
public final ChunkHolderManager chunkHolderManager;
|
|
@@ -240,14 +240,13 @@ public final class ChunkTaskScheduler {
|
|
};
|
|
|
|
// this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions
|
|
- this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING);
|
|
+ this.scheduleChunkTaskEventually(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING); // Folia - region threading
|
|
// so, make the main thread pick it up
|
|
MinecraftServer.chunkSystemCrash = new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException);
|
|
}
|
|
|
|
public boolean executeMainThreadTask() {
|
|
- TickThread.ensureTickThread("Cannot execute main thread task off-main");
|
|
- return this.mainThreadExecutor.executeTask();
|
|
+ throw new UnsupportedOperationException("Use regionised ticking hooks"); // Folia - regionised ticking
|
|
}
|
|
|
|
public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
|
|
@@ -267,7 +266,7 @@ public final class ChunkTaskScheduler {
|
|
public void scheduleTickingState(final int chunkX, final int chunkZ, final ChunkHolder.FullChunkStatus toStatus,
|
|
final boolean addTicket, final PrioritisedExecutor.Priority priority,
|
|
final Consumer<LevelChunk> 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<ChunkProgressionTask> 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<ChunkAccess> 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<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
|
|
+ final Consumer<ChunkAccess> loadCallback = onComplete == null && !addTicket ? null : (final ChunkAccess chunk) -> {
|
|
try {
|
|
if (onComplete != null) {
|
|
onComplete.accept(chunk);
|
|
@@ -449,7 +489,9 @@ public final class ChunkTaskScheduler {
|
|
if (!chunkHolder.upgradeGenTarget(toStatus)) {
|
|
this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks);
|
|
}
|
|
- chunkHolder.addStatusConsumer(toStatus, loadCallback);
|
|
+ if (loadCallback != null) {
|
|
+ chunkHolder.addStatusConsumer(toStatus, loadCallback);
|
|
+ }
|
|
}
|
|
}
|
|
} finally {
|
|
@@ -463,7 +505,7 @@ public final class ChunkTaskScheduler {
|
|
tasks.get(i).schedule();
|
|
}
|
|
|
|
- if (!scheduled) {
|
|
+ if (loadCallback != null && !scheduled) {
|
|
// couldn't schedule
|
|
try {
|
|
loadCallback.accept(chunk);
|
|
@@ -652,7 +694,7 @@ public final class ChunkTaskScheduler {
|
|
*/
|
|
@Deprecated
|
|
public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) {
|
|
- return this.scheduleChunkTask(run, PrioritisedExecutor.Priority.NORMAL);
|
|
+ throw new UnsupportedOperationException(); // Folia - regionised ticking
|
|
}
|
|
|
|
/**
|
|
@@ -660,7 +702,7 @@ public final class ChunkTaskScheduler {
|
|
*/
|
|
@Deprecated
|
|
public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final PrioritisedExecutor.Priority priority) {
|
|
- return this.mainThreadExecutor.queueRunnable(run, priority);
|
|
+ throw new UnsupportedOperationException(); // Folia - regionised ticking
|
|
}
|
|
|
|
public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
|
|
@@ -669,28 +711,33 @@ public final class ChunkTaskScheduler {
|
|
|
|
public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run,
|
|
final PrioritisedExecutor.Priority priority) {
|
|
- return this.mainThreadExecutor.createTask(run, priority);
|
|
+ return MinecraftServer.getServer().regionizedServer.taskQueue.createChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking
|
|
}
|
|
|
|
public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
|
|
- return this.mainThreadExecutor.queueRunnable(run);
|
|
+ return this.scheduleChunkTask(chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); // TODO rebase into chunk system patch
|
|
}
|
|
|
|
public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run,
|
|
final PrioritisedExecutor.Priority priority) {
|
|
- return this.mainThreadExecutor.queueRunnable(run, priority);
|
|
+ return MinecraftServer.getServer().regionizedServer.taskQueue.queueChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking
|
|
}
|
|
|
|
- public void executeTasksUntil(final BooleanSupplier exit) {
|
|
- if (Bukkit.isPrimaryThread()) {
|
|
- this.mainThreadExecutor.executeConditionally(exit);
|
|
- } else {
|
|
- long counter = 1L;
|
|
- while (!exit.getAsBoolean()) {
|
|
- counter = ConcurrentUtil.linearLongBackoff(counter, 100_000L, 5_000_000L); // 100us, 5ms
|
|
- }
|
|
- }
|
|
+ // Folia start - region threading
|
|
+ // this function is guaranteed to never touch the ticket lock or schedule lock
|
|
+ // yes, this IS a hack so that we can avoid deadlock due to region threading introducing the
|
|
+ // ticket lock in the schedule logic
|
|
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run,
|
|
+ final PrioritisedExecutor.Priority priority) {
|
|
+ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(chunkX, chunkZ, run, priority);
|
|
+ this.world.taskQueueRegionData.pushGlobalChunkTask(() -> {
|
|
+ MinecraftServer.getServer().regionizedServer.taskQueue.queueChunkTask(ChunkTaskScheduler.this.world, chunkX, chunkZ, run, priority);
|
|
+ });
|
|
+ return ret;
|
|
}
|
|
+ // Folia end - region threading
|
|
+
|
|
+ // Folia - regionised ticking
|
|
|
|
public boolean halt(final boolean sync, final long maxWaitNS) {
|
|
this.lightExecutor.halt();
|
|
@@ -699,6 +746,7 @@ public final class ChunkTaskScheduler {
|
|
this.loadExecutor.halt();
|
|
final long time = System.nanoTime();
|
|
if (sync) {
|
|
+ // start at 10 * 0.5ms -> 5ms
|
|
for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) {
|
|
if (
|
|
!this.lightExecutor.isActive() &&
|
|
diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java
|
|
index 8013dd333e27aa5fd0beb431fa32491eec9f5246..3b70ccd8e0b1ada943f57faf99c23b2935249cf6 100644
|
|
--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java
|
|
+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java
|
|
@@ -708,7 +708,7 @@ public final class NewChunkHolder {
|
|
boolean killed;
|
|
|
|
// must hold scheduling lock
|
|
- private void checkUnload() {
|
|
+ void checkUnload() { // Folia - region threading
|
|
if (this.killed) {
|
|
return;
|
|
}
|
|
@@ -1412,7 +1412,7 @@ public final class NewChunkHolder {
|
|
}
|
|
|
|
// must be scheduled to main, we do not trust the callback to not do anything stupid
|
|
- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
|
|
+ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading
|
|
for (final Consumer<ChunkAccess> 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<LevelChunk> consumer : consumers) {
|
|
try {
|
|
consumer.accept(chunk);
|
|
@@ -1715,7 +1715,7 @@ public final class NewChunkHolder {
|
|
return this.entityChunk;
|
|
}
|
|
|
|
- public long lastAutoSave;
|
|
+ public long lastAutoSave; // Folia - region threaded - change to relative delay
|
|
|
|
public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {}
|
|
|
|
@@ -1865,7 +1865,7 @@ public final class NewChunkHolder {
|
|
} catch (final ThreadDeath death) {
|
|
throw death;
|
|
} catch (final Throwable thr) {
|
|
- LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'");
|
|
+ LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); // TODO rebase
|
|
if (unloading && !completing) {
|
|
this.completeAsyncChunkDataSave(null);
|
|
}
|
|
@@ -1913,7 +1913,7 @@ public final class NewChunkHolder {
|
|
} catch (final ThreadDeath death) {
|
|
throw death;
|
|
} catch (final Throwable thr) {
|
|
- LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'");
|
|
+ LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); // TODO rebase
|
|
}
|
|
|
|
return true;
|
|
@@ -1939,7 +1939,7 @@ public final class NewChunkHolder {
|
|
} catch (final ThreadDeath death) {
|
|
throw death;
|
|
} catch (final Throwable thr) {
|
|
- LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'");
|
|
+ LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); // TODO rebase
|
|
}
|
|
|
|
return true;
|
|
diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
|
|
index 7ba60b4b4f29a42c58d98aafc5ea0fa3214f554c..dc1820e1231c15a7244883731a86f04fa8d20aa1 100644
|
|
--- a/src/main/java/io/papermc/paper/command/PaperCommand.java
|
|
+++ b/src/main/java/io/papermc/paper/command/PaperCommand.java
|
|
@@ -43,7 +43,7 @@ public final class PaperCommand extends Command {
|
|
commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand());
|
|
commands.put(Set.of("syncloadinfo"), new SyncLoadInfoCommand());
|
|
commands.put(Set.of("dumpitem"), new DumpItemCommand());
|
|
- commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand());
|
|
+ commands.put(Set.of("mobcaps"), new MobcapsCommand()); // Folia - region threading - revert per player mob caps
|
|
commands.put(Set.of("dumplisteners"), new DumpListenersCommand());
|
|
|
|
return commands.entrySet().stream()
|
|
diff --git a/src/main/java/io/papermc/paper/command/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java
|
|
index bbb8b1933ef33a3b91f69545f69dd3cfb84b27f5..b23b76a598731da8feef53c370b341233afdbea7 100644
|
|
--- a/src/main/java/io/papermc/paper/command/PaperCommands.java
|
|
+++ b/src/main/java/io/papermc/paper/command/PaperCommands.java
|
|
@@ -17,7 +17,7 @@ public final class PaperCommands {
|
|
private static final Map<String, Command> COMMANDS = new HashMap<>();
|
|
static {
|
|
COMMANDS.put("paper", new PaperCommand("paper"));
|
|
- COMMANDS.put("mspt", new MSPTCommand("mspt"));
|
|
+ COMMANDS.put("tps", new io.papermc.paper.threadedregions.commands.CommandServerHealth()); // Folia - region threading
|
|
}
|
|
|
|
public static void registerCommands(final MinecraftServer server) {
|
|
diff --git a/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java
|
|
index cd2e4d792e972b8bf1e07b8961594a670ae949cf..3ab8dbf2768a4ef8fb53af6f5431f7f6afe6d168 100644
|
|
--- a/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java
|
|
+++ b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java
|
|
@@ -18,7 +18,9 @@ import static net.kyori.adventure.text.format.NamedTextColor.YELLOW;
|
|
public final class HeapDumpCommand implements PaperSubcommand {
|
|
@Override
|
|
public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
|
|
this.dumpHeap(sender);
|
|
+ }); // Folia - region threading
|
|
return true;
|
|
}
|
|
|
|
diff --git a/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java b/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java
|
|
index 99c41a39cdad0271d089c6e03bebfdafba1aaa57..41aaa709dc2e474f23e759ebc51f33021c4f5485 100644
|
|
--- a/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java
|
|
+++ b/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java
|
|
@@ -46,7 +46,7 @@ public final class MobcapsCommand implements PaperSubcommand {
|
|
public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
|
|
switch (subCommand) {
|
|
case "mobcaps" -> this.printMobcaps(sender, args);
|
|
- case "playermobcaps" -> this.printPlayerMobcaps(sender, args);
|
|
+ //case "playermobcaps" -> this.printPlayerMobcaps(sender, args); // Folia - region threading - revert per player mob caps
|
|
}
|
|
return true;
|
|
}
|
|
@@ -55,7 +55,7 @@ public final class MobcapsCommand implements PaperSubcommand {
|
|
public List<String> 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<MobCategory> countGetter, final ToIntFunction<MobCategory> limitGetter) {
|
|
return MOB_CATEGORY_COLORS.entrySet().stream()
|
|
diff --git a/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java
|
|
index bd68139ae635f2ad7ec8e7a21e0056a139c4c62e..48a43341b17247355a531164019d5cc9c5555f26 100644
|
|
--- a/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java
|
|
+++ b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java
|
|
@@ -16,7 +16,9 @@ import static net.kyori.adventure.text.format.NamedTextColor.RED;
|
|
public final class ReloadCommand implements PaperSubcommand {
|
|
@Override
|
|
public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
|
|
this.doReload(sender);
|
|
+ }); // Folia - region threading
|
|
return true;
|
|
}
|
|
|
|
diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
|
|
index 9f5f0d8ddc8f480b48079c70e38c9c08eff403f6..3b83f25a24d6f9cdbf131d5a4432fb4ad018be4e 100644
|
|
--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
|
|
+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
|
|
@@ -288,6 +288,18 @@ public class GlobalConfiguration extends ConfigurationPart {
|
|
public boolean strictAdvancementDimensionCheck = false;
|
|
}
|
|
|
|
+ // Folia start - threaded regions
|
|
+ public ThreadedRegions threadedRegions;
|
|
+ public class ThreadedRegions extends Post {
|
|
+
|
|
+ public int threads = -1;
|
|
+
|
|
+ @Override
|
|
+ public void postProcess() {
|
|
+ io.papermc.paper.threadedregions.TickRegions.init(this);
|
|
+ }
|
|
+ }
|
|
+ // Folia end - threaded regions
|
|
public ChunkLoadingBasic chunkLoadingBasic;
|
|
|
|
public class ChunkLoadingBasic extends ConfigurationPart {
|
|
diff --git a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java
|
|
index 51cf0014c4229fc8671804d885b6381996810130..ee7b26d80ad3b4f76196492b9ffc11b33f15b9ad 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<NbtPathArgument.NbtPath> 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<MobCategory> 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/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
|
|
index c0e896343c22badd97c774c4ed1daa4e274f5d44..1bca4a629fe986e5afa47d8884dc44b2be800f32 100644
|
|
--- a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
|
|
+++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
|
|
@@ -243,10 +243,19 @@ class PaperPluginInstanceManager {
|
|
+ pluginName + " (Is it up to date?)", ex, plugin); // Paper
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
try {
|
|
- this.server.getScheduler().cancelTasks(plugin);
|
|
+ this.server.getGlobalRegionScheduler().cancelTasks(plugin);
|
|
} catch (Throwable ex) {
|
|
- this.handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for "
|
|
+ this.handlePluginException("Error occurred (in the plugin loader) while cancelling global tasks for "
|
|
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
+ try {
|
|
+ this.server.getAsyncScheduler().cancelTasks(plugin); // Folia - region threading
|
|
+ } catch (Throwable ex) {
|
|
+ this.handlePluginException("Error occurred (in the plugin loader) while cancelling async tasks for " // Folia - region threading
|
|
+ pluginName + " (Is it up to date?)", ex, plugin); // Paper
|
|
}
|
|
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java b/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..d9687722e02dfd4088c7030abbf5008eb0a092c8
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java
|
|
@@ -0,0 +1,181 @@
|
|
+package io.papermc.paper.threadedregions;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+import io.papermc.paper.util.TickThread;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
|
+import net.minecraft.world.entity.Entity;
|
|
+import org.bukkit.craftbukkit.entity.CraftEntity;
|
|
+
|
|
+import java.util.ArrayDeque;
|
|
+import java.util.ArrayList;
|
|
+import java.util.List;
|
|
+import java.util.function.Consumer;
|
|
+
|
|
+/**
|
|
+ * An entity can move between worlds with an arbitrary tick delay, be temporarily removed
|
|
+ * for players (i.e end credits), be partially removed from world state (i.e inactive but not removed),
|
|
+ * teleport between ticking regions, teleport between worlds (which will change the underlying Entity object
|
|
+ * for non-players), and even be removed entirely from the server. The uncertainty of an entity's state can make
|
|
+ * it difficult to schedule tasks without worrying about undefined behaviors resulting from any of the states listed
|
|
+ * previously.
|
|
+ *
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ */
|
|
+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<? extends Entity> run, Consumer<? extends Entity> retired) {}
|
|
+
|
|
+ private long tickCount = 0L;
|
|
+ private static final long RETIRED_TICK_COUNT = -1L;
|
|
+ private final Object stateLock = new Object();
|
|
+ private final Long2ObjectOpenHashMap<List<ScheduledTask>> oneTimeDelayed = new Long2ObjectOpenHashMap<>();
|
|
+
|
|
+ private final ArrayDeque<ScheduledTask> 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.
|
|
+ *
|
|
+ * <p>
|
|
+ * Note: This should only be invoked after synchronously removing the entity from the world.
|
|
+ * </p>
|
|
+ *
|
|
+ * @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<Entity> retireTask = (Consumer<Entity>)task.retired;
|
|
+ if (retireTask == null) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ retireTask.accept(thisEntity);
|
|
+ }
|
|
+
|
|
+ for (final List<ScheduledTask> tasks : this.oneTimeDelayed.values()) {
|
|
+ for (int i = 0, len = tasks.size(); i < len; ++i) {
|
|
+ final ScheduledTask task = tasks.get(i);
|
|
+ final Consumer<Entity> retireTask = (Consumer<Entity>)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.
|
|
+ *
|
|
+ * <p>
|
|
+ * It is guaranteed that the run and retired callback are invoked on the region which owns the entity.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * 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, <b>only</b> the parameter provided should be used.
|
|
+ * </p>
|
|
+ * @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<? extends Entity> run, final Consumer<? extends Entity> 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<ScheduledTask> 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<Entity>)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..6c288d1db81c4d6f574674889a98553baf9b86ee
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java
|
|
@@ -0,0 +1,174 @@
|
|
+package io.papermc.paper.threadedregions;
|
|
+
|
|
+import com.mojang.logging.LogUtils;
|
|
+import io.papermc.paper.util.TickThread;
|
|
+import net.minecraft.server.MinecraftServer;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.world.entity.Entity;
|
|
+import net.minecraft.world.level.ChunkPos;
|
|
+import org.slf4j.Logger;
|
|
+import java.util.ArrayList;
|
|
+import java.util.List;
|
|
+import java.util.concurrent.TimeUnit;
|
|
+
|
|
+public final class RegionShutdownThread extends TickThread {
|
|
+
|
|
+ private static final Logger LOGGER = LogUtils.getClassLogger();
|
|
+
|
|
+ ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> shuttingDown;
|
|
+
|
|
+ public RegionShutdownThread(final String name) {
|
|
+ super(name);
|
|
+ this.setUncaughtExceptionHandler((thread, thr) -> {
|
|
+ LOGGER.error("Error shutting down server", thr);
|
|
+ });
|
|
+ }
|
|
+
|
|
+ static ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> getRegion() {
|
|
+ final Thread currentThread = Thread.currentThread();
|
|
+ if (currentThread instanceof RegionShutdownThread shutdownThread) {
|
|
+ return shutdownThread.shuttingDown;
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+
|
|
+ static RegionizedWorldData getWorldData() {
|
|
+ final Thread currentThread = Thread.currentThread();
|
|
+ if (currentThread instanceof RegionShutdownThread shutdownThread) {
|
|
+ // no fast path for shutting down
|
|
+ if (shutdownThread.shuttingDown != null) {
|
|
+ return shutdownThread.shuttingDown.getData().world.worldRegionData.get();
|
|
+ }
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ // The region shutdown thread bypasses all tick thread checks, which will allow us to execute global saves
|
|
+ // it will not however let us perform arbitrary sync loads, arbitrary world state lookups simply because
|
|
+ // the data required to do that is regionised, and we can only access it when we OWN the region, and we do not.
|
|
+ // Thus, the only operation that the shutdown thread will perform
|
|
+
|
|
+ private void saveLevelData(final ServerLevel world) {
|
|
+ try {
|
|
+ world.saveLevelData();
|
|
+ } catch (final Throwable thr) {
|
|
+ LOGGER.error("Failed to save level data for " + world.getWorld().getName(), thr);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void finishTeleportations(final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region,
|
|
+ final ServerLevel world) {
|
|
+ try {
|
|
+ this.shuttingDown = region;
|
|
+ final List<ServerLevel.PendingTeleport> pendingTeleports = world.removeAllRegionTeleports();
|
|
+ if (pendingTeleports.isEmpty()) {
|
|
+ return;
|
|
+ }
|
|
+ final ChunkPos center = region.getCenterChunk();
|
|
+ LOGGER.info("Completing " + pendingTeleports.size() + " pending teleports in region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'");
|
|
+ for (final ServerLevel.PendingTeleport pendingTeleport : pendingTeleports) {
|
|
+ LOGGER.info("Completing teleportation to target position " + pendingTeleport.to());
|
|
+
|
|
+ // first, add entities to entity chunk so that they will be saved
|
|
+ for (final Entity.EntityTreeNode node : pendingTeleport.rootVehicle().getFullTree()) {
|
|
+ // assume that world and position are set to destination here
|
|
+ node.root.level = world; // in case the pending teleport is from a portal before it finds the exact destination
|
|
+ world.getEntityLookup().addEntityForShutdownTeleportComplete(node.root);
|
|
+ }
|
|
+
|
|
+ // then, rebuild the passenger tree so that when saving only the root vehicle will be written - and if
|
|
+ // there are any player passengers, that the later player saving will save the tree
|
|
+ pendingTeleport.rootVehicle().restore();
|
|
+
|
|
+ // now we are finished
|
|
+ LOGGER.info("Completed teleportation to target position " + pendingTeleport.to());
|
|
+ }
|
|
+ } catch (final Throwable thr) {
|
|
+ LOGGER.error("Failed to complete pending teleports", thr);
|
|
+ } finally {
|
|
+ this.shuttingDown = null;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void saveRegionChunks(final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region,
|
|
+ final boolean last) {
|
|
+ ChunkPos center = null;
|
|
+ try {
|
|
+ this.shuttingDown = region;
|
|
+ center = region.getCenterChunk();
|
|
+ LOGGER.info("Saving chunks around region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'");
|
|
+ region.regioniser.world.chunkTaskScheduler.chunkHolderManager.close(true, true, false, last, false);
|
|
+ } catch (final Throwable thr) {
|
|
+ LOGGER.error("Failed to save chunks for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr);
|
|
+ } finally {
|
|
+ this.shuttingDown = null;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void haltChunkSystem(final ServerLevel world) {
|
|
+ try {
|
|
+ world.chunkTaskScheduler.chunkHolderManager.close(false, true, true, false, false);
|
|
+ } catch (final Throwable thr) {
|
|
+ LOGGER.error("Failed to halt chunk system for world '" + world.getWorld().getName() + "'", thr);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void haltWorldNoRegions(final ServerLevel world) {
|
|
+ try {
|
|
+ world.chunkTaskScheduler.chunkHolderManager.close(true, true, true, true, false);
|
|
+ } catch (final Throwable thr) {
|
|
+ LOGGER.error("Failed to close world '" + world.getWorld().getName() + "' with no regions", thr);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final void run() {
|
|
+ // await scheduler termination
|
|
+ LOGGER.info("Awaiting scheduler termination for 60s");
|
|
+ if (TickRegions.getScheduler().halt(true, TimeUnit.SECONDS.toNanos(60L))) {
|
|
+ LOGGER.info("Scheduler halted");
|
|
+ } else {
|
|
+ LOGGER.warn("Scheduler did not terminate within 60s, proceeding with shutdown anyways");
|
|
+ }
|
|
+
|
|
+ MinecraftServer.getServer().stopServer(); // stop part 1: most logic, kicking players, plugins, etc
|
|
+ // halt all chunk systems first so that any in-progress chunk generation stops
|
|
+ LOGGER.info("Halting chunk systems");
|
|
+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
|
|
+ try {
|
|
+ world.chunkTaskScheduler.halt(false, 0L);
|
|
+ } catch (final Throwable throwable) {
|
|
+ LOGGER.error("Failed to soft halt chunk system for world '" + world.getWorld().getName() + "'", throwable);
|
|
+ }
|
|
+ }
|
|
+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
|
|
+ this.haltChunkSystem(world);
|
|
+ }
|
|
+ LOGGER.info("Halted chunk systems");
|
|
+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
|
|
+ final List<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>>
|
|
+ regions = new ArrayList<>();
|
|
+ world.regioniser.computeForAllRegionsUnsynchronised(regions::add);
|
|
+
|
|
+ for (int i = 0, len = regions.size(); i < len; ++i) {
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region = regions.get(i);
|
|
+ this.finishTeleportations(region, world);
|
|
+ }
|
|
+
|
|
+ for (int i = 0, len = regions.size(); i < len; ++i) {
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region = regions.get(i);
|
|
+ this.saveRegionChunks(region, (i + 1) == len);
|
|
+ }
|
|
+
|
|
+ this.saveLevelData(world);
|
|
+ }
|
|
+ // moved from stop part 1
|
|
+ // we need this to be after saving level data, as that will complete any teleportations the player is in
|
|
+ LOGGER.info("Saving players");
|
|
+ MinecraftServer.getServer().getPlayerList().saveAll();
|
|
+
|
|
+ MinecraftServer.getServer().stopPart2(); // stop part 2: close other resources (io thread, etc)
|
|
+ // done, part 2 should call exit()
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..1f48ada99d6d24880f9bda1cd05d41a4562e42f5
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java
|
|
@@ -0,0 +1,235 @@
|
|
+package io.papermc.paper.threadedregions;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import javax.annotation.Nullable;
|
|
+import java.util.function.Supplier;
|
|
+
|
|
+/**
|
|
+ * Use to manage data that needs to be regionised.
|
|
+ * <p>
|
|
+ * <b>Note:</b> that unlike {@link ThreadLocal}, regionised data is not deleted once the {@code RegionizedData} object is GC'd.
|
|
+ * The data is held in reference to the world it resides in.
|
|
+ * </p>
|
|
+ * <P>
|
|
+ * <b>Note:</b> Keep in mind that when regionised ticking is disabled, the entire server is considered a single region.
|
|
+ * That is, the data may or may not cross worlds. As such, the {@code RegionizedData} object must be instanced
|
|
+ * per world when appropriate, as it is no longer guaranteed that separate worlds contain separate regions.
|
|
+ * See below for more details on instancing per world.
|
|
+ * </P>
|
|
+ * <p>
|
|
+ * Regionised data may be <b>world-checked</b>. That is, {@link #get()} may throw an exception if the current
|
|
+ * region's world does not match the {@code RegionizedData}'s world. Consider the usages of {@code RegionizedData} below
|
|
+ * see why the behavior may or may not be desirable:
|
|
+ * <pre>
|
|
+ * {@code
|
|
+ * public class EntityTickList {
|
|
+ * private final List<Entity> entities = new ArrayList<>();
|
|
+ *
|
|
+ * public void addEntity(Entity e) {
|
|
+ * this.entities.add(e);
|
|
+ * }
|
|
+ *
|
|
+ * public void removeEntity(Entity e) {
|
|
+ * this.entities.remove(e);
|
|
+ * }
|
|
+ * }
|
|
+ *
|
|
+ * public class World {
|
|
+ *
|
|
+ * // callback is left out of this example
|
|
+ * // note: world != null here
|
|
+ * public final RegionizedData<EntityTickList> entityTickLists =
|
|
+ * new RegionizedData<>(this, () -> new EntityTickList(), ...);
|
|
+ *
|
|
+ * public void addTickingEntity(Entity e) {
|
|
+ * // What we expect here is that this world is the
|
|
+ * // current ticking region's world.
|
|
+ * // If that is true, then calling this.entityTickLists.get()
|
|
+ * // will retrieve the current region's EntityTickList
|
|
+ * // for this world, which is fine since the current
|
|
+ * // region is contained within this world.
|
|
+ *
|
|
+ * // But if the current region's world is not this world,
|
|
+ * // and if the world check is disabled, then we will actually
|
|
+ * // retrieve _this_ world's EntityTickList for the region,
|
|
+ * // and NOT the EntityTickList for the region's world.
|
|
+ * // This is because the RegionizedData object is instantiated
|
|
+ * // per world.
|
|
+ * this.entityTickLists.get().addEntity(e);
|
|
+ * }
|
|
+ * }
|
|
+ *
|
|
+ * public class TickTimes {
|
|
+ *
|
|
+ * private final List<Long> 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<World> worlds = ...;
|
|
+ *
|
|
+ * // callback is left out of this example
|
|
+ * // note: world == null here, because this RegionizedData object
|
|
+ * // is not instantiated per world, but rather globally.
|
|
+ * public final RegionizedData<TickTimes> tickTimes =
|
|
+ * new RegionizedData<>(null, () -> new TickTimes(), ...);
|
|
+ * }
|
|
+ * }
|
|
+ * </pre>
|
|
+ * In general, it is advised that if a RegionizedData object is instantiated <i>per world</i>, that world checking
|
|
+ * is enabled for it by passing the world to the constructor.
|
|
+ * </p>
|
|
+ */
|
|
+public final class RegionizedData<T> {
|
|
+
|
|
+ private final ServerLevel world;
|
|
+ private final Supplier<T> initialValueSupplier;
|
|
+ private final RegioniserCallback<T> callback;
|
|
+
|
|
+ /**
|
|
+ * Creates a regionised data holder. The provided initial value supplier may not be null, and it must
|
|
+ * never produce {@code null} values.
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * 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 <b>and</b> region,
|
|
+ * however using {@code null} for tasks to run at the end of a tick is valid since the tasks are tied to
|
|
+ * region <b>only</b>.
|
|
+ * </p>
|
|
+ * @param world The world in which the region data resides.
|
|
+ * @param supplier Initial value supplier used to lazy initialise region data.
|
|
+ * @param callback Region callback to manage this regionised data.
|
|
+ */
|
|
+ public RegionizedData(final ServerLevel world, final Supplier<T> supplier, final RegioniserCallback<T> 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<T> getCallback() {
|
|
+ return this.callback;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the current data type for the current ticking region. If there is no region, returns {@code null}.
|
|
+ * @return the current data type for the current ticking region. If there is no region, returns {@code null}.
|
|
+ * @throws IllegalStateException If the following are true: The server is in region ticking mode,
|
|
+ * this {@code RegionizedData}'s world is not {@code null},
|
|
+ * and the current ticking region's world does not match this {@code RegionizedData}'s world.
|
|
+ */
|
|
+ public @Nullable T get() {
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region =
|
|
+ TickRegionScheduler.getCurrentRegion();
|
|
+
|
|
+ if (region == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (this.world != null && this.world != region.getData().world) {
|
|
+ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey());
|
|
+ }
|
|
+
|
|
+ return region.getData().getOrCreateRegionizedData(this);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Class responsible for handling merge / split requests from the regioniser.
|
|
+ * <p>
|
|
+ * It is critical to note that each function is called while holding the region lock.
|
|
+ * </p>
|
|
+ */
|
|
+ public static interface RegioniserCallback<T> {
|
|
+
|
|
+ /**
|
|
+ * Completely merges the data in {@code from} to {@code into}.
|
|
+ * <p>
|
|
+ * <b>Calculating Tick Offsets:</b>
|
|
+ * 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}.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * <b>Critical Notes:</b>
|
|
+ * <li>
|
|
+ * <ul>
|
|
+ * 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.
|
|
+ * </ul>
|
|
+ * <ul>
|
|
+ * This function may not throw any exceptions, or the server will be left in an unrecoverable state.
|
|
+ * </ul>
|
|
+ * </li>
|
|
+ * </p>
|
|
+ *
|
|
+ * @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}.
|
|
+ * <p>
|
|
+ * 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:
|
|
+ * <pre>
|
|
+ * {@code
|
|
+ * int chunkX = ...;
|
|
+ * int chunkZ = ...;
|
|
+ *
|
|
+ * int regionSectionX = chunkX >> chunkToRegionShift;
|
|
+ * int regionSectionZ = chunkZ >> chunkToRegionShift;
|
|
+ * long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(regionSectionX, regionSectionZ);
|
|
+ * }
|
|
+ * </pre>
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ *
|
|
+ * @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<T> regionToData, final ReferenceOpenHashSet<T> dataSet
|
|
+ );
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..6c1d55144f044f39926ddf998104950b9efe3ee1
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java
|
|
@@ -0,0 +1,348 @@
|
|
+package io.papermc.paper.threadedregions;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
|
|
+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
|
|
+import com.mojang.logging.LogUtils;
|
|
+import io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler;
|
|
+import io.papermc.paper.util.TickThread;
|
|
+import net.minecraft.CrashReport;
|
|
+import net.minecraft.ReportedException;
|
|
+import net.minecraft.network.Connection;
|
|
+import net.minecraft.network.PacketListener;
|
|
+import net.minecraft.network.PacketSendListener;
|
|
+import net.minecraft.network.chat.Component;
|
|
+import net.minecraft.network.chat.MutableComponent;
|
|
+import net.minecraft.network.protocol.game.ClientboundDisconnectPacket;
|
|
+import net.minecraft.server.MinecraftServer;
|
|
+import net.minecraft.server.dedicated.DedicatedServer;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
|
|
+import net.minecraft.server.network.ServerLoginPacketListenerImpl;
|
|
+import net.minecraft.world.level.GameRules;
|
|
+import org.bukkit.Bukkit;
|
|
+import org.slf4j.Logger;
|
|
+import java.util.ArrayList;
|
|
+import java.util.Collections;
|
|
+import java.util.List;
|
|
+import java.util.concurrent.CopyOnWriteArrayList;
|
|
+import java.util.concurrent.atomic.AtomicBoolean;
|
|
+import java.util.function.BooleanSupplier;
|
|
+
|
|
+public final class RegionizedServer {
|
|
+
|
|
+ private static final Logger LOGGER = LogUtils.getLogger();
|
|
+ private static final RegionizedServer INSTANCE = new RegionizedServer();
|
|
+
|
|
+ public final RegionizedTaskQueue taskQueue = new RegionizedTaskQueue();
|
|
+
|
|
+ private final CopyOnWriteArrayList<ServerLevel> worlds = new CopyOnWriteArrayList<>();
|
|
+ private final CopyOnWriteArrayList<Connection> connections = new CopyOnWriteArrayList<>();
|
|
+
|
|
+ private final MultiThreadedQueue<Runnable> globalTickQueue = new MultiThreadedQueue<>();
|
|
+
|
|
+ private final GlobalTickTickHandle tickHandle = new GlobalTickTickHandle(this);
|
|
+
|
|
+ public static RegionizedServer getInstance() {
|
|
+ return INSTANCE;
|
|
+ }
|
|
+
|
|
+ public void addConnection(final Connection conn) {
|
|
+ this.connections.add(conn);
|
|
+ }
|
|
+
|
|
+ private boolean removeConnection(final Connection conn) {
|
|
+ return this.connections.remove(conn);
|
|
+ }
|
|
+
|
|
+ public void addWorld(final ServerLevel world) {
|
|
+ this.worlds.add(world);
|
|
+ }
|
|
+
|
|
+ public void init() {
|
|
+ // call init event _before_ scheduling anything
|
|
+ new RegionizedServerInitEvent().callEvent();
|
|
+
|
|
+ // now we can schedule
|
|
+ this.tickHandle.setInitialStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS);
|
|
+ TickRegions.getScheduler().scheduleRegion(this.tickHandle);
|
|
+ TickRegions.getScheduler().init();
|
|
+ }
|
|
+
|
|
+ public void invalidateStatus() {
|
|
+ this.lastServerStatus = 0L;
|
|
+ }
|
|
+
|
|
+ public void addTaskWithoutNotify(final Runnable run) {
|
|
+ this.globalTickQueue.add(run);
|
|
+ }
|
|
+
|
|
+ public void addTask(final Runnable run) {
|
|
+ this.addTaskWithoutNotify(run);
|
|
+ TickRegions.getScheduler().setHasTasks(this.tickHandle);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the current tick of the region ticking.
|
|
+ * @throws IllegalStateException If there is no current region.
|
|
+ */
|
|
+ public static long getCurrentTick() throws IllegalStateException {
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region =
|
|
+ TickRegionScheduler.getCurrentRegion();
|
|
+ if (region == null) {
|
|
+ if (TickThread.isShutdownThread()) {
|
|
+ return 0L;
|
|
+ }
|
|
+ throw new IllegalStateException("No currently ticking region");
|
|
+ }
|
|
+ return region.getData().getCurrentTick();
|
|
+ }
|
|
+
|
|
+ public static boolean isGlobalTickThread() {
|
|
+ return INSTANCE.tickHandle == TickRegionScheduler.getCurrentTickingTask();
|
|
+ }
|
|
+
|
|
+ public static void ensureGlobalTickThread(final String reason) {
|
|
+ if (!isGlobalTickThread()) {
|
|
+ throw new IllegalStateException(reason);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static TickRegionScheduler.RegionScheduleHandle getGlobalTickData() {
|
|
+ return INSTANCE.tickHandle;
|
|
+ }
|
|
+
|
|
+ private static final class GlobalTickTickHandle extends TickRegionScheduler.RegionScheduleHandle {
|
|
+
|
|
+ private final RegionizedServer server;
|
|
+
|
|
+ private final AtomicBoolean scheduled = new AtomicBoolean();
|
|
+ private final AtomicBoolean ticking = new AtomicBoolean();
|
|
+
|
|
+ public GlobalTickTickHandle(final RegionizedServer server) {
|
|
+ super(null, SchedulerThreadPool.DEADLINE_NOT_SET);
|
|
+ this.server = server;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Only valid to call BEFORE scheduled!!!!
|
|
+ */
|
|
+ final void setInitialStart(final long start) {
|
|
+ if (this.scheduled.getAndSet(true)) {
|
|
+ throw new IllegalStateException("Double scheduling global tick");
|
|
+ }
|
|
+ this.updateScheduledStart(start);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected boolean tryMarkTicking() {
|
|
+ return !this.ticking.getAndSet(true);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected boolean markNotTicking() {
|
|
+ return this.ticking.getAndSet(false);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) {
|
|
+ this.drainTasks();
|
|
+ this.server.globalTick(tickCount);
|
|
+ }
|
|
+
|
|
+ private void drainTasks() {
|
|
+ while (this.runOneTask());
|
|
+ }
|
|
+
|
|
+ private boolean runOneTask() {
|
|
+ final Runnable run = this.server.globalTickQueue.poll();
|
|
+ if (run == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ // TODO try catch?
|
|
+ run.run();
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected boolean runRegionTasks(final BooleanSupplier canContinue) {
|
|
+ do {
|
|
+ if (!this.runOneTask()) {
|
|
+ return false;
|
|
+ }
|
|
+ } while (canContinue.getAsBoolean());
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected boolean hasIntermediateTasks() {
|
|
+ return !this.server.globalTickQueue.isEmpty();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private long lastServerStatus;
|
|
+ private long tickCount;
|
|
+
|
|
+ private void globalTick(final int tickCount) {
|
|
+ ++this.tickCount;
|
|
+ // expire invalid click command callbacks
|
|
+ io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue((int)this.tickCount);
|
|
+
|
|
+ // scheduler
|
|
+ ((FoliaGlobalRegionScheduler)Bukkit.getGlobalRegionScheduler()).tick();
|
|
+
|
|
+ // commands
|
|
+ ((DedicatedServer)MinecraftServer.getServer()).handleConsoleInputs();
|
|
+
|
|
+ // needs
|
|
+ // player ping sample
|
|
+ // world global tick
|
|
+ // connection tick
|
|
+
|
|
+ // tick player ping sample
|
|
+ this.tickPlayerSample();
|
|
+
|
|
+ // tick worlds
|
|
+ for (final ServerLevel world : this.worlds) {
|
|
+ this.globalTick(world, tickCount);
|
|
+ }
|
|
+
|
|
+ // tick connections
|
|
+ this.tickConnections();
|
|
+
|
|
+ // player list
|
|
+ MinecraftServer.getServer().getPlayerList().tick();
|
|
+ }
|
|
+
|
|
+ private void tickPlayerSample() {
|
|
+ final MinecraftServer mcServer = MinecraftServer.getServer();
|
|
+
|
|
+ final long currtime = System.nanoTime();
|
|
+
|
|
+ // player ping sample
|
|
+ // copied from MinecraftServer#tickServer
|
|
+ // note: we need to reorder setPlayers to be the last operation it does, rather than the first to avoid publishing
|
|
+ // an uncomplete status
|
|
+ if (currtime - this.lastServerStatus >= 5000000000L) {
|
|
+ this.lastServerStatus = currtime;
|
|
+ mcServer.rebuildServerStatus();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private boolean hasConnectionMovedToMain(final Connection conn) {
|
|
+ final PacketListener packetListener = conn.getPacketListener();
|
|
+
|
|
+ return (packetListener instanceof ServerGamePacketListenerImpl) ||
|
|
+ (packetListener instanceof ServerLoginPacketListenerImpl loginListener && loginListener.state.ordinal() >= ServerLoginPacketListenerImpl.State.HANDING_OFF.ordinal());
|
|
+ }
|
|
+
|
|
+ private void tickConnections() {
|
|
+ final List<Connection> 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()) : "<ip address withheld>", exception); // Paper
|
|
+ MutableComponent ichatmutablecomponent = Component.literal("Internal server error");
|
|
+
|
|
+ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> {
|
|
+ conn.disconnect(ichatmutablecomponent);
|
|
+ }));
|
|
+ conn.setReadOnly();
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // A global tick only updates things like weather / worldborder, basically anything in the world that is
|
|
+ // NOT tied to a specific region, but rather shared amongst all of them.
|
|
+ private void globalTick(final ServerLevel world, final int tickCount) {
|
|
+ // needs
|
|
+ // worldborder tick
|
|
+ // advancing the weather cycle
|
|
+ // sleep status thing
|
|
+ // updating sky brightness
|
|
+ // time ticking (game time + daylight), plus PrimayLevelDat#getScheduledEvents ticking
|
|
+
|
|
+ // Typically, we expect there to be a running region to drain a world's global chunk tasks. However,
|
|
+ // this may not be the case - and thus, only the global tick thread can do anything.
|
|
+ world.taskQueueRegionData.drainGlobalChunkTasks();
|
|
+
|
|
+ // worldborder tick
|
|
+ this.tickWorldBorder(world);
|
|
+
|
|
+ // weather cycle
|
|
+ this.advanceWeatherCycle(world);
|
|
+
|
|
+ // sleep status
|
|
+ this.checkNightSkip(world);
|
|
+
|
|
+ // update raids
|
|
+ this.updateRaids(world);
|
|
+
|
|
+ // sky brightness
|
|
+ this.updateSkyBrightness(world);
|
|
+
|
|
+ // time ticking (TODO API synchronisation?)
|
|
+ this.tickTime(world, tickCount);
|
|
+
|
|
+ world.updateTickData();
|
|
+ }
|
|
+
|
|
+ private void updateRaids(final ServerLevel world) {
|
|
+ world.getRaids().globalTick();
|
|
+ }
|
|
+
|
|
+ private void checkNightSkip(final ServerLevel world) {
|
|
+ world.tickSleep();
|
|
+ }
|
|
+
|
|
+ private void advanceWeatherCycle(final ServerLevel world) {
|
|
+ world.advanceWeatherCycle();
|
|
+ }
|
|
+
|
|
+ private void updateSkyBrightness(final ServerLevel world) {
|
|
+ world.updateSkyBrightness();
|
|
+ }
|
|
+
|
|
+ private void tickWorldBorder(final ServerLevel world) {
|
|
+ world.getWorldBorder().tick();
|
|
+ }
|
|
+
|
|
+ private void tickTime(final ServerLevel world, final int tickCount) {
|
|
+ if (world.tickTime) {
|
|
+ if (world.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
|
|
+ world.setDayTime(world.levelData.getDayTime() + (long)tickCount);
|
|
+ }
|
|
+ world.serverLevelData.setGameTime(world.serverLevelData.getGameTime() + (long)tickCount);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static final record WorldLevelData(ServerLevel world, long nonRedstoneGameTime, long dayTime) {
|
|
+
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..ac043fbc74874c205b821c3d2d011b921d5704fa
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java
|
|
@@ -0,0 +1,752 @@
|
|
+package io.papermc.paper.threadedregions;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
|
|
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
|
|
+import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager;
|
|
+import io.papermc.paper.util.CoordinateUtils;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
|
|
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.server.level.TicketType;
|
|
+import net.minecraft.util.Unit;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.ArrayDeque;
|
|
+import java.util.Iterator;
|
|
+import java.util.concurrent.atomic.AtomicLong;
|
|
+import java.util.concurrent.locks.ReentrantLock;
|
|
+
|
|
+public final class RegionizedTaskQueue {
|
|
+
|
|
+ private static final TicketType<Unit> 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<Runnable> globalChunkTask = new MultiThreadedQueue<>();
|
|
+ private final SWMRLong2ObjectHashTable<AtomicLong> referenceCounters = new SWMRLong2ObjectHashTable<>();
|
|
+
|
|
+ public WorldRegionTaskData(final ServerLevel world) {
|
|
+ this.world = world;
|
|
+ }
|
|
+
|
|
+ private boolean executeGlobalChunkTask() {
|
|
+ final Runnable run = this.globalChunkTask.poll();
|
|
+ if (run != null) {
|
|
+ run.run();
|
|
+ return true;
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ public void drainGlobalChunkTasks() {
|
|
+ while (this.executeGlobalChunkTask());
|
|
+ }
|
|
+
|
|
+ public void pushGlobalChunkTask(final Runnable run) {
|
|
+ this.globalChunkTask.add(run);
|
|
+ }
|
|
+
|
|
+ private PrioritisedQueue getQueue(final boolean synchronise, final int chunkX, final int chunkZ, final boolean isChunkTask) {
|
|
+ final ThreadedRegionizer<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> regioniser = this.world.regioniser;
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region
|
|
+ = synchronise ? regioniser.getRegionAtSynchronised(chunkX, chunkZ) : regioniser.getRegionAtUnsynchronised(chunkX, chunkZ);
|
|
+ if (region == null) {
|
|
+ return null;
|
|
+ }
|
|
+ final RegionTaskQueueData taskQueueData = region.getData().getTaskQueueData();
|
|
+ return (isChunkTask ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue);
|
|
+ }
|
|
+
|
|
+ private void removeTicket(final long coord) {
|
|
+ this.world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel(
|
|
+ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE
|
|
+ );
|
|
+ }
|
|
+
|
|
+ private void addTicket(final long coord) {
|
|
+ this.world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel(
|
|
+ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE
|
|
+ );
|
|
+ }
|
|
+
|
|
+ private void decrementReference(final AtomicLong reference, final long coord) {
|
|
+ final long val = reference.decrementAndGet();
|
|
+ if (val == 0L) {
|
|
+ final ReentrantLock ticketLock = this.world.chunkTaskScheduler.chunkHolderManager.ticketLock;
|
|
+ ticketLock.lock();
|
|
+ try {
|
|
+ if (this.referenceCounters.remove(coord, reference)) {
|
|
+ WorldRegionTaskData.this.removeTicket(coord);
|
|
+ } // else: race condition, something replaced our reference - not our issue anymore
|
|
+ } finally {
|
|
+ ticketLock.unlock();
|
|
+ }
|
|
+ } else if (val < 0L) {
|
|
+ throw new IllegalStateException("Reference count < 0: " + val);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private AtomicLong incrementReference(final long coord) {
|
|
+ final AtomicLong ret = this.referenceCounters.get(coord);
|
|
+ if (ret != null) {
|
|
+ // try to fast acquire counter
|
|
+ int failures = 0;
|
|
+ for (long curr = ret.get();;) {
|
|
+ if (curr == 0L) {
|
|
+ // failed to fast acquire as reference expired
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = ret.compareAndExchange(curr, curr + 1L))) {
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ ++failures;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // slow acquire
|
|
+ final ReentrantLock ticketLock = this.world.chunkTaskScheduler.chunkHolderManager.ticketLock;
|
|
+ ticketLock.lock();
|
|
+ try {
|
|
+ final AtomicLong replace = new AtomicLong(1L);
|
|
+ final AtomicLong valueInMap = this.referenceCounters.putIfAbsent(coord, replace);
|
|
+ if (valueInMap == null) {
|
|
+ // replaced, we should usually be here
|
|
+ this.addTicket(coord);
|
|
+ return replace;
|
|
+ } // else: need to attempt to acquire the reference
|
|
+
|
|
+ int failures = 0;
|
|
+ for (long curr = valueInMap.get();;) {
|
|
+ if (curr == 0L) {
|
|
+ // don't need to add ticket here, since ticket is only removed during the lock
|
|
+ // we just need to replace the value in the map so that the thread removing fails and doesn't
|
|
+ // remove the ticket (see decrementReference)
|
|
+ this.referenceCounters.put(coord, replace);
|
|
+ return replace;
|
|
+ }
|
|
+
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = valueInMap.compareAndExchange(curr, curr + 1L))) {
|
|
+ // acquired
|
|
+ return valueInMap;
|
|
+ }
|
|
+
|
|
+ ++failures;
|
|
+ }
|
|
+ } finally {
|
|
+ ticketLock.unlock();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static final class RegionTaskQueueData {
|
|
+ private final PrioritisedQueue tickTaskQueue = new PrioritisedQueue();
|
|
+ private final PrioritisedQueue chunkQueue = new PrioritisedQueue();
|
|
+ private final WorldRegionTaskData worldRegionTaskData;
|
|
+
|
|
+ public RegionTaskQueueData(final WorldRegionTaskData worldRegionTaskData) {
|
|
+ this.worldRegionTaskData = worldRegionTaskData;
|
|
+ }
|
|
+
|
|
+ void mergeInto(final RegionTaskQueueData into) {
|
|
+ this.tickTaskQueue.mergeInto(into.tickTaskQueue);
|
|
+ this.chunkQueue.mergeInto(into.chunkQueue);
|
|
+ }
|
|
+
|
|
+ public boolean executeTickTask() {
|
|
+ return this.tickTaskQueue.executeTask();
|
|
+ }
|
|
+
|
|
+ public boolean executeChunkTask() {
|
|
+ return this.worldRegionTaskData.executeGlobalChunkTask() || this.chunkQueue.executeTask();
|
|
+ }
|
|
+
|
|
+ void split(final ThreadedRegionizer<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> regioniser,
|
|
+ final Long2ReferenceOpenHashMap<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>> into) {
|
|
+ this.tickTaskQueue.split(
|
|
+ false, regioniser, into
|
|
+ );
|
|
+ this.chunkQueue.split(
|
|
+ true, regioniser, into
|
|
+ );
|
|
+ }
|
|
+
|
|
+ public void drainTasks() {
|
|
+ // first, update chunk loader
|
|
+ final ServerLevel world = this.worldRegionTaskData.world;
|
|
+ if (world.playerChunkLoader.tickMidTick()) {
|
|
+ // only process ticket updates if the player chunk loader did anything
|
|
+ world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates();
|
|
+ }
|
|
+
|
|
+ final PrioritisedQueue tickTaskQueue = this.tickTaskQueue;
|
|
+ final PrioritisedQueue chunkTaskQueue = this.chunkQueue;
|
|
+
|
|
+ int allowedTickTasks = tickTaskQueue.getScheduledTasks();
|
|
+ int allowedChunkTasks = chunkTaskQueue.getScheduledTasks();
|
|
+
|
|
+ boolean executeTickTasks = allowedTickTasks > 0;
|
|
+ boolean executeChunkTasks = allowedChunkTasks > 0;
|
|
+ boolean executeGlobalTasks = true;
|
|
+
|
|
+ do {
|
|
+ executeTickTasks = executeTickTasks && allowedTickTasks-- > 0 && tickTaskQueue.executeTask();
|
|
+ executeChunkTasks = executeChunkTasks && allowedChunkTasks-- > 0 && chunkTaskQueue.executeTask();
|
|
+ executeGlobalTasks = executeGlobalTasks && this.worldRegionTaskData.executeGlobalChunkTask();
|
|
+ } while (executeTickTasks | executeChunkTasks | executeGlobalTasks);
|
|
+
|
|
+ if (allowedChunkTasks > 0) {
|
|
+ // if we executed chunk tasks, we should try to process ticket updates for full status changes
|
|
+ world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public boolean hasTasks() {
|
|
+ return !this.tickTaskQueue.isEmpty() || !this.chunkQueue.isEmpty();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ static final class PrioritisedQueue {
|
|
+ private final ArrayDeque<ChunkBasedPriorityTask>[] 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<ChunkBasedPriorityTask> queue : this.queues) {
|
|
+ ret += queue.size();
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public boolean isEmpty() {
|
|
+ final ArrayDeque<ChunkBasedPriorityTask>[] 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<ChunkBasedPriorityTask>[] thisQueues) {
|
|
+ final ArrayDeque<ChunkBasedPriorityTask>[] otherQueues = target.queues;
|
|
+ for (int i = 0; i < thisQueues.length; ++i) {
|
|
+ final ArrayDeque<ChunkBasedPriorityTask> fromQ = thisQueues[i];
|
|
+ final ArrayDeque<ChunkBasedPriorityTask> 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<ChunkBasedPriorityTask> iterator = fromQ.descendingIterator(); iterator.hasNext();) {
|
|
+ intoQ.addFirst(iterator.next());
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // into is a map of section coordinate to region
|
|
+ public void split(final boolean isChunkData,
|
|
+ final ThreadedRegionizer<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> regioniser,
|
|
+ final Long2ReferenceOpenHashMap<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>> into) {
|
|
+ final Reference2ReferenceOpenHashMap<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>, ArrayDeque<ChunkBasedPriorityTask>[]>
|
|
+ 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<ChunkBasedPriorityTask>[] thisQueues = this.queues;
|
|
+ for (int i = 0; i < thisQueues.length; ++i) {
|
|
+ final ArrayDeque<ChunkBasedPriorityTask> fromQ = thisQueues[i];
|
|
+
|
|
+ for (final ChunkBasedPriorityTask task : fromQ) {
|
|
+ final int sectionX = task.chunkX >> shift;
|
|
+ final int sectionZ = task.chunkZ >> shift;
|
|
+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>
|
|
+ region = into.get(sectionKey);
|
|
+ if (region == null) {
|
|
+ throw new IllegalStateException();
|
|
+ }
|
|
+
|
|
+ split.computeIfAbsent(region, (keyInMap) -> {
|
|
+ final ArrayDeque<ChunkBasedPriorityTask>[] 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<Reference2ReferenceMap.Entry<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>, ArrayDeque<ChunkBasedPriorityTask>[]>>
|
|
+ iterator = split.reference2ReferenceEntrySet().fastIterator();
|
|
+ iterator.hasNext();) {
|
|
+ final Reference2ReferenceMap.Entry<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>, ArrayDeque<ChunkBasedPriorityTask>[]>
|
|
+ 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<ChunkBasedPriorityTask>[] 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<ChunkBasedPriorityTask>[] 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<ChunkBasedPriorityTask> queue = queues[i];
|
|
+ while ((task = queue.pollFirst()) != null) {
|
|
+ if ((referenceCounter = task.trySetCompleting(i)) != null) {
|
|
+ break search_loop;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (task == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ try {
|
|
+ task.executeInternal();
|
|
+ } finally {
|
|
+ task.world.decrementReference(referenceCounter, task.sectionLowerLeftCoord);
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ private static final class ChunkBasedPriorityTask implements PrioritisedExecutor.PrioritisedTask {
|
|
+
|
|
+ private static final AtomicLong REFERENCE_COUNTER_NOT_SET = new AtomicLong(-1L);
|
|
+
|
|
+ private final WorldRegionTaskData world;
|
|
+ private final int chunkX;
|
|
+ private final int chunkZ;
|
|
+ private final long sectionLowerLeftCoord; // chunk coordinate
|
|
+ private final boolean isChunkTask;
|
|
+
|
|
+ private volatile AtomicLong referenceCounter;
|
|
+ private static final VarHandle REFERENCE_COUNTER_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "referenceCounter", AtomicLong.class);
|
|
+ private Runnable run;
|
|
+ private volatile PrioritisedExecutor.Priority priority;
|
|
+ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "priority", PrioritisedExecutor.Priority.class);
|
|
+
|
|
+ ChunkBasedPriorityTask(final WorldRegionTaskData world, final int chunkX, final int chunkZ, final boolean isChunkTask,
|
|
+ final Runnable run, final PrioritisedExecutor.Priority priority) {
|
|
+ this.world = world;
|
|
+ this.chunkX = chunkX;
|
|
+ this.chunkZ = chunkZ;
|
|
+ this.isChunkTask = isChunkTask;
|
|
+ this.run = run;
|
|
+ this.setReferenceCounterPlain(REFERENCE_COUNTER_NOT_SET);
|
|
+ this.setPriorityPlain(priority);
|
|
+
|
|
+ final int regionShift = world.world.regioniser.sectionChunkShift;
|
|
+ final int regionMask = (1 << regionShift) - 1;
|
|
+
|
|
+ this.sectionLowerLeftCoord = CoordinateUtils.getChunkKey(chunkX & ~regionMask, chunkZ & ~regionMask);
|
|
+ }
|
|
+
|
|
+ private PrioritisedExecutor.Priority getPriorityVolatile() {
|
|
+ return (PrioritisedExecutor.Priority)PRIORITY_HANDLE.getVolatile(this);
|
|
+ }
|
|
+
|
|
+ private void setPriorityPlain(final PrioritisedExecutor.Priority priority) {
|
|
+ PRIORITY_HANDLE.set(this, priority);
|
|
+ }
|
|
+
|
|
+ private void setPriorityVolatile(final PrioritisedExecutor.Priority priority) {
|
|
+ PRIORITY_HANDLE.setVolatile(this, priority);
|
|
+ }
|
|
+
|
|
+ private PrioritisedExecutor.Priority compareAndExchangePriority(final PrioritisedExecutor.Priority expect, final PrioritisedExecutor.Priority update) {
|
|
+ return (PrioritisedExecutor.Priority)PRIORITY_HANDLE.compareAndExchange(this, expect, update);
|
|
+ }
|
|
+
|
|
+ private void setReferenceCounterPlain(final AtomicLong value) {
|
|
+ REFERENCE_COUNTER_HANDLE.set(this, value);
|
|
+ }
|
|
+
|
|
+ private AtomicLong getReferenceCounterVolatile() {
|
|
+ return (AtomicLong)REFERENCE_COUNTER_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ private AtomicLong compareAndExchangeReferenceCounter(final AtomicLong expect, final AtomicLong update) {
|
|
+ return (AtomicLong)REFERENCE_COUNTER_HANDLE.compareAndExchange(this, expect, update);
|
|
+ }
|
|
+
|
|
+ private void executeInternal() {
|
|
+ try {
|
|
+ this.run.run();
|
|
+ } finally {
|
|
+ this.run = null;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void cancelInternal() {
|
|
+ this.run = null;
|
|
+ }
|
|
+
|
|
+ private boolean tryComplete(final boolean cancel) {
|
|
+ int failures = 0;
|
|
+ for (AtomicLong curr = this.getReferenceCounterVolatile();;) {
|
|
+ if (curr == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) {
|
|
+ ++failures;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ // we have the reference count, we win no matter what.
|
|
+ this.setPriorityVolatile(PrioritisedExecutor.Priority.COMPLETING);
|
|
+
|
|
+ try {
|
|
+ if (cancel) {
|
|
+ this.cancelInternal();
|
|
+ } else {
|
|
+ this.executeInternal();
|
|
+ }
|
|
+ } finally {
|
|
+ if (curr != REFERENCE_COUNTER_NOT_SET) {
|
|
+ this.world.decrementReference(curr, this.sectionLowerLeftCoord);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean queue() {
|
|
+ if (this.getReferenceCounterVolatile() != REFERENCE_COUNTER_NOT_SET) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final AtomicLong referenceCounter = this.world.incrementReference(this.sectionLowerLeftCoord);
|
|
+ if (this.compareAndExchangeReferenceCounter(REFERENCE_COUNTER_NOT_SET, referenceCounter) != REFERENCE_COUNTER_NOT_SET) {
|
|
+ // we don't expect race conditions here, so it is OK if we have to needlessly reference count
|
|
+ this.world.decrementReference(referenceCounter, this.sectionLowerLeftCoord);
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ boolean synchronise = false;
|
|
+ for (;;) {
|
|
+ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve
|
|
+ // the same queue again, as the region lock will be given to us only when the merge/split operation
|
|
+ // is done
|
|
+ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask);
|
|
+
|
|
+ if (queue == null) {
|
|
+ if (!synchronise) {
|
|
+ // may be incorrectly null when unsynchronised
|
|
+ continue;
|
|
+ }
|
|
+ // may have been cancelled before we got to the queue
|
|
+ if (this.getReferenceCounterVolatile() != null) {
|
|
+ throw new IllegalStateException("Expected null ref count when queue does not exist");
|
|
+ }
|
|
+ // the task never could be polled from the queue, so we return false
|
|
+ // don't decrement reference count, as we were certainly cancelled by another thread, which
|
|
+ // will decrement the reference count
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ synchronise = true;
|
|
+
|
|
+ final Boolean res = queue.tryPush(this);
|
|
+ if (res == null) {
|
|
+ // we were cancelled
|
|
+ // don't decrement reference count, as we were certainly cancelled by another thread, which
|
|
+ // will decrement the reference count
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (!res.booleanValue()) {
|
|
+ // failed, try again
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ // successfully queued
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private AtomicLong trySetCompleting(final int minPriority) {
|
|
+ // first, try to set priority to EXECUTING
|
|
+ for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) {
|
|
+ if (curr.isLowerPriority(minPriority)) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = this.compareAndExchangePriority(curr, PrioritisedExecutor.Priority.COMPLETING))) {
|
|
+ break;
|
|
+ } // else: continue
|
|
+ }
|
|
+
|
|
+ for (AtomicLong curr = this.getReferenceCounterVolatile();;) {
|
|
+ if (curr == null) {
|
|
+ // something acquired before us
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (curr == REFERENCE_COUNTER_NOT_SET) {
|
|
+ throw new IllegalStateException();
|
|
+ }
|
|
+
|
|
+ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) {
|
|
+ continue;
|
|
+ }
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void updatePriorityInQueue() {
|
|
+ boolean synchronise = false;
|
|
+ for (;;) {
|
|
+ final AtomicLong referenceCount = this.getReferenceCounterVolatile();
|
|
+ if (referenceCount == REFERENCE_COUNTER_NOT_SET || referenceCount == null) {
|
|
+ // cancelled or not queued
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (this.getPriorityVolatile() == PrioritisedExecutor.Priority.COMPLETING) {
|
|
+ // cancelled
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve
|
|
+ // the same queue again, as the region lock will be given to us only when the merge/split operation
|
|
+ // is done
|
|
+ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask);
|
|
+
|
|
+ if (queue == null) {
|
|
+ if (!synchronise) {
|
|
+ // may be incorrectly null when unsynchronised
|
|
+ continue;
|
|
+ }
|
|
+ // must have been removed
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ synchronise = true;
|
|
+
|
|
+ final Boolean res = queue.tryPush(this);
|
|
+ if (res == null) {
|
|
+ // we were cancelled
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (!res.booleanValue()) {
|
|
+ // failed, try again
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ // successfully queued
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public PrioritisedExecutor.Priority getPriority() {
|
|
+ return this.getPriorityVolatile();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean lowerPriority(final PrioritisedExecutor.Priority priority) {
|
|
+ int failures = 0;
|
|
+ for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) {
|
|
+ if (curr == PrioritisedExecutor.Priority.COMPLETING) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (curr.isLowerOrEqualPriority(priority)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
|
|
+ this.updatePriorityInQueue();
|
|
+ return true;
|
|
+ }
|
|
+ ++failures;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean setPriority(final PrioritisedExecutor.Priority priority) {
|
|
+ int failures = 0;
|
|
+ for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) {
|
|
+ if (curr == PrioritisedExecutor.Priority.COMPLETING) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (curr == priority) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
|
|
+ this.updatePriorityInQueue();
|
|
+ return true;
|
|
+ }
|
|
+ ++failures;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean raisePriority(final PrioritisedExecutor.Priority priority) {
|
|
+ int failures = 0;
|
|
+ for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) {
|
|
+ if (curr == PrioritisedExecutor.Priority.COMPLETING) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (curr.isHigherOrEqualPriority(priority)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
|
|
+ this.updatePriorityInQueue();
|
|
+ return true;
|
|
+ }
|
|
+ ++failures;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean execute() {
|
|
+ return this.tryComplete(false);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean cancel() {
|
|
+ return this.tryComplete(true);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..82788131d513c1cc23de93fd4e3c44ea610ca66c
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java
|
|
@@ -0,0 +1,649 @@
|
|
+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.VisibleForDebug;
|
|
+import net.minecraft.world.entity.Entity;
|
|
+import net.minecraft.world.entity.Mob;
|
|
+import net.minecraft.world.entity.ai.village.VillageSiege;
|
|
+import net.minecraft.world.entity.item.ItemEntity;
|
|
+import net.minecraft.world.level.BlockEventData;
|
|
+import net.minecraft.world.level.ChunkPos;
|
|
+import net.minecraft.world.level.Level;
|
|
+import net.minecraft.world.level.NaturalSpawner;
|
|
+import net.minecraft.world.level.block.Block;
|
|
+import net.minecraft.world.level.block.entity.BlockEntity;
|
|
+import net.minecraft.world.level.block.entity.TickingBlockEntity;
|
|
+import net.minecraft.world.level.chunk.LevelChunk;
|
|
+import net.minecraft.world.level.material.Fluid;
|
|
+import net.minecraft.world.level.redstone.CollectingNeighborUpdater;
|
|
+import net.minecraft.world.level.redstone.NeighborUpdater;
|
|
+import net.minecraft.world.phys.AABB;
|
|
+import net.minecraft.world.ticks.LevelTicks;
|
|
+import org.bukkit.craftbukkit.block.CraftBlockState;
|
|
+import org.bukkit.craftbukkit.util.UnsafeList;
|
|
+import org.slf4j.Logger;
|
|
+import javax.annotation.Nullable;
|
|
+import java.util.ArrayDeque;
|
|
+import java.util.ArrayList;
|
|
+import java.util.Arrays;
|
|
+import java.util.Collection;
|
|
+import java.util.Collections;
|
|
+import java.util.Iterator;
|
|
+import java.util.List;
|
|
+import java.util.Map;
|
|
+import java.util.function.Consumer;
|
|
+import java.util.function.Predicate;
|
|
+
|
|
+public final class RegionizedWorldData {
|
|
+
|
|
+ private static final Logger LOGGER = LogUtils.getLogger();
|
|
+
|
|
+ public static final RegionizedData.RegioniserCallback<RegionizedWorldData> REGION_CALLBACK = new RegionizedData.RegioniserCallback<>() {
|
|
+ @Override
|
|
+ public void merge(final RegionizedWorldData from, final RegionizedWorldData into, final long fromTickOffset) {
|
|
+ // connections
|
|
+ for (final Connection conn : from.connections) {
|
|
+ into.connections.add(conn);
|
|
+ }
|
|
+ // time
|
|
+ final long fromRedstoneTimeOffset = 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<Entity> iterator = from.entityTickList.unsafeIterator(); iterator.hasNext();) {
|
|
+ into.entityTickList.add(iterator.next());
|
|
+ }
|
|
+ for (final Iterator<Mob> 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<LevelChunk> iterator = from.entityTickingChunks.unsafeIterator(); iterator.hasNext();) {
|
|
+ into.entityTickingChunks.add(iterator.next());
|
|
+ }
|
|
+ // redstone torches
|
|
+ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) {
|
|
+ if (into.redstoneUpdateInfos == null) {
|
|
+ into.redstoneUpdateInfos = new ArrayDeque<>();
|
|
+ }
|
|
+ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) {
|
|
+ info.offsetTime(fromRedstoneTimeOffset);
|
|
+ into.redstoneUpdateInfos.add(info);
|
|
+ }
|
|
+ }
|
|
+ // light chunks being worked on
|
|
+ into.chunksBeingWorkedOn.putAll(from.chunksBeingWorkedOn);
|
|
+ // mob spawning
|
|
+ into.catSpawnerNextTick = Math.max(from.catSpawnerNextTick, into.catSpawnerNextTick);
|
|
+ into.patrolSpawnerNextTick = Math.max(from.patrolSpawnerNextTick, into.patrolSpawnerNextTick);
|
|
+ into.phantomSpawnerNextTick = Math.max(from.phantomSpawnerNextTick, into.phantomSpawnerNextTick);
|
|
+ if (from.wanderingTraderTickDelay != Integer.MIN_VALUE && into.wanderingTraderTickDelay != Integer.MIN_VALUE) {
|
|
+ into.wanderingTraderTickDelay = Math.max(from.wanderingTraderTickDelay, into.wanderingTraderTickDelay);
|
|
+ into.wanderingTraderSpawnDelay = Math.max(from.wanderingTraderSpawnDelay, into.wanderingTraderSpawnDelay);
|
|
+ into.wanderingTraderSpawnChance = Math.max(from.wanderingTraderSpawnChance, into.wanderingTraderSpawnChance);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void split(final RegionizedWorldData from, final int chunkToRegionShift,
|
|
+ final Long2ReferenceOpenHashMap<RegionizedWorldData> regionToData,
|
|
+ final ReferenceOpenHashSet<RegionizedWorldData> dataSet) {
|
|
+ // connections
|
|
+ for (final Connection conn : from.connections) {
|
|
+ final ServerPlayer player = conn.getPlayer();
|
|
+ final ChunkPos pos = player.chunkPosition();
|
|
+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
|
|
+ // the chunk holder must _exist_, and so the region section exists.
|
|
+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
|
|
+ .connections.add(conn);
|
|
+ }
|
|
+ // entities
|
|
+ for (final ServerPlayer player : from.localPlayers) {
|
|
+ final ChunkPos pos = player.chunkPosition();
|
|
+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
|
|
+ // the chunk holder must _exist_, and so the region section exists.
|
|
+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
|
|
+ .localPlayers.add(player);
|
|
+ }
|
|
+ for (final Entity entity : from.allEntities) {
|
|
+ final ChunkPos pos = entity.chunkPosition();
|
|
+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
|
|
+ // the chunk holder must _exist_, and so the region section exists.
|
|
+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift));
|
|
+ into.allEntities.add(entity);
|
|
+ // Note: entityTickList is a subset of allEntities
|
|
+ if (from.entityTickList.contains(entity)) {
|
|
+ into.entityTickList.add(entity);
|
|
+ }
|
|
+ // Note: navigatingMobs is a subset of allEntities
|
|
+ if (entity instanceof Mob mob && from.navigatingMobs.contains(mob)) {
|
|
+ into.navigatingMobs.add(mob);
|
|
+ }
|
|
+ }
|
|
+ // block ticking
|
|
+ for (final BlockEventData blockEventData : from.blockEvents) {
|
|
+ final BlockPos pos = blockEventData.pos();
|
|
+ final int chunkX = pos.getX() >> 4;
|
|
+ final int chunkZ = pos.getZ() >> 4;
|
|
+
|
|
+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift));
|
|
+ // Unlike entities, the chunk holder is not guaranteed to exist for block events, because the block events
|
|
+ // is just some list. So if it unloads, I guess it's just lost.
|
|
+ if (into != null) {
|
|
+ into.blockEvents.add(blockEventData);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final Long2ReferenceOpenHashMap<LevelTicks<Block>> levelTicksBlockRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f);
|
|
+ final Long2ReferenceOpenHashMap<LevelTicks<Fluid>> levelTicksFluidRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f);
|
|
+
|
|
+ for (final Iterator<Long2ReferenceMap.Entry<RegionizedWorldData>> iterator = regionToData.long2ReferenceEntrySet().fastIterator();
|
|
+ iterator.hasNext();) {
|
|
+ final Long2ReferenceMap.Entry<RegionizedWorldData> entry = iterator.next();
|
|
+ final long key = entry.getLongKey();
|
|
+ final RegionizedWorldData worldData = entry.getValue();
|
|
+
|
|
+ levelTicksBlockRegionData.put(key, worldData.blockLevelTicks);
|
|
+ levelTicksFluidRegionData.put(key, worldData.fluidLevelTicks);
|
|
+ }
|
|
+
|
|
+ from.blockLevelTicks.split(chunkToRegionShift, levelTicksBlockRegionData);
|
|
+ from.fluidLevelTicks.split(chunkToRegionShift, levelTicksFluidRegionData);
|
|
+
|
|
+ // tile entity ticking
|
|
+ for (final TickingBlockEntity tileEntity : from.pendingBlockEntityTickers) {
|
|
+ final BlockPos pos = tileEntity.getPos();
|
|
+ final int chunkX = pos.getX() >> 4;
|
|
+ final int chunkZ = pos.getZ() >> 4;
|
|
+
|
|
+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift));
|
|
+ if (into != null) {
|
|
+ into.pendingBlockEntityTickers.add(tileEntity);
|
|
+ } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets
|
|
+ // marked as removed. So if there is no section, it's probably removed!
|
|
+ }
|
|
+ for (final TickingBlockEntity tileEntity : from.blockEntityTickers) {
|
|
+ final BlockPos pos = tileEntity.getPos();
|
|
+ final int chunkX = pos.getX() >> 4;
|
|
+ final int chunkZ = pos.getZ() >> 4;
|
|
+
|
|
+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift));
|
|
+ if (into != null) {
|
|
+ into.blockEntityTickers.add(tileEntity);
|
|
+ } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets
|
|
+ // marked as removed. So if there is no section, it's probably removed!
|
|
+ }
|
|
+ // time
|
|
+ for (final RegionizedWorldData regionizedWorldData : dataSet) {
|
|
+ regionizedWorldData.redstoneTime = from.redstoneTime;
|
|
+ }
|
|
+ // ticking chunks
|
|
+ for (final Iterator<LevelChunk> iterator = from.entityTickingChunks.unsafeIterator(); iterator.hasNext();) {
|
|
+ final LevelChunk levelChunk = iterator.next();
|
|
+ final ChunkPos pos = levelChunk.getPos();
|
|
+
|
|
+ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded
|
|
+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
|
|
+ .entityTickingChunks.add(levelChunk);
|
|
+ }
|
|
+ // redstone torches
|
|
+ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) {
|
|
+ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) {
|
|
+ final BlockPos pos = info.pos;
|
|
+
|
|
+ final RegionizedWorldData worldData = regionToData.get(CoordinateUtils.getChunkKey((pos.getX() >> 4) >> chunkToRegionShift, (pos.getZ() >> 4) >> chunkToRegionShift));
|
|
+ if (worldData != null) {
|
|
+ if (worldData.redstoneUpdateInfos == null) {
|
|
+ worldData.redstoneUpdateInfos = new ArrayDeque<>();
|
|
+ }
|
|
+ worldData.redstoneUpdateInfos.add(info);
|
|
+ } // else: chunk unloaded
|
|
+ }
|
|
+ }
|
|
+ // light chunks being worked on
|
|
+ for (final Iterator<Long2IntOpenHashMap.Entry> iterator = from.chunksBeingWorkedOn.long2IntEntrySet().fastIterator(); iterator.hasNext();) {
|
|
+ final Long2IntOpenHashMap.Entry entry = iterator.next();
|
|
+ final long pos = entry.getLongKey();
|
|
+ final int chunkX = CoordinateUtils.getChunkX(pos);
|
|
+ final int chunkZ = CoordinateUtils.getChunkZ(pos);
|
|
+ final int value = entry.getIntValue();
|
|
+
|
|
+ // should never be null, as it is a reference counter for ticket
|
|
+ regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)).chunksBeingWorkedOn.put(pos, value);
|
|
+ }
|
|
+ // mob spawning
|
|
+ for (final RegionizedWorldData regionizedWorldData : dataSet) {
|
|
+ regionizedWorldData.catSpawnerNextTick = from.catSpawnerNextTick;
|
|
+ regionizedWorldData.patrolSpawnerNextTick = from.patrolSpawnerNextTick;
|
|
+ regionizedWorldData.phantomSpawnerNextTick = from.phantomSpawnerNextTick;
|
|
+ regionizedWorldData.wanderingTraderTickDelay = from.wanderingTraderTickDelay;
|
|
+ regionizedWorldData.wanderingTraderSpawnChance = from.wanderingTraderSpawnChance;
|
|
+ regionizedWorldData.wanderingTraderSpawnDelay = from.wanderingTraderSpawnDelay;
|
|
+ regionizedWorldData.villageSiegeState = new VillageSiegeState(); // just re set it, as the spawn pos will be invalid
|
|
+ }
|
|
+ }
|
|
+ };
|
|
+
|
|
+ public final ServerLevel world;
|
|
+
|
|
+ private RegionizedServer.WorldLevelData tickData;
|
|
+
|
|
+ // connections
|
|
+ public final List<Connection> 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<ServerPlayer> localPlayers = new ArrayList<>();
|
|
+ private final ReferenceList<Entity> allEntities = new ReferenceList<>();
|
|
+ private final IteratorSafeOrderedReferenceSet<Entity> entityTickList = new IteratorSafeOrderedReferenceSet<>();
|
|
+ private final IteratorSafeOrderedReferenceSet<Mob> navigatingMobs = new IteratorSafeOrderedReferenceSet<>();
|
|
+
|
|
+ // block ticking
|
|
+ private final ObjectLinkedOpenHashSet<BlockEventData> blockEvents = new ObjectLinkedOpenHashSet<>();
|
|
+ private final LevelTicks<Block> blockLevelTicks;
|
|
+ private final LevelTicks<Fluid> fluidLevelTicks;
|
|
+
|
|
+ // tile entity ticking
|
|
+ private final List<TickingBlockEntity> pendingBlockEntityTickers = new ArrayList<>();
|
|
+ private final List<TickingBlockEntity> 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<LevelChunk> entityTickingChunks = new IteratorSafeOrderedReferenceSet<>();
|
|
+
|
|
+ // Paper/CB api hook misc
|
|
+ // don't bother to merge/split these, no point
|
|
+ // From ServerLevel
|
|
+ public boolean hasPhysicsEvent = true; // Paper
|
|
+ public boolean hasEntityMoveEvent = false; // Paper
|
|
+ // Paper start - Optimize Hoppers
|
|
+ public boolean skipPullModeEventFire = false;
|
|
+ public boolean skipPushModeEventFire = false;
|
|
+ public boolean skipHopperEvents = false;
|
|
+ // Paper end - Optimize Hoppers
|
|
+ public long lastMidTickExecuteFailure;
|
|
+ public long lastMidTickExecute;
|
|
+ // From Level
|
|
+ public boolean populating;
|
|
+ public final NeighborUpdater neighborUpdater;
|
|
+ public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710
|
|
+ public boolean captureBlockStates = false;
|
|
+ public boolean captureTreeGeneration = false;
|
|
+ public final Map<BlockPos, CraftBlockState> capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper
|
|
+ public final Map<BlockPos, BlockEntity> capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper
|
|
+ public List<ItemEntity> captureDrops;
|
|
+ // Paper start
|
|
+ public int wakeupInactiveRemainingAnimals;
|
|
+ public int wakeupInactiveRemainingFlying;
|
|
+ public int wakeupInactiveRemainingMonsters;
|
|
+ public int wakeupInactiveRemainingVillagers;
|
|
+ // Paper end
|
|
+ public final TempCollisionList<AABB> tempCollisionList = new TempCollisionList<>();
|
|
+ public final TempCollisionList<Entity> tempEntitiesList = new TempCollisionList<>();
|
|
+ public int currentPrimedTnt = 0; // Spigot
|
|
+ @Nullable
|
|
+ @VisibleForDebug
|
|
+ public NaturalSpawner.SpawnState lastSpawnState;
|
|
+ public boolean shouldSignal = true;
|
|
+
|
|
+ // not transient
|
|
+ public java.util.ArrayDeque<net.minecraft.world.level.block.RedstoneTorchBlock.Toggle> redstoneUpdateInfos;
|
|
+ public final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap();
|
|
+
|
|
+ public static final class TempCollisionList<T> {
|
|
+ final UnsafeList<T> list = new UnsafeList<>(64);
|
|
+ boolean inUse;
|
|
+
|
|
+ public UnsafeList<T> get() {
|
|
+ if (this.inUse) {
|
|
+ return new UnsafeList<>(16);
|
|
+ }
|
|
+ this.inUse = true;
|
|
+ return this.list;
|
|
+ }
|
|
+
|
|
+ public void ret(List<T> 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<ServerPlayer> pooledHashSets = new PooledLinkedHashSets<>();
|
|
+ public final PlayerAreaMap mobSpawnMap = new PlayerAreaMap(this.pooledHashSets);
|
|
+ public int catSpawnerNextTick = 0;
|
|
+ public int patrolSpawnerNextTick = 0;
|
|
+ public int phantomSpawnerNextTick = 0;
|
|
+ public int wanderingTraderTickDelay = Integer.MIN_VALUE;
|
|
+ public int wanderingTraderSpawnDelay;
|
|
+ public int wanderingTraderSpawnChance;
|
|
+ public VillageSiegeState villageSiegeState = new VillageSiegeState();
|
|
+
|
|
+ public static final class VillageSiegeState {
|
|
+ public boolean hasSetupSiege;
|
|
+ public VillageSiege.State siegeState = VillageSiege.State.SIEGE_DONE;
|
|
+ public int zombiesToSpawn;
|
|
+ public int nextSpawnTime;
|
|
+ public int spawnX;
|
|
+ public int spawnY;
|
|
+ public int spawnZ;
|
|
+ }
|
|
+
|
|
+ public RegionizedWorldData(final ServerLevel world) {
|
|
+ this.world = world;
|
|
+ this.blockLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world.getProfilerSupplier(), world, true);
|
|
+ this.fluidLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world.getProfilerSupplier(), world, false);
|
|
+ this.neighborUpdater = new CollectingNeighborUpdater(world, world.neighbourUpdateMax);
|
|
+
|
|
+ // tasks may be drained before the region ticks, so we must set up the tick data early just in case
|
|
+ this.updateTickData();
|
|
+ }
|
|
+
|
|
+ public void checkWorld(final Level against) {
|
|
+ if (this.world != against) {
|
|
+ throw new IllegalStateException("World mismatch: expected " + this.world.getWorld().getName() + " but got " + (against == null ? "null" : against.getWorld().getName()));
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public RegionizedServer.WorldLevelData getTickData() {
|
|
+ return this.tickData;
|
|
+ }
|
|
+
|
|
+ public void updateTickData() {
|
|
+ this.tickData = this.world.tickData;
|
|
+ this.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper
|
|
+ this.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper
|
|
+ this.skipHopperEvents = this.world.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper
|
|
+ }
|
|
+
|
|
+ // connections
|
|
+ public void tickConnections() {
|
|
+ final List<Connection> 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()) : "<ip address withheld>", 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<Entity> getLocalEntities() {
|
|
+ return this.allEntities;
|
|
+ }
|
|
+
|
|
+ public Entity[] getLocalEntitiesCopy() {
|
|
+ return Arrays.copyOf(this.allEntities.getRawData(), this.allEntities.size(), Entity[].class);
|
|
+ }
|
|
+
|
|
+ public List<ServerPlayer> 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<Entity> action) {
|
|
+ final IteratorSafeOrderedReferenceSet.Iterator<Entity> 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<Mob> 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<? extends BlockEventData> blockEvents) {
|
|
+ for (final BlockEventData blockEventData : blockEvents) {
|
|
+ this.pushBlockEvent(blockEventData);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void removeIfBlockEvents(final Predicate<? super BlockEventData> predicate) {
|
|
+ for (final Iterator<BlockEventData> 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<Block> getBlockLevelTicks() {
|
|
+ return this.blockLevelTicks;
|
|
+ }
|
|
+
|
|
+ public LevelTicks<Fluid> 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<TickingBlockEntity> 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<LevelChunk> getEntityTickingChunks() {
|
|
+ return this.entityTickingChunks;
|
|
+ }
|
|
+}
|
|
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<Entity> onComplete) {
|
|
+ // retrieve coordinates
|
|
+ final Completable<Location> positionCompletable = new Completable<>();
|
|
+
|
|
+ positionCompletable.addWaiter(
|
|
+ (final Location loc, final Throwable thr) -> {
|
|
+ if (loc == null) {
|
|
+ onComplete.accept(null);
|
|
+ return;
|
|
+ }
|
|
+ final boolean scheduled = from.getBukkitEntity().taskScheduler.schedule(
|
|
+ (final Entity realFrom) -> {
|
|
+ final Vec3 pos = new Vec3(
|
|
+ loc.getX(), loc.getY(), loc.getZ()
|
|
+ );
|
|
+ (useFromRootVehicle ? realFrom.getRootVehicle() : realFrom).teleportAsync(
|
|
+ ((CraftWorld)loc.getWorld()).getHandle(), pos, null, null, null,
|
|
+ cause, teleportFlags, onComplete
|
|
+ );
|
|
+ },
|
|
+ (final Entity retired) -> {
|
|
+ onComplete.accept(null);
|
|
+ },
|
|
+ 1L
|
|
+ );
|
|
+ if (!scheduled) {
|
|
+ onComplete.accept(null);
|
|
+ }
|
|
+ }
|
|
+ );
|
|
+
|
|
+ final boolean scheduled = to.getBukkitEntity().taskScheduler.schedule(
|
|
+ (final Entity target) -> {
|
|
+ positionCompletable.complete(target.getBukkitEntity().getLocation());
|
|
+ },
|
|
+ (final Entity retired) -> {
|
|
+ onComplete.accept(null);
|
|
+ },
|
|
+ 1L
|
|
+ );
|
|
+ if (!scheduled) {
|
|
+ onComplete.accept(null);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private TeleportUtils() {}
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..ff290d9dcd3494215ce7e41945ef47f00e8c9489
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java
|
|
@@ -0,0 +1,1211 @@
|
|
+package io.papermc.paper.threadedregions;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import com.destroystokyo.paper.util.SneakyThrow;
|
|
+import com.mojang.logging.LogUtils;
|
|
+import io.papermc.paper.util.CoordinateUtils;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.longs.LongArrayList;
|
|
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
|
|
+import net.minecraft.core.BlockPos;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.world.entity.Entity;
|
|
+import net.minecraft.world.level.ChunkPos;
|
|
+import org.slf4j.Logger;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.ArrayList;
|
|
+import java.util.Arrays;
|
|
+import java.util.Iterator;
|
|
+import java.util.List;
|
|
+import java.util.concurrent.atomic.AtomicLong;
|
|
+import java.util.concurrent.locks.StampedLock;
|
|
+import java.util.function.BooleanSupplier;
|
|
+import java.util.function.Consumer;
|
|
+
|
|
+public final class ThreadedRegionizer<R extends ThreadedRegionizer.ThreadedRegionData<R, S>, S extends ThreadedRegionizer.ThreadedRegionSectionData> {
|
|
+
|
|
+ private static final Logger LOGGER = LogUtils.getLogger();
|
|
+
|
|
+ public final int regionSectionChunkSize;
|
|
+ public final int sectionChunkShift;
|
|
+ public final int minSectionRecalcCount;
|
|
+ public final int emptySectionCreateRadius;
|
|
+ public final int regionSectionMergeRadius;
|
|
+ public final double maxDeadRegionPercent;
|
|
+ public final ServerLevel world;
|
|
+
|
|
+ private final SWMRLong2ObjectHashTable<ThreadedRegionSection<R, S>> sections = new SWMRLong2ObjectHashTable<>();
|
|
+ private final SWMRLong2ObjectHashTable<ThreadedRegion<R, S>> regionsById = new SWMRLong2ObjectHashTable<>();
|
|
+ private final RegionCallbacks<R, S> callbacks;
|
|
+ private final StampedLock regionLock = new StampedLock();
|
|
+ private Thread writeLockOwner;
|
|
+
|
|
+ /*
|
|
+ static final record Operation(String type, int chunkX, int chunkZ) {}
|
|
+ private final MultiThreadedQueue<Operation> ops = new MultiThreadedQueue<>();
|
|
+ */
|
|
+
|
|
+ /*
|
|
+ * See REGION_LOGIC.md for complete details on what this class is doing
|
|
+ */
|
|
+
|
|
+ public ThreadedRegionizer(final int minSectionRecalcCount, final double maxDeadRegionPercent,
|
|
+ final int emptySectionCreateRadius, final int regionSectionMergeRadius,
|
|
+ final int regionSectionChunkShift, final ServerLevel world,
|
|
+ final RegionCallbacks<R, S> 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<Operation> ops = new ArrayList<>();
|
|
+
|
|
+ for (JsonElement elem : arr) {
|
|
+ JsonObject obj = elem.getAsJsonObject();
|
|
+ String val = obj.get("value").getAsString();
|
|
+
|
|
+ String type = substr(val, "type=", 0);
|
|
+ String x = substr(val, "chunkX=", 0);
|
|
+ String z = substr(val, "chunkZ=", 0);
|
|
+
|
|
+ ops.add(new Operation(type, Integer.parseInt(x), Integer.parseInt(z)));
|
|
+ }
|
|
+
|
|
+ for (Operation op : ops) {
|
|
+ switch (op.type) {
|
|
+ case "add": {
|
|
+ this.addChunk(op.chunkX, op.chunkZ);
|
|
+ break;
|
|
+ }
|
|
+ case "remove": {
|
|
+ this.removeChunk(op.chunkX, op.chunkZ);
|
|
+ break;
|
|
+ }
|
|
+ case "mark_ticking": {
|
|
+ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.tryMarkTicking();
|
|
+ break;
|
|
+ }
|
|
+ case "rel_region": {
|
|
+ if (this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.state == ThreadedRegion.STATE_TICKING) {
|
|
+ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.markNotTicking();
|
|
+ }
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ } catch (final Exception ex) {
|
|
+ throw new IllegalStateException(ex);
|
|
+ }
|
|
+ }
|
|
+ */
|
|
+
|
|
+ public void acquireReadLock() {
|
|
+ this.regionLock.readLock();
|
|
+ }
|
|
+
|
|
+ public void releaseReadLock() {
|
|
+ this.regionLock.tryUnlockRead();
|
|
+ }
|
|
+
|
|
+ private void acquireWriteLock() {
|
|
+ final Thread currentThread = Thread.currentThread();
|
|
+ if (this.writeLockOwner == currentThread) {
|
|
+ throw new IllegalStateException("Cannot recursively operate in the regioniser");
|
|
+ }
|
|
+ this.regionLock.writeLock();
|
|
+ this.writeLockOwner = currentThread;
|
|
+ }
|
|
+
|
|
+ private void releaseWriteLock() {
|
|
+ this.writeLockOwner = null;
|
|
+ this.regionLock.tryUnlockWrite();
|
|
+ }
|
|
+
|
|
+ private void onRegionCreate(final ThreadedRegion<R, S> region) {
|
|
+ final ThreadedRegion<R, S> 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<R, S> region) {
|
|
+ final ThreadedRegion<R, S> 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<? super ThreadedRegion<R, S>> consumer) {
|
|
+ this.regionLock.readLock();
|
|
+ try {
|
|
+ this.regionsById.forEachValue(consumer);
|
|
+ } finally {
|
|
+ this.regionLock.tryUnlockRead();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void computeForAllRegionsUnsynchronised(final Consumer<? super ThreadedRegion<R, S>> consumer) {
|
|
+ this.regionsById.forEachValue(consumer);
|
|
+ }
|
|
+
|
|
+ public ThreadedRegion<R, S> 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<R, S> section = this.sections.get(sectionKey);
|
|
+
|
|
+ return section == null ? null : section.getRegion();
|
|
+ }
|
|
+
|
|
+ public ThreadedRegion<R, S> 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<R, S> optimisticSection = this.sections.get(sectionKey);
|
|
+ final ThreadedRegion<R, S> 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<R, S> 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.
|
|
+ *
|
|
+ * <p>
|
|
+ * Note that it is illegal to additionally call addChunk or removeChunk for the same
|
|
+ * region section in parallel.
|
|
+ * </p>
|
|
+ */
|
|
+ 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<R, S> 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<R, S> section = this.sections.get(sectionKey);
|
|
+
|
|
+ List<ThreadedRegionSection<R, S>> newSections = new ArrayList<>();
|
|
+
|
|
+ if (section == null) {
|
|
+ // no section at all
|
|
+ section = new ThreadedRegionSection<>(sectionX, sectionZ, this, chunkX, chunkZ);
|
|
+ this.sections.put(sectionKey, section);
|
|
+ newSections.add(section);
|
|
+ } else {
|
|
+ section.addChunk(chunkX, chunkZ);
|
|
+ }
|
|
+ // due to the fast check from above, we know the section is empty whether we needed to create it or not
|
|
+
|
|
+ // enforce the adjacency invariant by creating / updating neighbour sections
|
|
+ final int createRadius = this.emptySectionCreateRadius;
|
|
+ final int searchRadius = createRadius + this.regionSectionMergeRadius;
|
|
+ ReferenceOpenHashSet<ThreadedRegion<R, S>> 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<R, S> 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<R, S> 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<R, S> firstUnlockedRegion = null;
|
|
+
|
|
+ for (final ThreadedRegion<R, S> 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<R, S> region : nearbyRegions) {
|
|
+ if (region == regionOfInterest) {
|
|
+ continue;
|
|
+ }
|
|
+ // need the relaxed check, as the region may already be
|
|
+ // a merge target
|
|
+ if (!region.tryKill()) {
|
|
+ regionOfInterest.mergeIntoLater(region);
|
|
+ delayedTrueMerge = true;
|
|
+ } else {
|
|
+ region.mergeInto(regionOfInterest);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (delayedTrueMerge && firstUnlockedRegion != null) {
|
|
+ // we need to retire this region, as it can no longer tick
|
|
+ if (regionOfInterest.state == ThreadedRegion.STATE_READY) {
|
|
+ regionOfInterest.state = ThreadedRegion.STATE_TRANSIENT;
|
|
+ this.callbacks.onRegionInactive(regionOfInterest);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // need to set alive if we created it and we didn't delay a merge
|
|
+ regionOfInterestAlive = firstUnlockedRegion == null && !delayedTrueMerge && regionOfInterest.mergeIntoLater.isEmpty() && regionOfInterest.expectingMergeFrom.isEmpty();
|
|
+ }
|
|
+
|
|
+ if (regionOfInterestAlive) {
|
|
+ regionOfInterest.state = ThreadedRegion.STATE_READY;
|
|
+ if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) {
|
|
+ throw new IllegalStateException("Should not happen on region " + this);
|
|
+ }
|
|
+ this.callbacks.onRegionActive(regionOfInterest);
|
|
+ }
|
|
+ } catch (final Throwable throwable) {
|
|
+ LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable);
|
|
+ SneakyThrow.sneaky(throwable);
|
|
+ return; // unreachable
|
|
+ } finally {
|
|
+ this.releaseWriteLock();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void removeChunk(final int chunkX, final int chunkZ) {
|
|
+ final int sectionX = chunkX >> this.sectionChunkShift;
|
|
+ final int sectionZ = chunkZ >> this.sectionChunkShift;
|
|
+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
|
|
+
|
|
+ // Given that for each section, no addChunk/removeChunk can occur in parallel,
|
|
+ // we can avoid the lock IF the section exists AND it has a chunk count > 1
|
|
+ final ThreadedRegionSection<R, S> 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<R, S> neighbourSection = this.sections.get(neighbourKey);
|
|
+
|
|
+ // should be non-null here always
|
|
+ neighbourSection.decrementNonEmptyNeighbours();
|
|
+ }
|
|
+ }
|
|
+ } catch (final Throwable throwable) {
|
|
+ LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable);
|
|
+ SneakyThrow.sneaky(throwable);
|
|
+ return; // unreachable
|
|
+ } finally {
|
|
+ this.releaseWriteLock();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // must hold regionLock
|
|
+ private void onRegionRelease(final ThreadedRegion<R, S> 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<ThreadedRegion<R, S>> expectingMergeFrom = region.expectingMergeFrom.clone();
|
|
+ for (final ThreadedRegion<R, S> mergeFrom : expectingMergeFrom) {
|
|
+ if (!mergeFrom.tryKill()) {
|
|
+ throw new IllegalStateException("Merge from region " + mergeFrom + " should be killable! Trying to merge into " + region);
|
|
+ }
|
|
+ mergeFrom.mergeInto(region);
|
|
+ }
|
|
+
|
|
+ if (!region.expectingMergeFrom.isEmpty()) {
|
|
+ throw new IllegalStateException("Region " + region + " should no longer have merge requests after mering from " + expectingMergeFrom);
|
|
+ }
|
|
+
|
|
+ if (!region.mergeIntoLater.isEmpty()) {
|
|
+ // There is another nearby ticking region that we need to merge into
|
|
+ region.state = ThreadedRegion.STATE_TRANSIENT;
|
|
+ this.callbacks.onRegionInactive(region);
|
|
+ // return to avoid removing dead sections or splitting, these actions will be performed
|
|
+ // by the region we merge into
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // now check whether we need to recalculate regions
|
|
+ final boolean removeDeadSections = hasExpectingMerges || region.hasNoAliveSections()
|
|
+ || (region.sectionByKey.size() >= this.minSectionRecalcCount && region.getDeadSectionPercent() >= this.maxDeadRegionPercent);
|
|
+ final boolean removedDeadSections = removeDeadSections && !region.deadSections.isEmpty();
|
|
+ if (removeDeadSections) {
|
|
+ // kill dead sections
|
|
+ for (final ThreadedRegionSection<R, S> deadSection : region.deadSections) {
|
|
+ final long key = CoordinateUtils.getChunkKey(deadSection.sectionX, deadSection.sectionZ);
|
|
+
|
|
+ if (!deadSection.isEmpty()) {
|
|
+ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!");
|
|
+ }
|
|
+ if (deadSection.hasNonEmptyNeighbours()) {
|
|
+ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has non-empty neighbours!");
|
|
+ }
|
|
+ if (!region.sectionByKey.remove(key, deadSection)) {
|
|
+ throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection);
|
|
+ }
|
|
+ if (this.sections.remove(key) != deadSection) {
|
|
+ throw new IllegalStateException("Cannot remove dead section '" +
|
|
+ deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " + this.sections.get(key));
|
|
+ }
|
|
+ }
|
|
+ region.deadSections.clear();
|
|
+ }
|
|
+
|
|
+ // if we removed dead sections, we should check if the region can be split into smaller ones
|
|
+ // otherwise, the region remains alive
|
|
+ if (!removedDeadSections) {
|
|
+ // didn't remove dead sections, don't check for split
|
|
+ region.state = ThreadedRegion.STATE_READY;
|
|
+ if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) {
|
|
+ throw new IllegalStateException("Illegal state " + region);
|
|
+ }
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // first, we need to build copy of coordinate->section map of all sections in recalculate
|
|
+ final Long2ReferenceOpenHashMap<ThreadedRegionSection<R, S>> 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<List<ThreadedRegionSection<R, S>>> 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<ThreadedRegionSection<R, S>> currRegion = new ArrayList<>();
|
|
+ final Iterator<ThreadedRegionSection<R, S>> firstIterator = recalculateSections.values().iterator();
|
|
+
|
|
+ currRegion.add(firstIterator.next());
|
|
+ firstIterator.remove();
|
|
+ search_loop:
|
|
+ for (int idx = 0; idx < currRegion.size(); ++idx) {
|
|
+ final ThreadedRegionSection<R, S> 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<R, S> section = recalculateSections.remove(CoordinateUtils.getChunkKey(dx + centerX, dz + centerZ));
|
|
+ if (section == null) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ currRegion.add(section);
|
|
+
|
|
+ if (recalculateSections.isEmpty()) {
|
|
+ // no point in searching further
|
|
+ break search_loop;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ newRegions.add(currRegion);
|
|
+ }
|
|
+
|
|
+ // now we have split the regions into separate parts, we can split recalculate
|
|
+
|
|
+ if (newRegions.size() == 1) {
|
|
+ // no need to split anything, we're done here
|
|
+ region.state = ThreadedRegion.STATE_READY;
|
|
+ if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) {
|
|
+ throw new IllegalStateException("Illegal state " + region);
|
|
+ }
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // need to split the region, so we need to kill the old one first
|
|
+ region.state = ThreadedRegion.STATE_DEAD;
|
|
+ region.onRemove(true);
|
|
+
|
|
+ // create new regions
|
|
+ final Long2ReferenceOpenHashMap<ThreadedRegion<R, S>> newRegionsMap = new Long2ReferenceOpenHashMap<>();
|
|
+ final ReferenceOpenHashSet<ThreadedRegion<R, S>> newRegionsSet = new ReferenceOpenHashSet<>();
|
|
+
|
|
+ for (final List<ThreadedRegionSection<R, S>> sections : newRegions) {
|
|
+ final ThreadedRegion<R, S> newRegion = new ThreadedRegion<>(this);
|
|
+ newRegionsSet.add(newRegion);
|
|
+
|
|
+ for (final ThreadedRegionSection<R, S> section : sections) {
|
|
+ section.setRegionRelease(null);
|
|
+ newRegion.addSection(section);
|
|
+ final ThreadedRegion<R, S> 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<R, S> newRegion : newRegionsSet) {
|
|
+ newRegion.state = ThreadedRegion.STATE_READY;
|
|
+ if (!newRegion.expectingMergeFrom.isEmpty() || !newRegion.mergeIntoLater.isEmpty()) {
|
|
+ throw new IllegalStateException("Illegal state " + newRegion);
|
|
+ }
|
|
+ newRegion.onCreate();
|
|
+ this.callbacks.onRegionActive(newRegion);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static final class ThreadedRegion<R extends ThreadedRegionData<R, S>, S extends ThreadedRegionSectionData> {
|
|
+
|
|
+ private static final AtomicLong REGION_ID_GENERATOR = new AtomicLong();
|
|
+
|
|
+ private static final int STATE_TRANSIENT = 0;
|
|
+ private static final int STATE_READY = 1;
|
|
+ private static final int STATE_TICKING = 2;
|
|
+ private static final int STATE_DEAD = 3;
|
|
+
|
|
+ public final long id;
|
|
+
|
|
+ private int state;
|
|
+
|
|
+ private final Long2ReferenceOpenHashMap<ThreadedRegionSection<R, S>> sectionByKey = new Long2ReferenceOpenHashMap<>();
|
|
+ private final ReferenceOpenHashSet<ThreadedRegionSection<R, S>> deadSections = new ReferenceOpenHashSet<>();
|
|
+
|
|
+ public final ThreadedRegionizer<R, S> regioniser;
|
|
+
|
|
+ private final R data;
|
|
+
|
|
+ private final ReferenceOpenHashSet<ThreadedRegion<R, S>> mergeIntoLater = new ReferenceOpenHashSet<>();
|
|
+ private final ReferenceOpenHashSet<ThreadedRegion<R, S>> expectingMergeFrom = new ReferenceOpenHashSet<>();
|
|
+
|
|
+ public ThreadedRegion(final ThreadedRegionizer<R, S> regioniser) {
|
|
+ this.regioniser = regioniser;
|
|
+ this.id = REGION_ID_GENERATOR.getAndIncrement();
|
|
+ this.state = STATE_TRANSIENT;
|
|
+ this.data = regioniser.callbacks.createNewData(this);
|
|
+ }
|
|
+
|
|
+ public LongArrayList getOwnedSections() {
|
|
+ final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread();
|
|
+ if (lock) {
|
|
+ this.regioniser.regionLock.readLock();
|
|
+ }
|
|
+ try {
|
|
+ final LongArrayList ret = new LongArrayList(this.sectionByKey.size());
|
|
+ ret.addAll(this.sectionByKey.keySet());
|
|
+
|
|
+ return ret;
|
|
+ } finally {
|
|
+ if (lock) {
|
|
+ this.regioniser.regionLock.tryUnlockRead();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public 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<ThreadedRegion<R, S>> into, final ReferenceOpenHashSet<ThreadedRegion<R, S>> regions) {
|
|
+ if (this.data != null) {
|
|
+ this.data.split(this.regioniser, into, regions);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void mergeInto(final ThreadedRegion<R, S> 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<R, S> section : this.sectionByKey.values()) {
|
|
+ section.setRegionRelease(null);
|
|
+ mergeTarget.addSection(section);
|
|
+ }
|
|
+ for (final ThreadedRegionSection<R, S> 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<R, S> 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<R, S> 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<R, S> region) {
|
|
+ if (region.isDead()) {
|
|
+ throw new IllegalStateException("Trying to merge later into a dead region: " + region);
|
|
+ }
|
|
+ final boolean add1, add2;
|
|
+ if ((add1 = this.mergeIntoLater.add(region)) != (add2 = region.expectingMergeFrom.add(this))) {
|
|
+ throw new IllegalStateException("Inconsistent state between target merge " + region + " and this " + this + ": add1,add2:" + add1 + "," + add2);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private boolean tryKill() {
|
|
+ switch (this.state) {
|
|
+ case STATE_TRANSIENT: {
|
|
+ this.state = STATE_DEAD;
|
|
+ this.onRemove(false);
|
|
+ return true;
|
|
+ }
|
|
+ case STATE_READY: {
|
|
+ this.state = STATE_DEAD;
|
|
+ this.onRemove(true);
|
|
+ return true;
|
|
+ }
|
|
+ case STATE_TICKING: {
|
|
+ return false;
|
|
+ }
|
|
+ case STATE_DEAD: {
|
|
+ throw new IllegalStateException("Already dead");
|
|
+ }
|
|
+ default: {
|
|
+ throw new IllegalStateException("Unknown state: " + this.state);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private boolean isDead() {
|
|
+ return this.state == STATE_DEAD;
|
|
+ }
|
|
+
|
|
+ private boolean isTicking() {
|
|
+ return this.state == STATE_TICKING;
|
|
+ }
|
|
+
|
|
+ private void removeDeadSection(final ThreadedRegionSection<R, S> section) {
|
|
+ this.deadSections.remove(section);
|
|
+ }
|
|
+
|
|
+ private void addDeadSection(final ThreadedRegionSection<R, S> section) {
|
|
+ this.deadSections.add(section);
|
|
+ }
|
|
+
|
|
+ private void addSection(final ThreadedRegionSection<R, S> section) {
|
|
+ if (section.getRegionPlain() != null) {
|
|
+ throw new IllegalStateException("Section already has region");
|
|
+ }
|
|
+ if (this.sectionByKey.putIfAbsent(section.sectionKey, section) != null) {
|
|
+ throw new IllegalStateException("Already have section " + section + ", mapped to " + this.sectionByKey.get(section.sectionKey));
|
|
+ }
|
|
+ section.setRegionRelease(this);
|
|
+ }
|
|
+
|
|
+ public R getData() {
|
|
+ return this.data;
|
|
+ }
|
|
+
|
|
+ public boolean tryMarkTicking(final BooleanSupplier abort) {
|
|
+ this.regioniser.acquireWriteLock();
|
|
+ try {
|
|
+ if (this.state != STATE_READY || abort.getAsBoolean()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (!this.mergeIntoLater.isEmpty() || !this.expectingMergeFrom.isEmpty()) {
|
|
+ throw new IllegalStateException("Region " + this + " should not be ready");
|
|
+ }
|
|
+
|
|
+ this.state = STATE_TICKING;
|
|
+ return true;
|
|
+ } finally {
|
|
+ this.regioniser.releaseWriteLock();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public boolean markNotTicking() {
|
|
+ this.regioniser.acquireWriteLock();
|
|
+ try {
|
|
+ if (this.state != STATE_TICKING) {
|
|
+ throw new IllegalStateException("Attempting to release non-locked state");
|
|
+ }
|
|
+
|
|
+ this.regioniser.onRegionRelease(this);
|
|
+
|
|
+ return this.state == STATE_READY;
|
|
+ } catch (final Throwable throwable) {
|
|
+ LOGGER.error("Failed to acquire region " + this, throwable);
|
|
+ SneakyThrow.sneaky(throwable);
|
|
+ return false; // unreachable
|
|
+ } finally {
|
|
+ this.regioniser.releaseWriteLock();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ final StringBuilder ret = new StringBuilder(128);
|
|
+
|
|
+ ret.append("ThreadedRegion{");
|
|
+ ret.append("state=").append(this.state).append(',');
|
|
+ // To avoid recursion in toString, maybe fix later?
|
|
+ //ret.append("mergeIntoLater=").append(this.mergeIntoLater).append(',');
|
|
+ //ret.append("expectingMergeFrom=").append(this.expectingMergeFrom).append(',');
|
|
+
|
|
+ ret.append("sectionCount=").append(this.sectionByKey.size()).append(',');
|
|
+ ret.append("sections=[");
|
|
+ for (final Iterator<ThreadedRegionSection<R, S>> iterator = this.sectionByKey.values().iterator(); iterator.hasNext();) {
|
|
+ final ThreadedRegionSection<R, S> section = iterator.next();
|
|
+
|
|
+ ret.append(section.toString());
|
|
+ if (iterator.hasNext()) {
|
|
+ ret.append(',');
|
|
+ }
|
|
+ }
|
|
+ ret.append(']');
|
|
+
|
|
+ ret.append('}');
|
|
+ return ret.toString();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static final class ThreadedRegionSection<R extends ThreadedRegionData<R, S>, 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<R, S> region;
|
|
+ private static final VarHandle REGION_HANDLE = ConcurrentUtil.getVarHandle(ThreadedRegionSection.class, "region", ThreadedRegion.class);
|
|
+
|
|
+ public final ThreadedRegionizer<R, S> regioniser;
|
|
+
|
|
+ private final int regionChunkShift;
|
|
+ private final int regionChunkMask;
|
|
+
|
|
+ private final S data;
|
|
+
|
|
+ private ThreadedRegion<R, S> getRegionPlain() {
|
|
+ return (ThreadedRegion<R, S>)REGION_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ private ThreadedRegion<R, S> getRegionAcquire() {
|
|
+ return (ThreadedRegion<R, S>)REGION_HANDLE.getAcquire(this);
|
|
+ }
|
|
+
|
|
+ private void setRegionRelease(final ThreadedRegion<R, S> value) {
|
|
+ REGION_HANDLE.setRelease(this, value);
|
|
+ }
|
|
+
|
|
+ // creates an empty section with zero non-empty neighbours
|
|
+ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer<R, S> regioniser) {
|
|
+ this.sectionX = sectionX;
|
|
+ this.sectionZ = sectionZ;
|
|
+ this.sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
|
|
+ this.chunksBitset = new long[Math.max(1, regioniser.regionSectionChunkSize * regioniser.regionSectionChunkSize / Long.SIZE)];
|
|
+ this.regioniser = regioniser;
|
|
+ this.regionChunkShift = regioniser.sectionChunkShift;
|
|
+ this.regionChunkMask = regioniser.regionSectionChunkSize - 1;
|
|
+ this.data = regioniser.callbacks
|
|
+ .createNewSectionData(sectionX, sectionZ, this.regionChunkShift);
|
|
+ }
|
|
+
|
|
+ // creates a section with an initial chunk with zero non-empty neighbours
|
|
+ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer<R, S> regioniser,
|
|
+ final int chunkXInit, final int chunkZInit) {
|
|
+ this(sectionX, sectionZ, regioniser);
|
|
+
|
|
+ final int initIndex = this.getChunkIndex(chunkXInit, chunkZInit);
|
|
+ this.chunkCount = 1;
|
|
+ this.chunksBitset[initIndex >>> 6] = 1L << (initIndex & (Long.SIZE - 1)); // index / Long.SIZE
|
|
+ }
|
|
+
|
|
+ // creates an empty section with the specified number of non-empty neighbours
|
|
+ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer<R, S> 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<R, S> 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<R extends ThreadedRegionData<R, S>, S extends ThreadedRegionSectionData> {
|
|
+
|
|
+ /**
|
|
+ * Splits this region data into the specified regions set.
|
|
+ * <p>
|
|
+ * <b>Note:</b>
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ * @param regioniser Regioniser for which the regions reside in.
|
|
+ * @param into A map of region section coordinate key to the region that owns the section.
|
|
+ * @param regions The set of regions to split into.
|
|
+ */
|
|
+ public void split(final ThreadedRegionizer<R, S> regioniser, final Long2ReferenceOpenHashMap<ThreadedRegion<R, S>> into,
|
|
+ final ReferenceOpenHashSet<ThreadedRegion<R, S>> 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.
|
|
+ * <p>
|
|
+ * <b>Note:</b>
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ * @param into Specified region.
|
|
+ */
|
|
+ public void mergeInto(final ThreadedRegion<R, S> into);
|
|
+ }
|
|
+
|
|
+ public static interface ThreadedRegionSectionData {}
|
|
+
|
|
+ public static interface RegionCallbacks<R extends ThreadedRegionData<R, S>, S extends ThreadedRegionSectionData> {
|
|
+
|
|
+ /**
|
|
+ * Creates new section data for the specified section x and section z.
|
|
+ * <p>
|
|
+ * <b>Note:</b>
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ * @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.
|
|
+ * <p>
|
|
+ * <b>Note:</b>
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ * @param forRegion The region to create the data for.
|
|
+ * @return New region data, may be {@code null}.
|
|
+ */
|
|
+ public R createNewData(final ThreadedRegion<R, S> 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.
|
|
+ * <p>
|
|
+ * <b>Note:</b>
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ * @param region The region that was created.
|
|
+ */
|
|
+ public void onRegionCreate(final ThreadedRegion<R, S> 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.
|
|
+ * <p>
|
|
+ * <b>Note:</b>
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ * @param region The region that is about to be destroyed.
|
|
+ */
|
|
+ public void onRegionDestroy(final ThreadedRegion<R, S> 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.
|
|
+ * <p>
|
|
+ * <b>Note:</b>
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ * @param region The region that is now active.
|
|
+ */
|
|
+ public void onRegionActive(final ThreadedRegion<R, S> 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.
|
|
+ * <p>
|
|
+ * <b>Note:</b>
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * 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.
|
|
+ * </p>
|
|
+ * @param region The region that is now inactive.
|
|
+ */
|
|
+ public void onRegionInactive(final ThreadedRegion<R, S> 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<TickRegionScheduler.TickTime> 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<TickRegionScheduler.TickTime> 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<TickInformation> collapsedData = new ArrayList<>();
|
|
+ for (int i = 0, len = allData.size(); i < len; ++i) {
|
|
+ final List<TickRegionScheduler.TickTime> 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..3f14eda409ca49095dc43796868435b705c36ce4
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java
|
|
@@ -0,0 +1,559 @@
|
|
+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());
|
|
+ ret.setUncaughtExceptionHandler(TickRegionScheduler.this::uncaughtException);
|
|
+ return ret;
|
|
+ }
|
|
+ });
|
|
+ }
|
|
+
|
|
+ public int getTotalThreadCount() {
|
|
+ return this.scheduler.getThreads().length;
|
|
+ }
|
|
+
|
|
+ private static void setTickingRegion(final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region) {
|
|
+ final Thread currThread = Thread.currentThread();
|
|
+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
|
|
+ throw new IllegalStateException("Must be tick thread runner");
|
|
+ }
|
|
+ if (region != null && tickThreadRunner.currentTickingRegion != null) {
|
|
+ throw new IllegalStateException("Trying to double set ticking region!");
|
|
+ }
|
|
+ if (region == null && tickThreadRunner.currentTickingRegion == null) {
|
|
+ throw new IllegalStateException("Trying to double unset ticking region!");
|
|
+ }
|
|
+ tickThreadRunner.currentTickingRegion = region;
|
|
+ if (region != null) {
|
|
+ tickThreadRunner.currentTickingWorldRegionizedData = region.regioniser.world.worldRegionData.get();
|
|
+ } else {
|
|
+ tickThreadRunner.currentTickingWorldRegionizedData = null;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private static void setTickTask(final SchedulerThreadPool.SchedulableTick task) {
|
|
+ final Thread currThread = Thread.currentThread();
|
|
+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
|
|
+ throw new IllegalStateException("Must be tick thread runner");
|
|
+ }
|
|
+ if (task != null && tickThreadRunner.currentTickingTask != null) {
|
|
+ throw new IllegalStateException("Trying to double set ticking task!");
|
|
+ }
|
|
+ if (task == null && tickThreadRunner.currentTickingTask == null) {
|
|
+ throw new IllegalStateException("Trying to double unset ticking task!");
|
|
+ }
|
|
+ tickThreadRunner.currentTickingTask = task;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the current ticking region, or {@code null} if there is no ticking region.
|
|
+ * If this thread is not a TickThread, then returns {@code null}.
|
|
+ */
|
|
+ public static ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> getCurrentRegion() {
|
|
+ final Thread currThread = Thread.currentThread();
|
|
+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
|
|
+ return RegionShutdownThread.getRegion();
|
|
+ }
|
|
+ return tickThreadRunner.currentTickingRegion;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the current ticking region's world regionised data, or {@code null} if there is no ticking region.
|
|
+ * This is a faster alternative to calling the {@link RegionizedData#get()} method.
|
|
+ * If this thread is not a TickThread, then returns {@code null}.
|
|
+ */
|
|
+ public static RegionizedWorldData getCurrentRegionizedWorldData() {
|
|
+ final Thread currThread = Thread.currentThread();
|
|
+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
|
|
+ return RegionShutdownThread.getWorldData();
|
|
+ }
|
|
+ return tickThreadRunner.currentTickingWorldRegionizedData;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the current ticking task, or {@code null} if there is no ticking region.
|
|
+ * If this thread is not a TickThread, then returns {@code null}.
|
|
+ */
|
|
+ public static SchedulerThreadPool.SchedulableTick getCurrentTickingTask() {
|
|
+ final Thread currThread = Thread.currentThread();
|
|
+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
|
|
+ return null;
|
|
+ }
|
|
+ return tickThreadRunner.currentTickingTask;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Schedules the given region
|
|
+ * @throws IllegalStateException If the region is already scheduled or is ticking
|
|
+ */
|
|
+ public void scheduleRegion(final RegionScheduleHandle region) {
|
|
+ region.scheduler = this;
|
|
+ this.scheduler.schedule(region);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Attempts to de-schedule the provided region. If the current region cannot be cancelled for its next tick or task
|
|
+ * execution, then it will be cancelled after.
|
|
+ */
|
|
+ public void descheduleRegion(final RegionScheduleHandle region) {
|
|
+ // To avoid acquiring any of the locks the scheduler may be using, we
|
|
+ // simply cancel the next action.
|
|
+ region.markNonSchedulable();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Updates the tick start to the farthest into the future of its current scheduled time and the
|
|
+ * provided time.
|
|
+ * @return {@code false} if the region was not scheduled or is currently ticking or the specified time is less-than its
|
|
+ * current start time, {@code true} if the next tick start was adjusted.
|
|
+ */
|
|
+ public boolean updateTickStartToMax(final RegionScheduleHandle region, final long newStart) {
|
|
+ return this.scheduler.updateTickStartToMax(region, newStart);
|
|
+ }
|
|
+
|
|
+ public boolean halt(final boolean sync, final long maxWaitNS) {
|
|
+ return this.scheduler.halt(sync, maxWaitNS);
|
|
+ }
|
|
+
|
|
+ public void setHasTasks(final RegionScheduleHandle region) {
|
|
+ this.scheduler.notifyTasks(region);
|
|
+ }
|
|
+
|
|
+ public void init() {
|
|
+ this.scheduler.start();
|
|
+ }
|
|
+
|
|
+ private void uncaughtException(final Thread thread, final Throwable thr) {
|
|
+ LOGGER.error("Uncaught exception in tick thread \"" + thread.getName() + "\"", thr);
|
|
+
|
|
+ // prevent further ticks from occurring
|
|
+ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD
|
|
+ this.scheduler.halt(false, 0L);
|
|
+
|
|
+ MinecraftServer.getServer().stopServer();
|
|
+ }
|
|
+
|
|
+ private void regionFailed(final RegionScheduleHandle handle, final boolean executingTasks, final Throwable thr) {
|
|
+ // when a region fails, we need to shut down the server gracefully
|
|
+
|
|
+ // prevent further ticks from occurring
|
|
+ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD
|
|
+ this.scheduler.halt(false, 0L);
|
|
+
|
|
+ final ChunkPos center = handle.region == null ? null : handle.region.region.getCenterChunk();
|
|
+
|
|
+ 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 ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> currentTickingRegion;
|
|
+ private RegionizedWorldData currentTickingWorldRegionizedData;
|
|
+ private SchedulerThreadPool.SchedulableTick currentTickingTask;
|
|
+
|
|
+ public TickThreadRunner(final Runnable run, final String name) {
|
|
+ super(run, name);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static abstract class RegionScheduleHandle extends SchedulerThreadPool.SchedulableTick {
|
|
+
|
|
+ protected long currentTick;
|
|
+ protected long lastTickStart;
|
|
+
|
|
+ protected final TickData tickTimes5s;
|
|
+ protected final TickData tickTimes15s;
|
|
+ protected final TickData tickTimes1m;
|
|
+ protected final TickData tickTimes5m;
|
|
+ protected final TickData tickTimes15m;
|
|
+ protected TickTime currentTickData;
|
|
+ protected Thread currentTickingThread;
|
|
+
|
|
+ public final TickRegions.TickRegionData region;
|
|
+ private final AtomicBoolean cancelled = new AtomicBoolean();
|
|
+
|
|
+ protected final Schedule tickSchedule;
|
|
+
|
|
+ private TickRegionScheduler scheduler;
|
|
+
|
|
+ public RegionScheduleHandle(final TickRegions.TickRegionData region, final long firstStart) {
|
|
+ this.currentTick = 0L;
|
|
+ this.lastTickStart = SchedulerThreadPool.DEADLINE_NOT_SET;
|
|
+ this.tickTimes5s = new TickData(TimeUnit.SECONDS.toNanos(5L));
|
|
+ this.tickTimes15s = new TickData(TimeUnit.SECONDS.toNanos(15L));
|
|
+ this.tickTimes1m = new TickData(TimeUnit.MINUTES.toNanos(1L));
|
|
+ this.tickTimes5m = new TickData(TimeUnit.MINUTES.toNanos(5L));
|
|
+ this.tickTimes15m = new TickData(TimeUnit.MINUTES.toNanos(15L));
|
|
+ this.region = region;
|
|
+
|
|
+ this.setScheduledStart(firstStart);
|
|
+ this.tickSchedule = new Schedule(firstStart == SchedulerThreadPool.DEADLINE_NOT_SET ? firstStart : firstStart - TIME_BETWEEN_TICKS);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Subclasses should call this instead of {@link ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool.SchedulableTick#setScheduledStart(long)}
|
|
+ * so that the tick schedule and scheduled start remain synchronised
|
|
+ */
|
|
+ protected final void updateScheduledStart(final long to) {
|
|
+ this.setScheduledStart(to);
|
|
+ this.tickSchedule.setLastPeriod(to == SchedulerThreadPool.DEADLINE_NOT_SET ? to : to - TIME_BETWEEN_TICKS);
|
|
+ }
|
|
+
|
|
+ public final void markNonSchedulable() {
|
|
+ this.cancelled.set(true);
|
|
+ }
|
|
+
|
|
+ public final boolean isMarkedAsNonSchedulable() {
|
|
+ return this.cancelled.get();
|
|
+ }
|
|
+
|
|
+ protected abstract boolean tryMarkTicking();
|
|
+
|
|
+ protected abstract boolean markNotTicking();
|
|
+
|
|
+ protected abstract void tickRegion(final int tickCount, final long startTime, final long scheduledEnd);
|
|
+
|
|
+ protected abstract boolean runRegionTasks(final BooleanSupplier canContinue);
|
|
+
|
|
+ protected abstract boolean hasIntermediateTasks();
|
|
+
|
|
+ @Override
|
|
+ public final boolean hasTasks() {
|
|
+ return this.hasIntermediateTasks();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final Boolean runTasks(final BooleanSupplier canContinue) {
|
|
+ if (this.cancelled.get()) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
|
|
+ final long tickStart = System.nanoTime();
|
|
+
|
|
+ if (!this.tryMarkTicking()) {
|
|
+ if (!this.cancelled.get()) {
|
|
+ throw new IllegalStateException("Scheduled region should be acquirable");
|
|
+ }
|
|
+ // region was killed
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ TickRegionScheduler.setTickTask(this);
|
|
+ if (this.region != null) {
|
|
+ TickRegionScheduler.setTickingRegion(this.region.region);
|
|
+ }
|
|
+
|
|
+ synchronized (this) {
|
|
+ this.currentTickData = new TickTime(
|
|
+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, tickStart, cpuStart,
|
|
+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME,
|
|
+ false
|
|
+ );
|
|
+ this.currentTickingThread = Thread.currentThread();
|
|
+ }
|
|
+
|
|
+ final boolean ret;
|
|
+ try {
|
|
+ ret = this.runRegionTasks(() -> {
|
|
+ return !RegionScheduleHandle.this.cancelled.get() && canContinue.getAsBoolean();
|
|
+ });
|
|
+ } catch (final Throwable thr) {
|
|
+ this.scheduler.regionFailed(this, true, thr);
|
|
+ if (thr instanceof ThreadDeath) {
|
|
+ throw (ThreadDeath)thr;
|
|
+ }
|
|
+ // don't release region for another tick
|
|
+ return null;
|
|
+ } finally {
|
|
+ TickRegionScheduler.setTickTask(null);
|
|
+ if (this.region != null) {
|
|
+ TickRegionScheduler.setTickingRegion(null);
|
|
+ }
|
|
+ final long tickEnd = System.nanoTime();
|
|
+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
|
|
+
|
|
+ final TickTime time = new TickTime(
|
|
+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET,
|
|
+ tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, false
|
|
+ );
|
|
+
|
|
+ this.addTickTime(time);
|
|
+ }
|
|
+
|
|
+ return !this.markNotTicking() || this.cancelled.get() ? null : Boolean.valueOf(ret);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final boolean runTick() {
|
|
+ // Remember, we are supposed use setScheduledStart if we return true here, otherwise
|
|
+ // the scheduler will try to schedule for the same time.
|
|
+ if (this.cancelled.get()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
|
|
+ final long tickStart = System.nanoTime();
|
|
+
|
|
+ // use max(), don't assume that tickStart >= scheduledStart
|
|
+ final int tickCount = Math.max(1, this.tickSchedule.getPeriodsAhead(TIME_BETWEEN_TICKS, tickStart));
|
|
+
|
|
+ if (!this.tryMarkTicking()) {
|
|
+ if (!this.cancelled.get()) {
|
|
+ throw new IllegalStateException("Scheduled region should be acquirable");
|
|
+ }
|
|
+ // region was killed
|
|
+ return false;
|
|
+ }
|
|
+ if (this.cancelled.get()) {
|
|
+ this.markNotTicking();
|
|
+ // region should be killed
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ TickRegionScheduler.setTickTask(this);
|
|
+ if (this.region != null) {
|
|
+ TickRegionScheduler.setTickingRegion(this.region.region);
|
|
+ }
|
|
+ this.incrementTickCount();
|
|
+ final long lastTickStart = this.lastTickStart;
|
|
+ this.lastTickStart = tickStart;
|
|
+
|
|
+ final long scheduledStart = this.getScheduledStart();
|
|
+ final long scheduledEnd = scheduledStart + TIME_BETWEEN_TICKS;
|
|
+
|
|
+ synchronized (this) {
|
|
+ this.currentTickData = new TickTime(
|
|
+ lastTickStart, scheduledStart, tickStart, cpuStart,
|
|
+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME,
|
|
+ true
|
|
+ );
|
|
+ this.currentTickingThread = Thread.currentThread();
|
|
+ }
|
|
+
|
|
+ try {
|
|
+ // next start isn't updated until the end of this tick
|
|
+ this.tickRegion(tickCount, tickStart, scheduledEnd);
|
|
+ } catch (final Throwable thr) {
|
|
+ this.scheduler.regionFailed(this, false, thr);
|
|
+ if (thr instanceof ThreadDeath) {
|
|
+ throw (ThreadDeath)thr;
|
|
+ }
|
|
+ // regionFailed will schedule a shutdown, so we should avoid letting this region tick further
|
|
+ return false;
|
|
+ } finally {
|
|
+ TickRegionScheduler.setTickTask(null);
|
|
+ if (this.region != null) {
|
|
+ TickRegionScheduler.setTickingRegion(null);
|
|
+ }
|
|
+ final long tickEnd = System.nanoTime();
|
|
+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
|
|
+
|
|
+ // in order to ensure all regions get their chance at scheduling, we have to ensure that regions
|
|
+ // that exceed the max tick time are not always prioritised over everything else. Thus, we use the greatest
|
|
+ // of the current time and "ideal" next tick start.
|
|
+ this.tickSchedule.advanceBy(tickCount, TIME_BETWEEN_TICKS);
|
|
+ this.setScheduledStart(TimeUtil.getGreatestTime(tickEnd, this.tickSchedule.getDeadline(TIME_BETWEEN_TICKS)));
|
|
+
|
|
+ final TickTime time = new TickTime(
|
|
+ lastTickStart, scheduledStart, tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, true
|
|
+ );
|
|
+
|
|
+ this.addTickTime(time);
|
|
+ }
|
|
+
|
|
+ // Only AFTER updating the tickStart
|
|
+ return this.markNotTicking() && !this.cancelled.get();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Only safe to call if this tick data matches the current ticking region.
|
|
+ */
|
|
+ private void addTickTime(final TickTime time) {
|
|
+ synchronized (this) {
|
|
+ this.currentTickData = null;
|
|
+ this.currentTickingThread = null;
|
|
+ this.tickTimes5s.addDataFrom(time);
|
|
+ this.tickTimes15s.addDataFrom(time);
|
|
+ this.tickTimes1m.addDataFrom(time);
|
|
+ this.tickTimes5m.addDataFrom(time);
|
|
+ this.tickTimes15m.addDataFrom(time);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private TickTime adjustCurrentTickData(final long tickEnd) {
|
|
+ final TickTime currentTickData = this.currentTickData;
|
|
+ if (currentTickData == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getThreadCpuTime(this.currentTickingThread.getId()) : 0L;
|
|
+
|
|
+ return new TickTime(
|
|
+ currentTickData.previousTickStart(), currentTickData.scheduledTickStart(),
|
|
+ currentTickData.tickStart(), currentTickData.tickStartCPU(),
|
|
+ tickEnd, cpuEnd,
|
|
+ MEASURE_CPU_TIME, currentTickData.isTickExecution()
|
|
+ );
|
|
+ }
|
|
+
|
|
+ public final TickData.TickReportData getTickReport5s(final long currTime) {
|
|
+ synchronized (this) {
|
|
+ return this.tickTimes5s.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public final TickData.TickReportData getTickReport15s(final long currTime) {
|
|
+ synchronized (this) {
|
|
+ return this.tickTimes15s.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public final TickData.TickReportData getTickReport1m(final long currTime) {
|
|
+ synchronized (this) {
|
|
+ return this.tickTimes1m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public final TickData.TickReportData getTickReport5m(final long currTime) {
|
|
+ synchronized (this) {
|
|
+ return this.tickTimes5m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public final TickData.TickReportData getTickReport15m(final long currTime) {
|
|
+ synchronized (this) {
|
|
+ return this.tickTimes15m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Only safe to call if this tick data matches the current ticking region.
|
|
+ */
|
|
+ private void incrementTickCount() {
|
|
+ ++this.currentTick;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Only safe to call if this tick data matches the current ticking region.
|
|
+ */
|
|
+ public final long getCurrentTick() {
|
|
+ return this.currentTick;
|
|
+ }
|
|
+
|
|
+ protected final void setCurrentTick(final long value) {
|
|
+ this.currentTick = value;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // All time units are in nanoseconds.
|
|
+ public static final record TickTime(
|
|
+ long previousTickStart,
|
|
+ long scheduledTickStart,
|
|
+ long tickStart,
|
|
+ long tickStartCPU,
|
|
+ long tickEnd,
|
|
+ long tickEndCPU,
|
|
+ boolean supportCPUTime,
|
|
+ boolean isTickExecution
|
|
+ ) {
|
|
+ /**
|
|
+ * The difference between the start tick time and the scheduled start tick time. This value is
|
|
+ * < 0 if the tick started before the scheduled tick time.
|
|
+ * Only valid when {@link #isTickExecution()} is {@code true}.
|
|
+ */
|
|
+ public final long startOvershoot() {
|
|
+ return this.tickStart - this.scheduledTickStart;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * The difference from the end tick time and the start tick time. Always >= 0 (unless nanoTime is just wrong).
|
|
+ */
|
|
+ public final long tickLength() {
|
|
+ return this.tickEnd - this.tickStart;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * The total CPU time from the start tick time to the end tick time. Generally should be equal to the tickLength,
|
|
+ * unless there is CPU starvation or the tick thread was blocked by I/O or other tasks. Returns Long.MIN_VALUE
|
|
+ * if CPU time measurement is not supported.
|
|
+ */
|
|
+ public final long tickCpuTime() {
|
|
+ if (!this.supportCPUTime()) {
|
|
+ return Long.MIN_VALUE;
|
|
+ }
|
|
+ return this.tickEndCPU - this.tickStartCPU;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * The difference in time from the start of the last tick to the start of the current tick. If there is no
|
|
+ * last tick, then this value is max(TIME_BETWEEN_TICKS, tickLength).
|
|
+ * Only valid when {@link #isTickExecution()} is {@code true}.
|
|
+ */
|
|
+ public final long differenceFromLastTick() {
|
|
+ if (this.hasLastTick()) {
|
|
+ return this.tickStart - this.previousTickStart;
|
|
+ }
|
|
+ return Math.max(TIME_BETWEEN_TICKS, this.tickLength());
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns whether there was a tick that occurred before this one.
|
|
+ * Only valid when {@link #isTickExecution()} is {@code true}.
|
|
+ */
|
|
+ public boolean hasLastTick() {
|
|
+ return this.previousTickStart != SchedulerThreadPool.DEADLINE_NOT_SET;
|
|
+ }
|
|
+
|
|
+ /*
|
|
+ * Remember, this is the expected behavior of the following:
|
|
+ *
|
|
+ * MSPT: Time per tick. This does not include overshoot time, just the tickLength().
|
|
+ *
|
|
+ * TPS: The number of ticks per second. It should be ticks / (sum of differenceFromLastTick).
|
|
+ */
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..1ac3c3612df4c1c5c28efc4e23ea107aa722050e
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
|
|
@@ -0,0 +1,355 @@
|
|
+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 ThreadedRegionizer.RegionCallbacks<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> {
|
|
+
|
|
+ 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 ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region) {
|
|
+ return new TickRegionData(region);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public TickRegionSectionData createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void onRegionCreate(final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region) {
|
|
+ // nothing for now
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void onRegionDestroy(final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region) {
|
|
+ // nothing for now
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void onRegionActive(final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region) {
|
|
+ final TickRegionData data = region.getData();
|
|
+
|
|
+ data.tickHandle.checkInitialSchedule();
|
|
+ scheduler.scheduleRegion(data.tickHandle);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void onRegionInactive(final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region) {
|
|
+ final TickRegionData data = region.getData();
|
|
+
|
|
+ scheduler.descheduleRegion(data.tickHandle);
|
|
+ // old handle cannot be scheduled anymore, copy to a new handle
|
|
+ data.tickHandle = data.tickHandle.copy();
|
|
+ }
|
|
+
|
|
+ public static final class TickRegionSectionData implements ThreadedRegionizer.ThreadedRegionSectionData {}
|
|
+
|
|
+ public static final class TickRegionData implements ThreadedRegionizer.ThreadedRegionData<TickRegionData, TickRegionSectionData> {
|
|
+
|
|
+ private static final AtomicLong ID_GENERATOR = new AtomicLong();
|
|
+ /** Never 0L, since 0L is reserved for global region. */
|
|
+ public final long id = ID_GENERATOR.incrementAndGet();
|
|
+
|
|
+ public final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region;
|
|
+ public final ServerLevel world;
|
|
+
|
|
+ // generic regionised data
|
|
+ private final Reference2ReferenceOpenHashMap<RegionizedData<?>, Object> regionizedData = new Reference2ReferenceOpenHashMap<>();
|
|
+
|
|
+ // tick data
|
|
+ private ConcreteRegionTickHandle tickHandle = new ConcreteRegionTickHandle(this, SchedulerThreadPool.DEADLINE_NOT_SET);
|
|
+
|
|
+ // queue data
|
|
+ private final RegionizedTaskQueue.RegionTaskQueueData taskQueueData;
|
|
+
|
|
+ // chunk holder manager data
|
|
+ private final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = new ChunkHolderManager.HolderManagerRegionData();
|
|
+
|
|
+ private TickRegionData(final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region) {
|
|
+ this.region = region;
|
|
+ this.world = region.regioniser.world;
|
|
+ this.taskQueueData = new RegionizedTaskQueue.RegionTaskQueueData(this.world.taskQueueRegionData);
|
|
+ }
|
|
+
|
|
+ public RegionizedTaskQueue.RegionTaskQueueData getTaskQueueData() {
|
|
+ return this.taskQueueData;
|
|
+ }
|
|
+
|
|
+ // the value returned can be invalidated at any time, except when the caller
|
|
+ // is ticking this region
|
|
+ public TickRegionScheduler.RegionScheduleHandle getRegionSchedulingHandle() {
|
|
+ return this.tickHandle;
|
|
+ }
|
|
+
|
|
+ public long getCurrentTick() {
|
|
+ return this.tickHandle.getCurrentTick();
|
|
+ }
|
|
+
|
|
+ public ChunkHolderManager.HolderManagerRegionData getHolderManagerRegionData() {
|
|
+ return this.holderManagerRegionData;
|
|
+ }
|
|
+
|
|
+ <T> T getOrCreateRegionizedData(final RegionizedData<T> regionizedData) {
|
|
+ T ret = (T)this.regionizedData.get(regionizedData);
|
|
+
|
|
+ if (ret != null) {
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ ret = regionizedData.createNewValue();
|
|
+ this.regionizedData.put(regionizedData, ret);
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void split(final ThreadedRegionizer<TickRegionData, TickRegionSectionData> regioniser,
|
|
+ final Long2ReferenceOpenHashMap<ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData>> into,
|
|
+ final ReferenceOpenHashSet<ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData>> regions) {
|
|
+ final int shift = regioniser.sectionChunkShift;
|
|
+
|
|
+ // tick data
|
|
+ // note: here it is OK force us to access tick handle, as this region is owned (and thus not scheduled),
|
|
+ // and the other regions to split into are not scheduled yet.
|
|
+ for (final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region : regions) {
|
|
+ final TickRegionData data = region.getData();
|
|
+ data.tickHandle.copyDeadlineAndTickCount(this.tickHandle);
|
|
+ }
|
|
+
|
|
+ // generic regionised data
|
|
+ for (final Iterator<Reference2ReferenceMap.Entry<RegionizedData<?>, Object>> dataIterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator();
|
|
+ dataIterator.hasNext();) {
|
|
+ final Reference2ReferenceMap.Entry<RegionizedData<?>, Object> regionDataEntry = dataIterator.next();
|
|
+ final RegionizedData<?> data = regionDataEntry.getKey();
|
|
+ final Object from = regionDataEntry.getValue();
|
|
+
|
|
+ final ReferenceOpenHashSet<Object> dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f);
|
|
+
|
|
+ for (final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region : regions) {
|
|
+ dataSet.add(region.getData().getOrCreateRegionizedData(data));
|
|
+ }
|
|
+
|
|
+ final Long2ReferenceOpenHashMap<Object> regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f);
|
|
+
|
|
+ for (final Iterator<Long2ReferenceMap.Entry<ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData>>> regionIterator = into.long2ReferenceEntrySet().fastIterator();
|
|
+ regionIterator.hasNext();) {
|
|
+ final Long2ReferenceMap.Entry<ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData>> entry = regionIterator.next();
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region = entry.getValue();
|
|
+ final Object to = region.getData().getOrCreateRegionizedData(data);
|
|
+
|
|
+ regionToData.put(entry.getLongKey(), to);
|
|
+ }
|
|
+
|
|
+ ((RegionizedData<Object>)data).getCallback().split(from, shift, regionToData, dataSet);
|
|
+ }
|
|
+
|
|
+ // chunk holder manager data
|
|
+ {
|
|
+ final ReferenceOpenHashSet<ChunkHolderManager.HolderManagerRegionData> dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f);
|
|
+
|
|
+ for (final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region : regions) {
|
|
+ dataSet.add(region.getData().holderManagerRegionData);
|
|
+ }
|
|
+
|
|
+ final Long2ReferenceOpenHashMap<ChunkHolderManager.HolderManagerRegionData> regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f);
|
|
+
|
|
+ for (final Iterator<Long2ReferenceMap.Entry<ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData>>> regionIterator = into.long2ReferenceEntrySet().fastIterator();
|
|
+ regionIterator.hasNext();) {
|
|
+ final Long2ReferenceMap.Entry<ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData>> entry = regionIterator.next();
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region = entry.getValue();
|
|
+ final ChunkHolderManager.HolderManagerRegionData to = region.getData().holderManagerRegionData;
|
|
+
|
|
+ regionToData.put(entry.getLongKey(), to);
|
|
+ }
|
|
+
|
|
+ this.holderManagerRegionData.split(shift, regionToData, dataSet);
|
|
+ }
|
|
+
|
|
+ // task queue
|
|
+ this.taskQueueData.split(regioniser, into);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void mergeInto(final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> 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<Reference2ReferenceMap.Entry<RegionizedData<?>, Object>> iterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator();
|
|
+ iterator.hasNext();) {
|
|
+ final Reference2ReferenceMap.Entry<RegionizedData<?>, Object> entry = iterator.next();
|
|
+ final RegionizedData<?> regionizedData = entry.getKey();
|
|
+ final Object from = entry.getValue();
|
|
+ final Object to = into.getData().getOrCreateRegionizedData(regionizedData);
|
|
+
|
|
+ ((RegionizedData<Object>)regionizedData).getCallback().merge(from, to, fromTickOffset);
|
|
+ }
|
|
+
|
|
+ // chunk holder manager data
|
|
+ this.holderManagerRegionData.merge(into.getData().holderManagerRegionData, fromTickOffset);
|
|
+
|
|
+ // task queue
|
|
+ this.taskQueueData.mergeInto(data.taskQueueData);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private static final class ConcreteRegionTickHandle extends TickRegionScheduler.RegionScheduleHandle {
|
|
+
|
|
+ private final TickRegionData region;
|
|
+
|
|
+ private ConcreteRegionTickHandle(final TickRegionData region, final long start) {
|
|
+ super(region, start);
|
|
+ this.region = region;
|
|
+ }
|
|
+
|
|
+ private ConcreteRegionTickHandle copy() {
|
|
+ final ConcreteRegionTickHandle ret = new ConcreteRegionTickHandle(this.region, this.getScheduledStart());
|
|
+
|
|
+ ret.currentTick = this.currentTick;
|
|
+ ret.lastTickStart = this.lastTickStart;
|
|
+ ret.tickSchedule.setLastPeriod(this.tickSchedule.getLastPeriod());
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ private void updateSchedulingToMax(final ConcreteRegionTickHandle from) {
|
|
+ if (from.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (this.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) {
|
|
+ this.updateScheduledStart(from.getScheduledStart());
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ this.updateScheduledStart(TimeUtil.getGreatestTime(from.getScheduledStart(), this.getScheduledStart()));
|
|
+ }
|
|
+
|
|
+ private void copyDeadlineAndTickCount(final ConcreteRegionTickHandle from) {
|
|
+ this.currentTick = from.currentTick;
|
|
+
|
|
+ if (from.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ this.tickSchedule.setLastPeriod(from.tickSchedule.getLastPeriod());
|
|
+ this.setScheduledStart(from.getScheduledStart());
|
|
+ }
|
|
+
|
|
+ private void checkInitialSchedule() {
|
|
+ if (this.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) {
|
|
+ this.updateScheduledStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected boolean tryMarkTicking() {
|
|
+ return this.region.region.tryMarkTicking(ConcreteRegionTickHandle.this::isMarkedAsNonSchedulable);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected boolean markNotTicking() {
|
|
+ return this.region.region.markNotTicking();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) {
|
|
+ MinecraftServer.getServer().tickServer(startTime, scheduledEnd, TimeUnit.MILLISECONDS.toMillis(10L), this.region);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected boolean runRegionTasks(final BooleanSupplier canContinue) {
|
|
+ final RegionizedTaskQueue.RegionTaskQueueData queue = this.region.taskQueueData;
|
|
+
|
|
+ // first, update chunk loader
|
|
+ final ServerLevel world = this.region.world;
|
|
+ if (world.playerChunkLoader.tickMidTick()) {
|
|
+ // only process ticket updates if the player chunk loader did anything
|
|
+ world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates();
|
|
+ }
|
|
+
|
|
+ boolean processedChunkTask = false;
|
|
+
|
|
+ boolean executeChunkTask = true;
|
|
+ boolean executeTickTask = true;
|
|
+ do {
|
|
+ if (executeTickTask) {
|
|
+ executeTickTask = queue.executeTickTask();
|
|
+ }
|
|
+ if (executeChunkTask) {
|
|
+ processedChunkTask |= (executeChunkTask = queue.executeChunkTask());
|
|
+ }
|
|
+ } while ((executeChunkTask | executeTickTask) && canContinue.getAsBoolean());
|
|
+
|
|
+ if (processedChunkTask) {
|
|
+ // if we processed any chunk tasks, try to process ticket level updates for full status changes
|
|
+ world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates();
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected boolean hasIntermediateTasks() {
|
|
+ return this.region.taskQueueData.hasTasks();
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..a3fb013ef00cb0e4069c8ef8a4044c1fa9e58602
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java
|
|
@@ -0,0 +1,326 @@
|
|
+package io.papermc.paper.threadedregions.commands;
|
|
+
|
|
+import io.papermc.paper.threadedregions.RegionizedServer;
|
|
+import io.papermc.paper.threadedregions.ThreadedRegionizer;
|
|
+import io.papermc.paper.threadedregions.TickData;
|
|
+import io.papermc.paper.threadedregions.TickRegionScheduler;
|
|
+import io.papermc.paper.threadedregions.TickRegions;
|
|
+import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
|
|
+import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair;
|
|
+import net.kyori.adventure.text.Component;
|
|
+import net.kyori.adventure.text.TextComponent;
|
|
+import net.kyori.adventure.text.event.ClickEvent;
|
|
+import net.kyori.adventure.text.event.HoverEvent;
|
|
+import net.kyori.adventure.text.format.NamedTextColor;
|
|
+import net.kyori.adventure.text.format.TextColor;
|
|
+import net.kyori.adventure.text.format.TextDecoration;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.world.level.ChunkPos;
|
|
+import org.bukkit.Bukkit;
|
|
+import org.bukkit.World;
|
|
+import org.bukkit.command.Command;
|
|
+import org.bukkit.command.CommandSender;
|
|
+import org.bukkit.craftbukkit.CraftWorld;
|
|
+import org.bukkit.entity.Entity;
|
|
+import org.bukkit.entity.Player;
|
|
+import java.text.DecimalFormat;
|
|
+import java.util.ArrayList;
|
|
+import java.util.Arrays;
|
|
+import java.util.List;
|
|
+import java.util.Locale;
|
|
+
|
|
+public final class CommandServerHealth extends Command {
|
|
+
|
|
+ private static final 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("/<command> [server/region] [lowest regions to display]");
|
|
+ this.setDescription("Reports information about server health.");
|
|
+ this.setPermission("bukkit.command.tps");
|
|
+ }
|
|
+
|
|
+ private static Component formatRegionInfo(final String prefix, final double util, final double mspt, final double tps,
|
|
+ final boolean newline) {
|
|
+ return Component.text()
|
|
+ .append(Component.text(prefix, PRIMARY, TextDecoration.BOLD))
|
|
+ .append(Component.text(ONE_DECIMAL_PLACES.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 ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> 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<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>> regions =
|
|
+ new ArrayList<>();
|
|
+
|
|
+ for (final World bukkitWorld : Bukkit.getWorlds()) {
|
|
+ final ServerLevel world = ((CraftWorld)bukkitWorld).getHandle();
|
|
+ world.regioniser.computeForAllRegions(regions::add);
|
|
+ }
|
|
+
|
|
+ final double minTps;
|
|
+ final double medianTps;
|
|
+ final double maxTps;
|
|
+ double totalUtil = 0.0;
|
|
+
|
|
+ final DoubleArrayList tpsByRegion = new DoubleArrayList();
|
|
+ final List<TickData.TickReportData> reportsByRegion = new ArrayList<>();
|
|
+ final int maxThreadCount = TickRegions.getScheduler().getTotalThreadCount();
|
|
+
|
|
+ final long currTime = System.nanoTime();
|
|
+ final TickData.TickReportData globalTickReport = RegionizedServer.getGlobalTickData().getTickReport15s(currTime);
|
|
+
|
|
+ for (final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region : regions) {
|
|
+ final TickData.TickReportData report = region.getData().getRegionSchedulingHandle().getTickReport15s(currTime);
|
|
+ tpsByRegion.add(report == null ? 20.0 : report.tpsData().segmentAll().average());
|
|
+ reportsByRegion.add(report);
|
|
+ totalUtil += (report == null ? 0.0 : report.utilisation());
|
|
+ }
|
|
+
|
|
+ totalUtil += globalTickReport.utilisation();
|
|
+
|
|
+ tpsByRegion.sort(null);
|
|
+ if (!tpsByRegion.isEmpty()) {
|
|
+ minTps = tpsByRegion.getDouble(0);
|
|
+ maxTps = tpsByRegion.getDouble(tpsByRegion.size() - 1);
|
|
+
|
|
+ final int middle = tpsByRegion.size() >> 1;
|
|
+ if ((tpsByRegion.size() & 1) == 0) {
|
|
+ // even, average the two middle points
|
|
+ medianTps = (tpsByRegion.getDouble(middle - 1) + tpsByRegion.getDouble(middle)) / 2.0;
|
|
+ } else {
|
|
+ // odd, can just grab middle
|
|
+ medianTps = tpsByRegion.getDouble(middle);
|
|
+ }
|
|
+ } else {
|
|
+ // no regions = green
|
|
+ minTps = medianTps = maxTps = 20.0;
|
|
+ }
|
|
+
|
|
+ final List<ObjectObjectImmutablePair<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>, 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<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>, TickData.TickReportData>
|
|
+ pair = regionsBelowThreshold.get(i);
|
|
+
|
|
+ final TickData.TickReportData report = pair.right();
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> 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<String> 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<String> getSortedList(final Iterable<String> iterable) {
|
|
+ final List<String> ret = new ArrayList<>();
|
|
+ for (final String val : iterable) {
|
|
+ ret.add(val);
|
|
+ }
|
|
+
|
|
+ ret.sort(String.CASE_INSENSITIVE_ORDER);
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ public static List<String> getSortedList(final Iterable<String> iterable, final String prefix) {
|
|
+ final List<String> 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 <T> List<String> getSortedList(final Iterable<T> iterable, final Function<T, String> transform) {
|
|
+ final List<String> 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 <T> List<String> getSortedList(final Iterable<T> iterable, final Function<T, String> transform, final String prefix) {
|
|
+ final List<String> ret = new ArrayList<>();
|
|
+ for (final T val : iterable) {
|
|
+ final String string = transform.apply(val);
|
|
+ if (string != null && string.regionMatches(0, prefix, 0, prefix.length())) {
|
|
+ ret.add(string);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ ret.sort(String.CASE_INSENSITIVE_ORDER);
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ public static TextColor getColourForTPS(final double tps) {
|
|
+ final double difference = Math.min(Math.abs(20.0 - tps), 20.0);
|
|
+ final double coordinate;
|
|
+ if (difference <= 2.0) {
|
|
+ // >= 18 tps
|
|
+ coordinate = 70.0 + ((140.0 - 70.0)/(0.0 - 2.0)) * (difference - 2.0);
|
|
+ } else if (difference <= 5.0) {
|
|
+ // >= 15 tps
|
|
+ coordinate = 30.0 + ((70.0 - 30.0)/(2.0 - 5.0)) * (difference - 5.0);
|
|
+ } else if (difference <= 10.0) {
|
|
+ // >= 10 tps
|
|
+ coordinate = 10.0 + ((30.0 - 10.0)/(5.0 - 10.0)) * (difference - 10.0);
|
|
+ } else {
|
|
+ // >= 0.0 tps
|
|
+ coordinate = 0.0 + ((10.0 - 0.0)/(10.0 - 20.0)) * (difference - 20.0);
|
|
+ }
|
|
+
|
|
+ return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f));
|
|
+ }
|
|
+
|
|
+ public static TextColor getColourForMSPT(final double mspt) {
|
|
+ final double clamped = Math.min(Math.abs(mspt), 50.0);
|
|
+ final double coordinate;
|
|
+ if (clamped <= 15.0) {
|
|
+ coordinate = 130.0 + ((140.0 - 130.0)/(0.0 - 15.0)) * (clamped - 15.0);
|
|
+ } else if (clamped <= 25.0) {
|
|
+ coordinate = 90.0 + ((130.0 - 90.0)/(15.0 - 25.0)) * (clamped - 25.0);
|
|
+ } else if (clamped <= 35.0) {
|
|
+ coordinate = 30.0 + ((90.0 - 30.0)/(25.0 - 35.0)) * (clamped - 35.0);
|
|
+ } else if (clamped <= 40.0) {
|
|
+ coordinate = 15.0 + ((30.0 - 15.0)/(35.0 - 40.0)) * (clamped - 40.0);
|
|
+ } else {
|
|
+ coordinate = 0.0 + ((15.0 - 0.0)/(40.0 - 50.0)) * (clamped - 50.0);
|
|
+ }
|
|
+
|
|
+ return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f));
|
|
+ }
|
|
+
|
|
+ public static TextColor getUtilisationColourRegion(final double util) {
|
|
+ // TODO anything better?
|
|
+ // assume 20TPS
|
|
+ return getColourForMSPT(util * 50.0);
|
|
+ }
|
|
+
|
|
+ public static ServerPlayer getPlayer(final String name) {
|
|
+ for (final ServerPlayer player : MinecraftServer.getServer().getPlayerList().players) {
|
|
+ if (player.getGameProfile().getName().equalsIgnoreCase(name)) {
|
|
+ return player;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ private CommandUtil() {}
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..4874ca2ddf6b7bf7b818f97cfbc59d349a69f5ce
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java
|
|
@@ -0,0 +1,327 @@
|
|
+package io.papermc.paper.threadedregions.scheduler;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+import com.mojang.logging.LogUtils;
|
|
+import org.bukkit.plugin.IllegalPluginAccessException;
|
|
+import org.bukkit.plugin.Plugin;
|
|
+import org.slf4j.Logger;
|
|
+import java.util.Set;
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
+import java.util.concurrent.Executor;
|
|
+import java.util.concurrent.Executors;
|
|
+import java.util.concurrent.ScheduledExecutorService;
|
|
+import java.util.concurrent.ScheduledFuture;
|
|
+import java.util.concurrent.SynchronousQueue;
|
|
+import java.util.concurrent.ThreadFactory;
|
|
+import java.util.concurrent.ThreadPoolExecutor;
|
|
+import java.util.concurrent.TimeUnit;
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
+import java.util.function.Consumer;
|
|
+import java.util.logging.Level;
|
|
+
|
|
+public final class FoliaAsyncScheduler implements AsyncScheduler {
|
|
+
|
|
+ private static final Logger LOGGER = LogUtils.getLogger();
|
|
+
|
|
+ private final Executor executors = new ThreadPoolExecutor(Math.max(4, Runtime.getRuntime().availableProcessors() / 2), Integer.MAX_VALUE,
|
|
+ 30L, TimeUnit.SECONDS, new SynchronousQueue<>(),
|
|
+ new ThreadFactory() {
|
|
+ private final AtomicInteger idGenerator = new AtomicInteger();
|
|
+
|
|
+ @Override
|
|
+ public Thread newThread(final Runnable run) {
|
|
+ final Thread ret = new Thread(run);
|
|
+
|
|
+ ret.setName("Folia Async Scheduler Thread #" + this.idGenerator.getAndIncrement());
|
|
+ ret.setPriority(Thread.NORM_PRIORITY - 1);
|
|
+ ret.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> {
|
|
+ LOGGER.error("Uncaught exception in thread: " + thread.getName(), thr);
|
|
+ });
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+ );
|
|
+
|
|
+ private final ScheduledExecutorService timerThread = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
|
|
+ @Override
|
|
+ public Thread newThread(final Runnable run) {
|
|
+ final Thread ret = new Thread(run);
|
|
+
|
|
+ ret.setName("Folia Async Scheduler Thread Timer");
|
|
+ ret.setPriority(Thread.NORM_PRIORITY + 1);
|
|
+ ret.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> {
|
|
+ LOGGER.error("Uncaught exception in thread: " + thread.getName(), thr);
|
|
+ });
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ });
|
|
+
|
|
+ private final Set<AsyncScheduledTask> tasks = ConcurrentHashMap.newKeySet();
|
|
+
|
|
+ @Override
|
|
+ public ScheduledTask runNow(final Plugin plugin, final Consumer<ScheduledTask> task) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(task, "Task may not be null");
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled");
|
|
+ }
|
|
+
|
|
+ final AsyncScheduledTask ret = new AsyncScheduledTask(plugin, -1L, task, null, -1L);
|
|
+
|
|
+ this.tasks.add(ret);
|
|
+ this.executors.execute(ret);
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ // handle race condition where plugin is disabled asynchronously
|
|
+ ret.cancel();
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ScheduledTask runDelayed(final Plugin plugin, final Consumer<ScheduledTask> task, final long delay,
|
|
+ final TimeUnit unit) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(task, "Task may not be null");
|
|
+ Validate.notNull(unit, "Time unit may not be null");
|
|
+ if (delay < 0L) {
|
|
+ throw new IllegalArgumentException("Delay may not be < 0");
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled");
|
|
+ }
|
|
+
|
|
+ return this.scheduleTimerTask(plugin, task, delay, -1L, unit);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ScheduledTask runAtFixedRate(final Plugin plugin, final Consumer<ScheduledTask> task, final long initialDelay,
|
|
+ final long period, final TimeUnit unit) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(task, "Task may not be null");
|
|
+ Validate.notNull(unit, "Time unit may not be null");
|
|
+ if (initialDelay < 0L) {
|
|
+ throw new IllegalArgumentException("Initial delay may not be < 0");
|
|
+ }
|
|
+ if (period <= 0L) {
|
|
+ throw new IllegalArgumentException("Period may not be <= 0");
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled");
|
|
+ }
|
|
+
|
|
+ return this.scheduleTimerTask(plugin, task, initialDelay, period, unit);
|
|
+ }
|
|
+
|
|
+ private AsyncScheduledTask scheduleTimerTask(final Plugin plugin, final Consumer<ScheduledTask> task, final long initialDelay,
|
|
+ final long period, final TimeUnit unit) {
|
|
+ final AsyncScheduledTask ret = new AsyncScheduledTask(
|
|
+ plugin, period <= 0 ? period : unit.toNanos(period), task, null,
|
|
+ System.nanoTime() + unit.toNanos(initialDelay)
|
|
+ );
|
|
+
|
|
+ synchronized (ret) {
|
|
+ // even though ret is not published, we need to synchronise while scheduling to avoid a race condition
|
|
+ // for when a scheduled task immediately executes before we update the delay field and state field
|
|
+ ret.setDelay(this.timerThread.schedule(ret, initialDelay, unit));
|
|
+ this.tasks.add(ret);
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ // handle race condition where plugin is disabled asynchronously
|
|
+ ret.cancel();
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void cancelTasks(final Plugin plugin) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+
|
|
+ for (final AsyncScheduledTask task : this.tasks) {
|
|
+ if (task.plugin == plugin) {
|
|
+ task.cancel();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private final class AsyncScheduledTask implements ScheduledTask, Runnable {
|
|
+
|
|
+ private static final int STATE_ON_TIMER = 0;
|
|
+ private static final int STATE_SCHEDULED_EXECUTOR = 1;
|
|
+ private static final int STATE_EXECUTING = 2;
|
|
+ private static final int STATE_EXECUTING_CANCELLED = 3;
|
|
+ private static final int STATE_FINISHED = 4;
|
|
+ private static final int STATE_CANCELLED = 5;
|
|
+
|
|
+ private final Plugin plugin;
|
|
+ private final long repeatDelay; // in ns
|
|
+ private Consumer<ScheduledTask> run;
|
|
+ private ScheduledFuture<?> delay;
|
|
+ private int state;
|
|
+ private long scheduleTarget;
|
|
+
|
|
+ public AsyncScheduledTask(final Plugin plugin, final long repeatDelay, final Consumer<ScheduledTask> run,
|
|
+ final ScheduledFuture<?> delay, final long firstTarget) {
|
|
+ this.plugin = plugin;
|
|
+ this.repeatDelay = repeatDelay;
|
|
+ this.run = run;
|
|
+ this.delay = delay;
|
|
+ this.state = delay == null ? STATE_SCHEDULED_EXECUTOR : STATE_ON_TIMER;
|
|
+ this.scheduleTarget = firstTarget;
|
|
+ }
|
|
+
|
|
+ private void setDelay(final ScheduledFuture<?> delay) {
|
|
+ this.delay = delay;
|
|
+ this.state = STATE_SCHEDULED_EXECUTOR;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void run() {
|
|
+ final boolean repeating = this.isRepeatingTask();
|
|
+ // try to advance state
|
|
+ final boolean timer;
|
|
+ synchronized (this) {
|
|
+ if (this.state == STATE_ON_TIMER) {
|
|
+ timer = true;
|
|
+ this.delay = null;
|
|
+ this.state = STATE_SCHEDULED_EXECUTOR;
|
|
+ } else if (this.state != STATE_SCHEDULED_EXECUTOR) {
|
|
+ // cancelled
|
|
+ if (this.state != STATE_CANCELLED) {
|
|
+ throw new IllegalStateException("Wrong state: " + this.state);
|
|
+ }
|
|
+ return;
|
|
+ } else {
|
|
+ timer = false;
|
|
+ this.state = STATE_EXECUTING;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (timer) {
|
|
+ // the scheduled executor is single thread, and unfortunately not expandable with threads
|
|
+ // so we just schedule onto the executor
|
|
+ FoliaAsyncScheduler.this.executors.execute(this);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ try {
|
|
+ this.run.accept(this);
|
|
+ } catch (final Throwable throwable) {
|
|
+ this.plugin.getLogger().log(Level.WARNING, "Async task for " + this.plugin.getDescription().getFullName() + " generated an exception", throwable);
|
|
+ } finally {
|
|
+ boolean removeFromTasks = false;
|
|
+ synchronized (this) {
|
|
+ if (!repeating) {
|
|
+ // only want to execute once, so we're done
|
|
+ removeFromTasks = true;
|
|
+ this.state = STATE_FINISHED;
|
|
+ } else if (this.state != STATE_EXECUTING_CANCELLED) {
|
|
+ this.state = STATE_ON_TIMER;
|
|
+ // account for any delays, whether it be by task exec. or scheduler issues so that we keep
|
|
+ // the fixed schedule
|
|
+ final long currTime = System.nanoTime();
|
|
+ final long delay = Math.max(0L, this.scheduleTarget + this.repeatDelay - currTime);
|
|
+ this.scheduleTarget = currTime + delay;
|
|
+ this.delay = FoliaAsyncScheduler.this.timerThread.schedule(this, delay, TimeUnit.NANOSECONDS);
|
|
+ } else {
|
|
+ // cancelled repeating task
|
|
+ removeFromTasks = true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (removeFromTasks) {
|
|
+ this.run = null;
|
|
+ FoliaAsyncScheduler.this.tasks.remove(this);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Plugin getOwningPlugin() {
|
|
+ return this.plugin;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isRepeatingTask() {
|
|
+ return this.repeatDelay > 0L;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public CancelledState cancel() {
|
|
+ ScheduledFuture<?> delay = null;
|
|
+ CancelledState ret;
|
|
+ synchronized (this) {
|
|
+ switch (this.state) {
|
|
+ case STATE_ON_TIMER: {
|
|
+ delay = this.delay;
|
|
+ this.delay = null;
|
|
+ this.state = STATE_CANCELLED;
|
|
+ ret = CancelledState.CANCELLED_BY_CALLER;
|
|
+ break;
|
|
+ }
|
|
+ case STATE_SCHEDULED_EXECUTOR: {
|
|
+ this.state = STATE_CANCELLED;
|
|
+ ret = CancelledState.CANCELLED_BY_CALLER;
|
|
+ break;
|
|
+ }
|
|
+ case STATE_EXECUTING: {
|
|
+ if (!this.isRepeatingTask()) {
|
|
+ return CancelledState.RUNNING;
|
|
+ }
|
|
+ this.state = STATE_EXECUTING_CANCELLED;
|
|
+ return CancelledState.NEXT_RUNS_CANCELLED;
|
|
+ }
|
|
+ case STATE_EXECUTING_CANCELLED: {
|
|
+ return CancelledState.NEXT_RUNS_CANCELLED_ALREADY;
|
|
+ }
|
|
+ case STATE_FINISHED: {
|
|
+ return CancelledState.ALREADY_EXECUTED;
|
|
+ }
|
|
+ case STATE_CANCELLED: {
|
|
+ return CancelledState.CANCELLED_ALREADY;
|
|
+ }
|
|
+ default: {
|
|
+ throw new IllegalStateException("Unknown state: " + this.state);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (delay != null) {
|
|
+ delay.cancel(false);
|
|
+ }
|
|
+ this.run = null;
|
|
+ FoliaAsyncScheduler.this.tasks.remove(this);
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ExecutionState getExecutionState() {
|
|
+ synchronized (this) {
|
|
+ switch (this.state) {
|
|
+ case STATE_ON_TIMER:
|
|
+ case STATE_SCHEDULED_EXECUTOR:
|
|
+ return ExecutionState.IDLE;
|
|
+ case STATE_EXECUTING:
|
|
+ return ExecutionState.RUNNING;
|
|
+ case STATE_EXECUTING_CANCELLED:
|
|
+ return ExecutionState.CANCELLED_RUNNING;
|
|
+ case STATE_FINISHED:
|
|
+ return ExecutionState.FINISHED;
|
|
+ case STATE_CANCELLED:
|
|
+ return ExecutionState.CANCELLED;
|
|
+ default: {
|
|
+ throw new IllegalStateException("Unknown state: " + this.state);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..60862f43a4469bccf3cf1b565de2956ba50396f6
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java
|
|
@@ -0,0 +1,267 @@
|
|
+package io.papermc.paper.threadedregions.scheduler;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+import net.minecraft.world.entity.Entity;
|
|
+import org.bukkit.craftbukkit.entity.CraftEntity;
|
|
+import org.bukkit.plugin.IllegalPluginAccessException;
|
|
+import org.bukkit.plugin.Plugin;
|
|
+import org.jetbrains.annotations.Nullable;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.function.Consumer;
|
|
+import java.util.logging.Level;
|
|
+
|
|
+public final class FoliaEntityScheduler implements EntityScheduler {
|
|
+
|
|
+ private final CraftEntity entity;
|
|
+
|
|
+ public FoliaEntityScheduler(final CraftEntity entity) {
|
|
+ this.entity = entity;
|
|
+ }
|
|
+
|
|
+ private static Consumer<? extends net.minecraft.world.entity.Entity> wrap(final Plugin plugin, final Runnable runnable) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(runnable, "Runnable may not be null");
|
|
+
|
|
+ return (final net.minecraft.world.entity.Entity nmsEntity) -> {
|
|
+ if (!plugin.isEnabled()) {
|
|
+ // don't execute if the plugin is disabled
|
|
+ return;
|
|
+ }
|
|
+ try {
|
|
+ runnable.run();
|
|
+ } catch (final Throwable throwable) {
|
|
+ plugin.getLogger().log(Level.WARNING, "Entity task for " + plugin.getDescription().getFullName() + " generated an exception", throwable);
|
|
+ }
|
|
+ };
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean execute(final Plugin plugin, final Runnable run, final Runnable retired,
|
|
+ final long delay) {
|
|
+ final Consumer<? extends net.minecraft.world.entity.Entity> runNMS = wrap(plugin, run);
|
|
+ final Consumer<? extends net.minecraft.world.entity.Entity> runRetired = retired == null ? null : wrap(plugin, retired);
|
|
+
|
|
+ return this.entity.taskScheduler.schedule(runNMS, runRetired, delay);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public @Nullable ScheduledTask run(final Plugin plugin, final Consumer<ScheduledTask> task, final Runnable retired) {
|
|
+ return this.runDelayed(plugin, task, retired, 1);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public @Nullable ScheduledTask runDelayed(final Plugin plugin, final Consumer<ScheduledTask> task, final Runnable retired,
|
|
+ final int delayTicks) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(task, "Task may not be null");
|
|
+ if (delayTicks <= 0) {
|
|
+ throw new IllegalArgumentException("Delay ticks may not be <= 0");
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled");
|
|
+ }
|
|
+
|
|
+ final EntityScheduledTask ret = new EntityScheduledTask(plugin, -1, task, retired);
|
|
+
|
|
+ if (!this.scheduleInternal(ret, delayTicks)) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ // handle race condition where plugin is disabled asynchronously
|
|
+ ret.cancel();
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public @Nullable ScheduledTask runAtFixedRate(final Plugin plugin, final Consumer<ScheduledTask> task,
|
|
+ final Runnable retired, final int initialDelayTicks, final int periodTicks) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(task, "Task may not be null");
|
|
+ if (initialDelayTicks <= 0) {
|
|
+ throw new IllegalArgumentException("Initial delay ticks may not be <= 0");
|
|
+ }
|
|
+ if (periodTicks <= 0) {
|
|
+ throw new IllegalArgumentException("Period ticks may not be <= 0");
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled");
|
|
+ }
|
|
+
|
|
+ final EntityScheduledTask ret = new EntityScheduledTask(plugin, periodTicks, task, retired);
|
|
+
|
|
+ if (!this.scheduleInternal(ret, initialDelayTicks)) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ // handle race condition where plugin is disabled asynchronously
|
|
+ ret.cancel();
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ private boolean scheduleInternal(final EntityScheduledTask ret, final int delay) {
|
|
+ return this.entity.taskScheduler.schedule(ret, ret, delay);
|
|
+ }
|
|
+
|
|
+ private final class EntityScheduledTask implements ScheduledTask, Consumer<Entity> {
|
|
+
|
|
+ private static final int STATE_IDLE = 0;
|
|
+ private static final int STATE_EXECUTING = 1;
|
|
+ private static final int STATE_EXECUTING_CANCELLED = 2;
|
|
+ private static final int STATE_FINISHED = 3;
|
|
+ private static final int STATE_CANCELLED = 4;
|
|
+
|
|
+ private final Plugin plugin;
|
|
+ private final int repeatDelay; // in ticks
|
|
+ private Consumer<ScheduledTask> run;
|
|
+ private Runnable retired;
|
|
+ private volatile int state;
|
|
+
|
|
+ private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(FoliaEntityScheduler.EntityScheduledTask.class, "state", int.class);
|
|
+
|
|
+ private EntityScheduledTask(final Plugin plugin, final int repeatDelay, final Consumer<ScheduledTask> run, final Runnable retired) {
|
|
+ this.plugin = plugin;
|
|
+ this.repeatDelay = repeatDelay;
|
|
+ this.run = run;
|
|
+ this.retired = retired;
|
|
+ }
|
|
+
|
|
+ private final int getStateVolatile() {
|
|
+ return (int)STATE_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ private final int compareAndExchangeStateVolatile(final int expect, final int update) {
|
|
+ return (int)STATE_HANDLE.compareAndExchange(this, expect, update);
|
|
+ }
|
|
+
|
|
+ private final void setStateVolatile(final int value) {
|
|
+ STATE_HANDLE.setVolatile(this, value);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void accept(final Entity entity) {
|
|
+ if (!this.plugin.isEnabled()) {
|
|
+ // don't execute if the plugin is disabled
|
|
+ this.setStateVolatile(STATE_CANCELLED);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ final boolean repeating = this.isRepeatingTask();
|
|
+ if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) {
|
|
+ // cancelled
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ final boolean retired = entity.isRemoved();
|
|
+
|
|
+ try {
|
|
+ if (!retired) {
|
|
+ this.run.accept(this);
|
|
+ } else {
|
|
+ if (this.retired != null) {
|
|
+ this.retired.run();
|
|
+ }
|
|
+ }
|
|
+ } catch (final Throwable throwable) {
|
|
+ this.plugin.getLogger().log(Level.WARNING, "Entity task for " + this.plugin.getDescription().getFullName() + " generated an exception", throwable);
|
|
+ } finally {
|
|
+ boolean reschedule = false;
|
|
+ if (!repeating && !retired) {
|
|
+ this.setStateVolatile(STATE_FINISHED);
|
|
+ } else if (retired || !this.plugin.isEnabled()) {
|
|
+ this.setStateVolatile(STATE_CANCELLED);
|
|
+ } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) {
|
|
+ reschedule = true;
|
|
+ } // else: cancelled repeating task
|
|
+
|
|
+ if (!reschedule) {
|
|
+ this.run = null;
|
|
+ this.retired = null;
|
|
+ } else {
|
|
+ if (!FoliaEntityScheduler.this.scheduleInternal(this, this.repeatDelay)) {
|
|
+ // the task itself must have removed the entity, so in this case we need to mark as cancelled
|
|
+ this.setStateVolatile(STATE_CANCELLED);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Plugin getOwningPlugin() {
|
|
+ return this.plugin;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isRepeatingTask() {
|
|
+ return this.repeatDelay > 0;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public CancelledState cancel() {
|
|
+ for (int curr = this.getStateVolatile();;) {
|
|
+ switch (curr) {
|
|
+ case STATE_IDLE: {
|
|
+ if (STATE_IDLE == (curr = this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_CANCELLED))) {
|
|
+ this.state = STATE_CANCELLED;
|
|
+ this.run = null;
|
|
+ this.retired = null;
|
|
+ return CancelledState.CANCELLED_BY_CALLER;
|
|
+ }
|
|
+ // try again
|
|
+ continue;
|
|
+ }
|
|
+ case STATE_EXECUTING: {
|
|
+ if (!this.isRepeatingTask()) {
|
|
+ return CancelledState.RUNNING;
|
|
+ }
|
|
+ if (STATE_EXECUTING == (curr = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_EXECUTING_CANCELLED))) {
|
|
+ return CancelledState.NEXT_RUNS_CANCELLED;
|
|
+ }
|
|
+ // try again
|
|
+ continue;
|
|
+ }
|
|
+ case STATE_EXECUTING_CANCELLED: {
|
|
+ return CancelledState.NEXT_RUNS_CANCELLED_ALREADY;
|
|
+ }
|
|
+ case STATE_FINISHED: {
|
|
+ return CancelledState.ALREADY_EXECUTED;
|
|
+ }
|
|
+ case STATE_CANCELLED: {
|
|
+ return CancelledState.CANCELLED_ALREADY;
|
|
+ }
|
|
+ default: {
|
|
+ throw new IllegalStateException("Unknown state: " + curr);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ExecutionState getExecutionState() {
|
|
+ final int state = this.getStateVolatile();
|
|
+ switch (state) {
|
|
+ case STATE_IDLE:
|
|
+ return ExecutionState.IDLE;
|
|
+ case STATE_EXECUTING:
|
|
+ return ExecutionState.RUNNING;
|
|
+ case STATE_EXECUTING_CANCELLED:
|
|
+ return ExecutionState.CANCELLED_RUNNING;
|
|
+ case STATE_FINISHED:
|
|
+ return ExecutionState.FINISHED;
|
|
+ case STATE_CANCELLED:
|
|
+ return ExecutionState.CANCELLED;
|
|
+ default: {
|
|
+ throw new IllegalStateException("Unknown state: " + state);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaGlobalRegionScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaGlobalRegionScheduler.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..505329d601d56e42daa0b794092590cb16560f6d
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaGlobalRegionScheduler.java
|
|
@@ -0,0 +1,266 @@
|
|
+package io.papermc.paper.threadedregions.scheduler;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
|
+import org.bukkit.plugin.IllegalPluginAccessException;
|
|
+import org.bukkit.plugin.Plugin;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.ArrayList;
|
|
+import java.util.List;
|
|
+import java.util.function.Consumer;
|
|
+import java.util.logging.Level;
|
|
+
|
|
+public final class FoliaGlobalRegionScheduler implements GlobalRegionScheduler {
|
|
+
|
|
+ private long tickCount = 0L;
|
|
+ private final Object stateLock = new Object();
|
|
+ private final Long2ObjectOpenHashMap<List<GlobalScheduledTask>> tasksByDeadline = new Long2ObjectOpenHashMap<>();
|
|
+
|
|
+ public void tick() {
|
|
+ final List<GlobalScheduledTask> run;
|
|
+ synchronized (this.stateLock) {
|
|
+ ++this.tickCount;
|
|
+ if (this.tasksByDeadline.isEmpty()) {
|
|
+ run = null;
|
|
+ } else {
|
|
+ run = this.tasksByDeadline.remove(this.tickCount);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (run == null) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ for (int i = 0, len = run.size(); i < len; ++i) {
|
|
+ run.get(i).run();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void execute(final Plugin plugin, final Runnable run) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(run, "Runnable may not be null");
|
|
+
|
|
+ this.run(plugin, (final ScheduledTask task) -> {
|
|
+ run.run();
|
|
+ });
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ScheduledTask run(final Plugin plugin, final Consumer<ScheduledTask> task) {
|
|
+ return this.runDelayed(plugin, task, 1);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ScheduledTask runDelayed(final Plugin plugin, final Consumer<ScheduledTask> task, final int delayTicks) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(task, "Task may not be null");
|
|
+ if (delayTicks <= 0) {
|
|
+ throw new IllegalArgumentException("Delay ticks may not be <= 0");
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled");
|
|
+ }
|
|
+
|
|
+ final GlobalScheduledTask ret = new GlobalScheduledTask(plugin, -1, task);
|
|
+
|
|
+ this.scheduleInternal(ret, delayTicks);
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ // handle race condition where plugin is disabled asynchronously
|
|
+ ret.cancel();
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ScheduledTask runAtFixedRate(final Plugin plugin, final Consumer<ScheduledTask> task, final int initialDelayTicks, final int periodTicks) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(task, "Task may not be null");
|
|
+ if (initialDelayTicks <= 0) {
|
|
+ throw new IllegalArgumentException("Initial delay ticks may not be <= 0");
|
|
+ }
|
|
+ if (periodTicks <= 0) {
|
|
+ throw new IllegalArgumentException("Period ticks may not be <= 0");
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled");
|
|
+ }
|
|
+
|
|
+ final GlobalScheduledTask ret = new GlobalScheduledTask(plugin, periodTicks, task);
|
|
+
|
|
+ this.scheduleInternal(ret, initialDelayTicks);
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ // handle race condition where plugin is disabled asynchronously
|
|
+ ret.cancel();
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void cancelTasks(final Plugin plugin) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+
|
|
+ final List<GlobalScheduledTask> toCancel = new ArrayList<>();
|
|
+ synchronized (this.stateLock) {
|
|
+ for (final List<GlobalScheduledTask> tasks : this.tasksByDeadline.values()) {
|
|
+ for (int i = 0, len = tasks.size(); i < len; ++i) {
|
|
+ final GlobalScheduledTask task = tasks.get(i);
|
|
+ if (task.plugin == plugin) {
|
|
+ toCancel.add(task);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for (int i = 0, len = toCancel.size(); i < len; ++i) {
|
|
+ toCancel.get(i).cancel();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void scheduleInternal(final GlobalScheduledTask task, final int delay) {
|
|
+ // note: delay > 0
|
|
+ synchronized (this.stateLock) {
|
|
+ this.tasksByDeadline.computeIfAbsent(this.tickCount + delay, (final long keyInMap) -> {
|
|
+ return new ArrayList<>();
|
|
+ }).add(task);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private final class GlobalScheduledTask implements ScheduledTask, Runnable {
|
|
+
|
|
+ private static final int STATE_IDLE = 0;
|
|
+ private static final int STATE_EXECUTING = 1;
|
|
+ private static final int STATE_EXECUTING_CANCELLED = 2;
|
|
+ private static final int STATE_FINISHED = 3;
|
|
+ private static final int STATE_CANCELLED = 4;
|
|
+
|
|
+ private final Plugin plugin;
|
|
+ private final int repeatDelay; // in ticks
|
|
+ private Consumer<ScheduledTask> run;
|
|
+ private volatile int state;
|
|
+
|
|
+ private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(GlobalScheduledTask.class, "state", int.class);
|
|
+
|
|
+ private GlobalScheduledTask(final Plugin plugin, final int repeatDelay, final Consumer<ScheduledTask> run) {
|
|
+ this.plugin = plugin;
|
|
+ this.repeatDelay = repeatDelay;
|
|
+ this.run = run;
|
|
+ }
|
|
+
|
|
+ private final int getStateVolatile() {
|
|
+ return (int)STATE_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ private final int compareAndExchangeStateVolatile(final int expect, final int update) {
|
|
+ return (int)STATE_HANDLE.compareAndExchange(this, expect, update);
|
|
+ }
|
|
+
|
|
+ private final void setStateVolatile(final int value) {
|
|
+ STATE_HANDLE.setVolatile(this, value);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void run() {
|
|
+ final boolean repeating = this.isRepeatingTask();
|
|
+ if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) {
|
|
+ // cancelled
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ try {
|
|
+ this.run.accept(this);
|
|
+ } catch (final Throwable throwable) {
|
|
+ this.plugin.getLogger().log(Level.WARNING, "Global task for " + this.plugin.getDescription().getFullName() + " generated an exception", throwable);
|
|
+ } finally {
|
|
+ boolean reschedule = false;
|
|
+ if (!repeating) {
|
|
+ this.setStateVolatile(STATE_FINISHED);
|
|
+ } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) {
|
|
+ reschedule = true;
|
|
+ } // else: cancelled repeating task
|
|
+
|
|
+ if (!reschedule) {
|
|
+ this.run = null;
|
|
+ } else {
|
|
+ FoliaGlobalRegionScheduler.this.scheduleInternal(this, this.repeatDelay);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Plugin getOwningPlugin() {
|
|
+ return this.plugin;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isRepeatingTask() {
|
|
+ return this.repeatDelay > 0;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public CancelledState cancel() {
|
|
+ for (int curr = this.getStateVolatile();;) {
|
|
+ switch (curr) {
|
|
+ case STATE_IDLE: {
|
|
+ if (STATE_IDLE == (curr = this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_CANCELLED))) {
|
|
+ this.state = STATE_CANCELLED;
|
|
+ this.run = null;
|
|
+ return CancelledState.CANCELLED_BY_CALLER;
|
|
+ }
|
|
+ // try again
|
|
+ continue;
|
|
+ }
|
|
+ case STATE_EXECUTING: {
|
|
+ if (!this.isRepeatingTask()) {
|
|
+ return CancelledState.RUNNING;
|
|
+ }
|
|
+ if (STATE_EXECUTING == (curr = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_EXECUTING_CANCELLED))) {
|
|
+ return CancelledState.NEXT_RUNS_CANCELLED;
|
|
+ }
|
|
+ // try again
|
|
+ continue;
|
|
+ }
|
|
+ case STATE_EXECUTING_CANCELLED: {
|
|
+ return CancelledState.NEXT_RUNS_CANCELLED_ALREADY;
|
|
+ }
|
|
+ case STATE_FINISHED: {
|
|
+ return CancelledState.ALREADY_EXECUTED;
|
|
+ }
|
|
+ case STATE_CANCELLED: {
|
|
+ return CancelledState.CANCELLED_ALREADY;
|
|
+ }
|
|
+ default: {
|
|
+ throw new IllegalStateException("Unknown state: " + curr);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ExecutionState getExecutionState() {
|
|
+ final int state = this.getStateVolatile();
|
|
+ switch (state) {
|
|
+ case STATE_IDLE:
|
|
+ return ExecutionState.IDLE;
|
|
+ case STATE_EXECUTING:
|
|
+ return ExecutionState.RUNNING;
|
|
+ case STATE_EXECUTING_CANCELLED:
|
|
+ return ExecutionState.CANCELLED_RUNNING;
|
|
+ case STATE_FINISHED:
|
|
+ return ExecutionState.FINISHED;
|
|
+ case STATE_CANCELLED:
|
|
+ return ExecutionState.CANCELLED;
|
|
+ default: {
|
|
+ throw new IllegalStateException("Unknown state: " + state);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..7ee75fe44507f79362f15d6445e32954dcc4308c
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java
|
|
@@ -0,0 +1,422 @@
|
|
+package io.papermc.paper.threadedregions.scheduler;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager;
|
|
+import io.papermc.paper.threadedregions.RegionizedData;
|
|
+import io.papermc.paper.threadedregions.RegionizedServer;
|
|
+import io.papermc.paper.threadedregions.TickRegionScheduler;
|
|
+import io.papermc.paper.threadedregions.TickRegions;
|
|
+import io.papermc.paper.util.CoordinateUtils;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.server.level.TicketType;
|
|
+import net.minecraft.util.Unit;
|
|
+import org.bukkit.Bukkit;
|
|
+import org.bukkit.World;
|
|
+import org.bukkit.craftbukkit.CraftWorld;
|
|
+import org.bukkit.plugin.IllegalPluginAccessException;
|
|
+import org.bukkit.plugin.Plugin;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.ArrayList;
|
|
+import java.util.Iterator;
|
|
+import java.util.List;
|
|
+import java.util.function.Consumer;
|
|
+import java.util.logging.Level;
|
|
+
|
|
+public final class FoliaRegionScheduler implements RegionScheduler {
|
|
+
|
|
+ private static Runnable wrap(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) {
|
|
+ return () -> {
|
|
+ try {
|
|
+ run.run();
|
|
+ } catch (final Throwable throwable) {
|
|
+ plugin.getLogger().log(Level.WARNING, "Location task for " + plugin.getDescription().getFullName()
|
|
+ + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable);
|
|
+ }
|
|
+ };
|
|
+ }
|
|
+
|
|
+ private static final RegionizedData<Scheduler> SCHEDULER_DATA = new RegionizedData<>(null, Scheduler::new, Scheduler.REGIONISER_CALLBACK);
|
|
+
|
|
+ private static void scheduleInternalOnRegion(final LocationScheduledTask task, final int delay) {
|
|
+ SCHEDULER_DATA.get().queueTask(task, delay);
|
|
+ }
|
|
+
|
|
+ private static void scheduleInternalOffRegion(final LocationScheduledTask task, final int delay) {
|
|
+ final World world = task.world;
|
|
+ if (world == null) {
|
|
+ // cancelled
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ ((CraftWorld) world).getHandle(), task.chunkX, task.chunkZ, () -> {
|
|
+ scheduleInternalOnRegion(task, delay);
|
|
+ }
|
|
+ );
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void execute(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(world, "World may not be null");
|
|
+ Validate.notNull(run, "Runnable may not be null");
|
|
+
|
|
+ RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ ((CraftWorld) world).getHandle(), chunkX, chunkZ, wrap(plugin, world, chunkX, chunkZ, run)
|
|
+ );
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ScheduledTask run(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Consumer<ScheduledTask> task) {
|
|
+ return this.runDelayed(plugin, world, chunkX, chunkZ, task, 1);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ScheduledTask runDelayed(final Plugin plugin, final World world, final int chunkX, final int chunkZ,
|
|
+ final Consumer<ScheduledTask> task, final int delayTicks) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(world, "World may not be null");
|
|
+ Validate.notNull(task, "Task may not be null");
|
|
+ if (delayTicks <= 0) {
|
|
+ throw new IllegalArgumentException("Delay ticks may not be <= 0");
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled");
|
|
+ }
|
|
+
|
|
+ final LocationScheduledTask ret = new LocationScheduledTask(plugin, world, chunkX, chunkZ, -1, task);
|
|
+
|
|
+ if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
|
|
+ scheduleInternalOnRegion(ret, delayTicks);
|
|
+ } else {
|
|
+ scheduleInternalOffRegion(ret, delayTicks);
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ // handle race condition where plugin is disabled asynchronously
|
|
+ ret.cancel();
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ScheduledTask runAtFixedRate(final Plugin plugin, final World world, final int chunkX, final int chunkZ,
|
|
+ final Consumer<ScheduledTask> task, final int initialDelayTicks, final int periodTicks) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(world, "World may not be null");
|
|
+ Validate.notNull(task, "Task may not be null");
|
|
+ if (initialDelayTicks <= 0) {
|
|
+ throw new IllegalArgumentException("Initial delay ticks may not be <= 0");
|
|
+ }
|
|
+ if (periodTicks <= 0) {
|
|
+ throw new IllegalArgumentException("Period ticks may not be <= 0");
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled");
|
|
+ }
|
|
+
|
|
+ final LocationScheduledTask ret = new LocationScheduledTask(plugin, world, chunkX, chunkZ, periodTicks, task);
|
|
+
|
|
+ if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
|
|
+ scheduleInternalOnRegion(ret, initialDelayTicks);
|
|
+ } else {
|
|
+ scheduleInternalOffRegion(ret, initialDelayTicks);
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ // handle race condition where plugin is disabled asynchronously
|
|
+ ret.cancel();
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ public void tick() {
|
|
+ SCHEDULER_DATA.get().tick();
|
|
+ }
|
|
+
|
|
+ private static final class Scheduler {
|
|
+ private static final RegionizedData.RegioniserCallback<Scheduler> REGIONISER_CALLBACK = new RegionizedData.RegioniserCallback<>() {
|
|
+ @Override
|
|
+ public void merge(final Scheduler from, final Scheduler into, final long fromTickOffset) {
|
|
+ for (final Iterator<Long2ObjectMap.Entry<Long2ObjectOpenHashMap<List<LocationScheduledTask>>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator();
|
|
+ sectionIterator.hasNext();) {
|
|
+ final Long2ObjectMap.Entry<Long2ObjectOpenHashMap<List<LocationScheduledTask>>> entry = sectionIterator.next();
|
|
+ final long sectionKey = entry.getLongKey();
|
|
+ final Long2ObjectOpenHashMap<List<LocationScheduledTask>> section = entry.getValue();
|
|
+
|
|
+ final Long2ObjectOpenHashMap<List<LocationScheduledTask>> sectionAdjusted = new Long2ObjectOpenHashMap<>(section.size());
|
|
+
|
|
+ for (final Iterator<Long2ObjectMap.Entry<List<LocationScheduledTask>>> iterator = section.long2ObjectEntrySet().fastIterator();
|
|
+ iterator.hasNext();) {
|
|
+ final Long2ObjectMap.Entry<List<LocationScheduledTask>> e = iterator.next();
|
|
+ final long newTick = e.getLongKey() + fromTickOffset;
|
|
+ final List<LocationScheduledTask> tasks = e.getValue();
|
|
+
|
|
+ sectionAdjusted.put(newTick, tasks);
|
|
+ }
|
|
+
|
|
+ into.tasksByDeadlineBySection.put(sectionKey, sectionAdjusted);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void split(final Scheduler from, final int chunkToRegionShift, final Long2ReferenceOpenHashMap<Scheduler> regionToData,
|
|
+ final ReferenceOpenHashSet<Scheduler> dataSet) {
|
|
+ for (final Scheduler into : dataSet) {
|
|
+ into.tickCount = from.tickCount;
|
|
+ }
|
|
+
|
|
+ for (final Iterator<Long2ObjectMap.Entry<Long2ObjectOpenHashMap<List<LocationScheduledTask>>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator();
|
|
+ sectionIterator.hasNext();) {
|
|
+ final Long2ObjectMap.Entry<Long2ObjectOpenHashMap<List<LocationScheduledTask>>> entry = sectionIterator.next();
|
|
+ final long sectionKey = entry.getLongKey();
|
|
+ final Long2ObjectOpenHashMap<List<LocationScheduledTask>> section = entry.getValue();
|
|
+
|
|
+ final Scheduler into = regionToData.get(sectionKey);
|
|
+
|
|
+ into.tasksByDeadlineBySection.put(sectionKey, section);
|
|
+ }
|
|
+ }
|
|
+ };
|
|
+
|
|
+ private long tickCount = 0L;
|
|
+ // map of region section -> map of deadline -> list of tasks
|
|
+ private final Long2ObjectOpenHashMap<Long2ObjectOpenHashMap<List<LocationScheduledTask>>> tasksByDeadlineBySection = new Long2ObjectOpenHashMap<>();
|
|
+
|
|
+ private void addTicket(final int sectionX, final int sectionZ) {
|
|
+ final ServerLevel world = TickRegionScheduler.getCurrentRegionizedWorldData().world;
|
|
+ final int chunkX = sectionX << TickRegions.getRegionChunkShift();
|
|
+ final int chunkZ = sectionZ << TickRegions.getRegionChunkShift();
|
|
+
|
|
+ world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel(
|
|
+ TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE
|
|
+ );
|
|
+ }
|
|
+
|
|
+ private void removeTicket(final long sectionKey) {
|
|
+ final ServerLevel world = TickRegionScheduler.getCurrentRegionizedWorldData().world;
|
|
+ final int chunkX = CoordinateUtils.getChunkX(sectionKey) << TickRegions.getRegionChunkShift();
|
|
+ final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << TickRegions.getRegionChunkShift();
|
|
+
|
|
+ world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel(
|
|
+ TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE
|
|
+ );
|
|
+ }
|
|
+
|
|
+ private void queueTask(final LocationScheduledTask task, final int delay) {
|
|
+ // note: must be on the thread that owns this scheduler
|
|
+ // note: delay > 0
|
|
+
|
|
+ final World world = task.world;
|
|
+ if (world == null) {
|
|
+ // cancelled
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ final int sectionX = task.chunkX >> TickRegions.getRegionChunkShift();
|
|
+ final int sectionZ = task.chunkZ >> TickRegions.getRegionChunkShift();
|
|
+
|
|
+ final Long2ObjectOpenHashMap<List<LocationScheduledTask>> section =
|
|
+ this.tasksByDeadlineBySection.computeIfAbsent(CoordinateUtils.getChunkKey(sectionX, sectionZ), (final long keyInMap) -> {
|
|
+ return new Long2ObjectOpenHashMap<>();
|
|
+ }
|
|
+ );
|
|
+
|
|
+ if (section.isEmpty()) {
|
|
+ // need to keep the scheduler loaded for this location in order for tick() to be called...
|
|
+ this.addTicket(sectionX, sectionZ);
|
|
+ }
|
|
+
|
|
+ section.computeIfAbsent(this.tickCount + delay, (final long keyInMap) -> {
|
|
+ return new ArrayList<>();
|
|
+ }).add(task);
|
|
+ }
|
|
+
|
|
+ public void tick() {
|
|
+ ++this.tickCount;
|
|
+
|
|
+ final List<LocationScheduledTask> run = new ArrayList<>();
|
|
+
|
|
+ for (final Iterator<Long2ObjectMap.Entry<Long2ObjectOpenHashMap<List<LocationScheduledTask>>>> sectionIterator = this.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator();
|
|
+ sectionIterator.hasNext();) {
|
|
+ final Long2ObjectMap.Entry<Long2ObjectOpenHashMap<List<LocationScheduledTask>>> entry = sectionIterator.next();
|
|
+ final long sectionKey = entry.getLongKey();
|
|
+ final Long2ObjectOpenHashMap<List<LocationScheduledTask>> section = entry.getValue();
|
|
+
|
|
+ final List<LocationScheduledTask> tasks = section.remove(this.tickCount);
|
|
+
|
|
+ if (tasks == null) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ run.addAll(tasks);
|
|
+
|
|
+ if (section.isEmpty()) {
|
|
+ this.removeTicket(sectionKey);
|
|
+ sectionIterator.remove();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for (int i = 0, len = run.size(); i < len; ++i) {
|
|
+ run.get(i).run();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private static final class LocationScheduledTask implements ScheduledTask, Runnable {
|
|
+
|
|
+ private static final int STATE_IDLE = 0;
|
|
+ private static final int STATE_EXECUTING = 1;
|
|
+ private static final int STATE_EXECUTING_CANCELLED = 2;
|
|
+ private static final int STATE_FINISHED = 3;
|
|
+ private static final int STATE_CANCELLED = 4;
|
|
+
|
|
+ private final Plugin plugin;
|
|
+ private final int chunkX;
|
|
+ private final int chunkZ;
|
|
+ private final int repeatDelay; // in ticks
|
|
+ private World world;
|
|
+ private Consumer<ScheduledTask> run;
|
|
+
|
|
+ private volatile int state;
|
|
+ private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(LocationScheduledTask.class, "state", int.class);
|
|
+
|
|
+ private LocationScheduledTask(final Plugin plugin, final World world, final int chunkX, final int chunkZ,
|
|
+ final int repeatDelay, final Consumer<ScheduledTask> run) {
|
|
+ this.plugin = plugin;
|
|
+ this.world = world;
|
|
+ this.chunkX = chunkX;
|
|
+ this.chunkZ = chunkZ;
|
|
+ this.repeatDelay = repeatDelay;
|
|
+ this.run = run;
|
|
+ }
|
|
+
|
|
+ private final int getStateVolatile() {
|
|
+ return (int)STATE_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ private final int compareAndExchangeStateVolatile(final int expect, final int update) {
|
|
+ return (int)STATE_HANDLE.compareAndExchange(this, expect, update);
|
|
+ }
|
|
+
|
|
+ private final void setStateVolatile(final int value) {
|
|
+ STATE_HANDLE.setVolatile(this, value);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void run() {
|
|
+ if (!this.plugin.isEnabled()) {
|
|
+ // don't execute if the plugin is disabled
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ final boolean repeating = this.isRepeatingTask();
|
|
+ if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) {
|
|
+ // cancelled
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ try {
|
|
+ this.run.accept(this);
|
|
+ } catch (final Throwable throwable) {
|
|
+ this.plugin.getLogger().log(Level.WARNING, "Location task for " + this.plugin.getDescription().getFullName()
|
|
+ + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable);
|
|
+ } finally {
|
|
+ boolean reschedule = false;
|
|
+ if (!repeating) {
|
|
+ this.setStateVolatile(STATE_FINISHED);
|
|
+ } else if (!this.plugin.isEnabled()) {
|
|
+ this.setStateVolatile(STATE_CANCELLED);
|
|
+ } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) {
|
|
+ reschedule = true;
|
|
+ } // else: cancelled repeating task
|
|
+
|
|
+ if (!reschedule) {
|
|
+ this.run = null;
|
|
+ this.world = null;
|
|
+ } else {
|
|
+ FoliaRegionScheduler.scheduleInternalOnRegion(this, this.repeatDelay);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Plugin getOwningPlugin() {
|
|
+ return this.plugin;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isRepeatingTask() {
|
|
+ return this.repeatDelay > 0;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public CancelledState cancel() {
|
|
+ for (int curr = this.getStateVolatile();;) {
|
|
+ switch (curr) {
|
|
+ case STATE_IDLE: {
|
|
+ if (STATE_IDLE == (curr = this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_CANCELLED))) {
|
|
+ this.state = STATE_CANCELLED;
|
|
+ this.run = null;
|
|
+ this.world = null;
|
|
+ return CancelledState.CANCELLED_BY_CALLER;
|
|
+ }
|
|
+ // try again
|
|
+ continue;
|
|
+ }
|
|
+ case STATE_EXECUTING: {
|
|
+ if (!this.isRepeatingTask()) {
|
|
+ return CancelledState.RUNNING;
|
|
+ }
|
|
+ if (STATE_EXECUTING == (curr = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_EXECUTING_CANCELLED))) {
|
|
+ return CancelledState.NEXT_RUNS_CANCELLED;
|
|
+ }
|
|
+ // try again
|
|
+ continue;
|
|
+ }
|
|
+ case STATE_EXECUTING_CANCELLED: {
|
|
+ return CancelledState.NEXT_RUNS_CANCELLED_ALREADY;
|
|
+ }
|
|
+ case STATE_FINISHED: {
|
|
+ return CancelledState.ALREADY_EXECUTED;
|
|
+ }
|
|
+ case STATE_CANCELLED: {
|
|
+ return CancelledState.CANCELLED_ALREADY;
|
|
+ }
|
|
+ default: {
|
|
+ throw new IllegalStateException("Unknown state: " + curr);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ExecutionState getExecutionState() {
|
|
+ final int state = this.getStateVolatile();
|
|
+ switch (state) {
|
|
+ case STATE_IDLE:
|
|
+ return ExecutionState.IDLE;
|
|
+ case STATE_EXECUTING:
|
|
+ return ExecutionState.RUNNING;
|
|
+ case STATE_EXECUTING_CANCELLED:
|
|
+ return ExecutionState.CANCELLED_RUNNING;
|
|
+ case STATE_FINISHED:
|
|
+ return ExecutionState.FINISHED;
|
|
+ case STATE_CANCELLED:
|
|
+ return ExecutionState.CANCELLED;
|
|
+ default: {
|
|
+ throw new IllegalStateException("Unknown state: " + state);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java
|
|
index e08f4e39db4ee3fed62e37364d17dcc5c5683504..7e92e14cdb9f3d895550991b2ea154a60f9c091b 100644
|
|
--- a/src/main/java/io/papermc/paper/util/CachedLists.java
|
|
+++ b/src/main/java/io/papermc/paper/util/CachedLists.java
|
|
@@ -9,49 +9,57 @@ import java.util.List;
|
|
public final class CachedLists {
|
|
|
|
// Paper start - optimise collisions
|
|
- static final UnsafeList<AABB> TEMP_COLLISION_LIST = new UnsafeList<>(1024);
|
|
- static boolean tempCollisionListInUse;
|
|
+ // Folia - region threading
|
|
|
|
public static UnsafeList<AABB> getTempCollisionList() {
|
|
- if (!Bukkit.isPrimaryThread() || tempCollisionListInUse) {
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData();
|
|
+ if (worldData == null) {
|
|
return new UnsafeList<>(16);
|
|
}
|
|
- tempCollisionListInUse = true;
|
|
- return TEMP_COLLISION_LIST;
|
|
+ return worldData.tempCollisionList.get();
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
public static void returnTempCollisionList(List<AABB> list) {
|
|
- if (list != TEMP_COLLISION_LIST) {
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData();
|
|
+ if (worldData == null) {
|
|
return;
|
|
}
|
|
- ((UnsafeList)list).setSize(0);
|
|
- tempCollisionListInUse = false;
|
|
+ worldData.tempCollisionList.ret(list);
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
- static final UnsafeList<Entity> TEMP_GET_ENTITIES_LIST = new UnsafeList<>(1024);
|
|
- static boolean tempGetEntitiesListInUse;
|
|
+ // Folia - region threading
|
|
|
|
public static UnsafeList<Entity> getTempGetEntitiesList() {
|
|
- if (!Bukkit.isPrimaryThread() || tempGetEntitiesListInUse) {
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData();
|
|
+ if (worldData == null) {
|
|
return new UnsafeList<>(16);
|
|
}
|
|
- tempGetEntitiesListInUse = true;
|
|
- return TEMP_GET_ENTITIES_LIST;
|
|
+ return worldData.tempEntitiesList.get();
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
public static void returnTempGetEntitiesList(List<Entity> list) {
|
|
- if (list != TEMP_GET_ENTITIES_LIST) {
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData();
|
|
+ if (worldData == null) {
|
|
return;
|
|
}
|
|
- ((UnsafeList)list).setSize(0);
|
|
- tempGetEntitiesListInUse = false;
|
|
+ worldData.tempEntitiesList.ret(list);
|
|
+ // Folia end - region threading
|
|
}
|
|
// Paper end - optimise collisions
|
|
|
|
public static void reset() {
|
|
- // Paper start - optimise collisions
|
|
- TEMP_COLLISION_LIST.completeReset();
|
|
- TEMP_GET_ENTITIES_LIST.completeReset();
|
|
- // Paper end - optimise collisions
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData();
|
|
+ if (worldData != null) {
|
|
+ worldData.resetCollisionLists();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
}
|
|
}
|
|
diff --git a/src/main/java/io/papermc/paper/util/CollisionUtil.java b/src/main/java/io/papermc/paper/util/CollisionUtil.java
|
|
index a87f6380b2c387fb0cdd40d5087b5c93492e3c88..77d369ca6be814d1f5a19ec37b44800529e6cda3 100644
|
|
--- a/src/main/java/io/papermc/paper/util/CollisionUtil.java
|
|
+++ b/src/main/java/io/papermc/paper/util/CollisionUtil.java
|
|
@@ -480,7 +480,12 @@ public final class CollisionUtil {
|
|
if (chunkProvider == null) {
|
|
chunk = (ChunkAccess)getter.getChunkForCollisions(currChunkX, currChunkZ);
|
|
} else {
|
|
+ // Folia start - ignore chunk if we do not own the region
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(chunkProvider.chunkMap.level, currChunkX, currChunkZ)) {
|
|
+ chunk = null;
|
|
+ } else { // Folia end - ignore chunk if we do not own the region
|
|
chunk = loadChunks ? chunkProvider.getChunk(currChunkX, currChunkZ, true) : chunkProvider.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ);
|
|
+ } // Folia - ignore chunk if we do not own the region
|
|
}
|
|
|
|
if (chunk == null) {
|
|
diff --git a/src/main/java/io/papermc/paper/util/CoordinateUtils.java b/src/main/java/io/papermc/paper/util/CoordinateUtils.java
|
|
index 413e4b6da027876dbbe8eb78f2568a440f431547..3a7dbcb9964723b8ed5e6b0a1ee4267923c746e4 100644
|
|
--- a/src/main/java/io/papermc/paper/util/CoordinateUtils.java
|
|
+++ b/src/main/java/io/papermc/paper/util/CoordinateUtils.java
|
|
@@ -5,6 +5,7 @@ import net.minecraft.core.SectionPos;
|
|
import net.minecraft.util.Mth;
|
|
import net.minecraft.world.entity.Entity;
|
|
import net.minecraft.world.level.ChunkPos;
|
|
+import net.minecraft.world.phys.Vec3;
|
|
|
|
public final class CoordinateUtils {
|
|
|
|
@@ -122,6 +123,31 @@ public final class CoordinateUtils {
|
|
return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54);
|
|
}
|
|
|
|
+ // TODO rebase
|
|
+ public static int getBlockX(final Vec3 pos) {
|
|
+ return Mth.floor(pos.x);
|
|
+ }
|
|
+
|
|
+ public static int getBlockY(final Vec3 pos) {
|
|
+ return Mth.floor(pos.y);
|
|
+ }
|
|
+
|
|
+ public static int getBlockZ(final Vec3 pos) {
|
|
+ return Mth.floor(pos.z);
|
|
+ }
|
|
+
|
|
+ public static int getChunkX(final Vec3 pos) {
|
|
+ return Mth.floor(pos.x) >> 4;
|
|
+ }
|
|
+
|
|
+ public static int getChunkY(final Vec3 pos) {
|
|
+ return Mth.floor(pos.y) >> 4;
|
|
+ }
|
|
+
|
|
+ public static int getChunkZ(final Vec3 pos) {
|
|
+ return Mth.floor(pos.z) >> 4;
|
|
+ }
|
|
+
|
|
private CoordinateUtils() {
|
|
throw new RuntimeException();
|
|
}
|
|
diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java
|
|
index c856a9a0d085b278da416c59996fc131811f790c..915cbf8c02c4bba0c62e5589229ee27e30535c07 100644
|
|
--- a/src/main/java/io/papermc/paper/util/MCUtil.java
|
|
+++ b/src/main/java/io/papermc/paper/util/MCUtil.java
|
|
@@ -29,6 +29,7 @@ import net.minecraft.world.level.ClipContext;
|
|
import net.minecraft.world.level.Level;
|
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
|
import net.minecraft.world.level.chunk.ChunkStatus;
|
|
+import net.minecraft.world.phys.Vec3;
|
|
import org.apache.commons.lang.exception.ExceptionUtils;
|
|
import com.mojang.authlib.GameProfile;
|
|
import org.bukkit.Location;
|
|
@@ -333,6 +334,7 @@ public final class MCUtil {
|
|
*/
|
|
public static void ensureMain(String reason, Runnable run) {
|
|
if (!isMainThread()) {
|
|
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
|
|
if (reason != null) {
|
|
MinecraftServer.LOGGER.warn("Asynchronous " + reason + "!", new IllegalStateException());
|
|
}
|
|
@@ -473,6 +475,30 @@ public final class MCUtil {
|
|
return new Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ());
|
|
}
|
|
|
|
+ // Folia start - TODO MERGE INTO MCUTIL
|
|
+ /**
|
|
+ * Converts a NMS World/Vector to Bukkit Location
|
|
+ * @param world
|
|
+ * @param pos
|
|
+ * @return
|
|
+ */
|
|
+ public static Location toLocation(Level world, Vec3 pos) {
|
|
+ return new Location(world.getWorld(), pos.x(), pos.y(), pos.z());
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Converts a NMS World/Vector to Bukkit Location
|
|
+ * @param world
|
|
+ * @param pos
|
|
+ * @param yaw
|
|
+ * @param pitch
|
|
+ * @return
|
|
+ */
|
|
+ public static Location toLocation(Level world, Vec3 pos, float yaw, float pitch) {
|
|
+ return new Location(world.getWorld(), pos.x(), pos.y(), pos.z(), yaw, pitch);
|
|
+ }
|
|
+ // Folia end - TODO MERGE INTO MCUTIL
|
|
+
|
|
/**
|
|
* Converts an NMS entity's current location to a Bukkit Location
|
|
* @param entity
|
|
diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java
|
|
index fc57850b80303fcade89ca95794f63910404a407..ed2c5099f73a4df5200d2ac490db712e8e299c96 100644
|
|
--- a/src/main/java/io/papermc/paper/util/TickThread.java
|
|
+++ b/src/main/java/io/papermc/paper/util/TickThread.java
|
|
@@ -1,8 +1,21 @@
|
|
package io.papermc.paper.util;
|
|
|
|
+import io.papermc.paper.threadedregions.RegionShutdownThread;
|
|
+import io.papermc.paper.threadedregions.RegionizedServer;
|
|
+import io.papermc.paper.threadedregions.RegionizedWorldData;
|
|
+import io.papermc.paper.threadedregions.ThreadedRegionizer;
|
|
+import io.papermc.paper.threadedregions.TickRegionScheduler;
|
|
+import io.papermc.paper.threadedregions.TickRegions;
|
|
+import net.minecraft.core.BlockPos;
|
|
import net.minecraft.server.MinecraftServer;
|
|
import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.server.level.ServerPlayer;
|
|
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
|
|
+import net.minecraft.util.Mth;
|
|
import net.minecraft.world.entity.Entity;
|
|
+import net.minecraft.world.level.ChunkPos;
|
|
+import net.minecraft.world.level.Level;
|
|
+import net.minecraft.world.phys.Vec3;
|
|
import org.bukkit.Bukkit;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
@@ -38,6 +51,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 +104,114 @@ public class TickThread extends Thread {
|
|
return Thread.currentThread() instanceof TickThread;
|
|
}
|
|
|
|
+ public static boolean isShutdownThread() {
|
|
+ return Thread.currentThread().getClass() == RegionShutdownThread.class;
|
|
+ }
|
|
+
|
|
+ public static boolean isTickThreadFor(final ServerLevel world, final BlockPos pos) {
|
|
+ return isTickThreadFor(world, pos.getX() >> 4, pos.getZ() >> 4);
|
|
+ }
|
|
+
|
|
+ public static boolean isTickThreadFor(final ServerLevel world, final ChunkPos pos) {
|
|
+ return isTickThreadFor(world, pos.x, pos.z);
|
|
+ }
|
|
+
|
|
+ public static boolean isTickThreadFor(final ServerLevel world, final Vec3 pos) {
|
|
+ return isTickThreadFor(world, Mth.floor(pos.x) >> 4, Mth.floor(pos.z) >> 4);
|
|
+ }
|
|
+
|
|
public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) {
|
|
- return Thread.currentThread() instanceof TickThread;
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region =
|
|
+ TickRegionScheduler.getCurrentRegion();
|
|
+ if (region == null) {
|
|
+ return isShutdownThread();
|
|
+ }
|
|
+ return world.regioniser.getRegionAtUnsynchronised(chunkX, chunkZ) == region;
|
|
+ }
|
|
+
|
|
+ public static boolean isTickThreadFor(final ServerLevel world, final Vec3 position, final Vec3 deltaMovement, final int buffer) {
|
|
+ final int fromChunkX = CoordinateUtils.getChunkX(position);
|
|
+ final int fromChunkZ = CoordinateUtils.getChunkZ(position);
|
|
+
|
|
+ final int toChunkX = CoordinateUtils.getChunkCoordinate(position.x + deltaMovement.x);
|
|
+ final int toChunkZ = CoordinateUtils.getChunkCoordinate(position.z + deltaMovement.z);
|
|
+
|
|
+ // expect from < to, but that may not be the case
|
|
+ return isTickThreadFor(
|
|
+ world,
|
|
+ Math.min(fromChunkX, toChunkX) - buffer,
|
|
+ Math.min(fromChunkZ, toChunkZ) - buffer,
|
|
+ Math.max(fromChunkX, toChunkX) + buffer,
|
|
+ Math.max(fromChunkZ, toChunkZ) + buffer
|
|
+ );
|
|
+ }
|
|
+
|
|
+ public static boolean isTickThreadFor(final ServerLevel world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) {
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region =
|
|
+ TickRegionScheduler.getCurrentRegion();
|
|
+ if (region == null) {
|
|
+ return isShutdownThread();
|
|
+ }
|
|
+
|
|
+ final int shift = world.regioniser.sectionChunkShift;
|
|
+
|
|
+ final int minSectionX = fromChunkX >> shift;
|
|
+ final int maxSectionX = toChunkX >> shift;
|
|
+ final int minSectionZ = fromChunkZ >> shift;
|
|
+ final int maxSectionZ = toChunkZ >> shift;
|
|
+
|
|
+ for (int secZ = minSectionZ; secZ <= maxSectionZ; ++secZ) {
|
|
+ for (int secX = minSectionX; secX <= maxSectionX; ++secX) {
|
|
+ final int lowerLeftCX = secX << shift;
|
|
+ final int lowerLeftCZ = secZ << shift;
|
|
+ if (world.regioniser.getRegionAtUnsynchronised(lowerLeftCX, lowerLeftCZ) != region) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) {
|
|
+ return isTickThreadFor(world, chunkX - radius, chunkZ - radius, chunkX + radius, chunkZ + radius);
|
|
}
|
|
|
|
public static boolean isTickThreadFor(final Entity entity) {
|
|
- return Thread.currentThread() instanceof TickThread;
|
|
+ if (entity == null) {
|
|
+ return true;
|
|
+ }
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region =
|
|
+ TickRegionScheduler.getCurrentRegion();
|
|
+ if (region == null) {
|
|
+ if (RegionizedServer.isGlobalTickThread()) {
|
|
+ if (entity instanceof ServerPlayer serverPlayer) {
|
|
+ return serverPlayer.connection == null;
|
|
+ } else {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ return isShutdownThread();
|
|
+ }
|
|
+
|
|
+ final Level level = entity.level;
|
|
+ if (level != region.regioniser.world) {
|
|
+ // world mismatch
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData();
|
|
+
|
|
+ // pass through the check if the entity is removed and we own its chunk
|
|
+ if (worldData.hasEntity(entity)) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ if (entity instanceof ServerPlayer serverPlayer) {
|
|
+ ServerGamePacketListenerImpl conn = serverPlayer.connection;
|
|
+ return conn != null && worldData.connections.contains(conn.connection);
|
|
+ } else {
|
|
+ return ((entity.hasNullCallback() || entity.isRemoved())) && isTickThreadFor((ServerLevel)level, entity.chunkPosition());
|
|
+ }
|
|
}
|
|
}
|
|
diff --git a/src/main/java/io/papermc/paper/util/set/LinkedSortedSet.java b/src/main/java/io/papermc/paper/util/set/LinkedSortedSet.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..cf9b66afc1762dbe2c625f09f9e804ca7dc0f128
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/util/set/LinkedSortedSet.java
|
|
@@ -0,0 +1,273 @@
|
|
+package io.papermc.paper.util.set;
|
|
+
|
|
+import java.util.Comparator;
|
|
+import java.util.Iterator;
|
|
+import java.util.NoSuchElementException;
|
|
+
|
|
+// TODO rebase into util patch
|
|
+public final class LinkedSortedSet<E> implements Iterable<E> {
|
|
+
|
|
+ public final Comparator<? super E> comparator;
|
|
+
|
|
+ protected Link<E> head;
|
|
+ protected Link<E> tail;
|
|
+
|
|
+ public LinkedSortedSet() {
|
|
+ this((Comparator)Comparator.naturalOrder());
|
|
+ }
|
|
+
|
|
+ public LinkedSortedSet(final Comparator<? super E> comparator) {
|
|
+ this.comparator = comparator;
|
|
+ }
|
|
+
|
|
+ public void clear() {
|
|
+ this.head = this.tail = null;
|
|
+ }
|
|
+
|
|
+ public boolean isEmpty() {
|
|
+ return this.head == null;
|
|
+ }
|
|
+
|
|
+ public E first() {
|
|
+ final Link<E> head = this.head;
|
|
+ return head == null ? null : head.element;
|
|
+ }
|
|
+
|
|
+ public E last() {
|
|
+ final Link<E> tail = this.tail;
|
|
+ return tail == null ? null : tail.element;
|
|
+ }
|
|
+
|
|
+ public boolean containsFirst(final E element) {
|
|
+ final Comparator<? super E> comparator = this.comparator;
|
|
+ for (Link<E> 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<? super E> comparator = this.comparator;
|
|
+ for (Link<E> curr = this.tail; curr != null; curr = curr.prev) {
|
|
+ if (comparator.compare(element, curr.element) == 0) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ private void removeNode(final Link<E> node) {
|
|
+ final Link<E> prev = node.prev;
|
|
+ final Link<E> 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<E> link) {
|
|
+ if (link.element == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ this.removeNode(link);
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ public boolean removeFirst(final E element) {
|
|
+ final Comparator<? super E> comparator = this.comparator;
|
|
+ for (Link<E> 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<? super E> comparator = this.comparator;
|
|
+ for (Link<E> 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<E> iterator() {
|
|
+ return new Iterator<>() {
|
|
+ private Link<E> next = LinkedSortedSet.this.head;
|
|
+
|
|
+ @Override
|
|
+ public boolean hasNext() {
|
|
+ return this.next != null;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public E next() {
|
|
+ final Link<E> next = this.next;
|
|
+ if (next == null) {
|
|
+ throw new NoSuchElementException();
|
|
+ }
|
|
+ this.next = next.next;
|
|
+ return next.element;
|
|
+ }
|
|
+ };
|
|
+ }
|
|
+
|
|
+ public E pollFirst() {
|
|
+ final Link<E> head = this.head;
|
|
+ if (head == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ final E ret = head.element;
|
|
+ final Link<E> 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<E> tail = this.tail;
|
|
+ if (tail == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ final E ret = tail.element;
|
|
+ final Link<E> 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<E> addLast(final E element) {
|
|
+ final Comparator<? super E> comparator = this.comparator;
|
|
+
|
|
+ Link<E> curr = this.tail;
|
|
+ if (curr != null) {
|
|
+ int compare;
|
|
+
|
|
+ while ((compare = comparator.compare(element, curr.element)) < 0) {
|
|
+ Link<E> 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<E> next = curr.next;
|
|
+ final Link<E> 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<E> addFirst(final E element) {
|
|
+ final Comparator<? super E> comparator = this.comparator;
|
|
+
|
|
+ Link<E> curr = this.head;
|
|
+ if (curr != null) {
|
|
+ int compare;
|
|
+
|
|
+ while ((compare = comparator.compare(element, curr.element)) > 0) {
|
|
+ Link<E> 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<E> prev = curr.prev;
|
|
+ final Link<E> 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<E> {
|
|
+ private E element;
|
|
+ private Link<E> prev;
|
|
+ private Link<E> next;
|
|
+
|
|
+ private Link() {}
|
|
+
|
|
+ private Link(final E element) {
|
|
+ this.element = element;
|
|
+ }
|
|
+
|
|
+ private Link(final E element, final Link<E> prev, final Link<E> 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 7b6b51392b123d34382233adcf4c3d4867bdaa32..4c793605566f1755c99496c00dc1ef4f38c4ac7d 100644
|
|
--- a/src/main/java/net/minecraft/commands/CommandSourceStack.java
|
|
+++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java
|
|
@@ -66,7 +66,7 @@ public class CommandSourceStack implements SharedSuggestionProvider, com.destroy
|
|
|
|
public CommandSourceStack(CommandSource output, Vec3 pos, Vec2 rot, ServerLevel world, int level, String name, Component displayName, MinecraftServer server, @Nullable Entity entity) {
|
|
this(output, pos, rot, world, level, name, displayName, server, entity, false, (commandcontext, flag, j) -> {
|
|
- }, EntityAnchorArgument.Anchor.FEET, CommandSigningContext.ANONYMOUS, TaskChainer.immediate(server));
|
|
+ }, EntityAnchorArgument.Anchor.FEET, CommandSigningContext.ANONYMOUS, TaskChainer.immediate((Runnable run) -> { io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run);})); // Folia - region threading
|
|
}
|
|
|
|
protected CommandSourceStack(CommandSource output, Vec3 pos, Vec2 rot, ServerLevel world, int level, String name, Component displayName, MinecraftServer server, @Nullable Entity entity, boolean silent, @Nullable ResultConsumer<CommandSourceStack> consumer, EntityAnchorArgument.Anchor entityAnchor, CommandSigningContext signedArguments, TaskChainer messageChainTaskQueue) {
|
|
diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java
|
|
index 87cc7562e4a166d078fe11b7f6980497fc0bd33e..e970a9807f1497b8b9f4155f558f4fcb719cb368 100644
|
|
--- a/src/main/java/net/minecraft/commands/Commands.java
|
|
+++ b/src/main/java/net/minecraft/commands/Commands.java
|
|
@@ -143,13 +143,13 @@ public class Commands {
|
|
AdvancementCommands.register(this.dispatcher);
|
|
AttributeCommand.register(this.dispatcher, commandRegistryAccess);
|
|
ExecuteCommand.register(this.dispatcher, commandRegistryAccess);
|
|
- BossBarCommands.register(this.dispatcher);
|
|
+ //BossBarCommands.register(this.dispatcher); // Folia - region threading - TODO
|
|
ClearInventoryCommands.register(this.dispatcher, commandRegistryAccess);
|
|
- CloneCommands.register(this.dispatcher, commandRegistryAccess);
|
|
+ //CloneCommands.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO
|
|
DamageCommand.register(this.dispatcher, commandRegistryAccess);
|
|
- DataCommands.register(this.dispatcher);
|
|
- DataPackCommand.register(this.dispatcher);
|
|
- DebugCommand.register(this.dispatcher);
|
|
+ //DataCommands.register(this.dispatcher); // Folia - region threading - TODO
|
|
+ //DataPackCommand.register(this.dispatcher); // Folia - region threading - TODO
|
|
+ //DebugCommand.register(this.dispatcher); // Folia - region threading - TODO
|
|
DefaultGameModeCommands.register(this.dispatcher);
|
|
DifficultyCommand.register(this.dispatcher);
|
|
EffectCommands.register(this.dispatcher, commandRegistryAccess);
|
|
@@ -159,45 +159,45 @@ public class Commands {
|
|
FillCommand.register(this.dispatcher, commandRegistryAccess);
|
|
FillBiomeCommand.register(this.dispatcher, commandRegistryAccess);
|
|
ForceLoadCommand.register(this.dispatcher);
|
|
- FunctionCommand.register(this.dispatcher);
|
|
+ //FunctionCommand.register(this.dispatcher); // Folia - region threading - TODO
|
|
GameModeCommand.register(this.dispatcher);
|
|
GameRuleCommand.register(this.dispatcher);
|
|
GiveCommand.register(this.dispatcher, commandRegistryAccess);
|
|
HelpCommand.register(this.dispatcher);
|
|
- ItemCommands.register(this.dispatcher, commandRegistryAccess);
|
|
+ //ItemCommands.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO later
|
|
KickCommand.register(this.dispatcher);
|
|
KillCommand.register(this.dispatcher);
|
|
ListPlayersCommand.register(this.dispatcher);
|
|
LocateCommand.register(this.dispatcher, commandRegistryAccess);
|
|
- LootCommand.register(this.dispatcher, commandRegistryAccess);
|
|
+ //LootCommand.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO later
|
|
MsgCommand.register(this.dispatcher);
|
|
ParticleCommand.register(this.dispatcher, commandRegistryAccess);
|
|
PlaceCommand.register(this.dispatcher);
|
|
PlaySoundCommand.register(this.dispatcher);
|
|
- ReloadCommand.register(this.dispatcher);
|
|
+ //ReloadCommand.register(this.dispatcher); // Folia - region threading
|
|
RecipeCommand.register(this.dispatcher);
|
|
- RideCommand.register(this.dispatcher);
|
|
+ //RideCommand.register(this.dispatcher); // Folia - region threading - TODO later
|
|
SayCommand.register(this.dispatcher);
|
|
- ScheduleCommand.register(this.dispatcher);
|
|
- ScoreboardCommand.register(this.dispatcher);
|
|
+ //ScheduleCommand.register(this.dispatcher); // Folia - region threading
|
|
+ //ScoreboardCommand.register(this.dispatcher); // Folia - region threading - TODO later
|
|
SeedCommand.register(this.dispatcher, environment != Commands.CommandSelection.INTEGRATED);
|
|
SetBlockCommand.register(this.dispatcher, commandRegistryAccess);
|
|
SetSpawnCommand.register(this.dispatcher);
|
|
SetWorldSpawnCommand.register(this.dispatcher);
|
|
- SpectateCommand.register(this.dispatcher);
|
|
- SpreadPlayersCommand.register(this.dispatcher);
|
|
+ //SpectateCommand.register(this.dispatcher); // Folia - region threading - TODO later
|
|
+ //SpreadPlayersCommand.register(this.dispatcher); // Folia - region threading - TODO later
|
|
StopSoundCommand.register(this.dispatcher);
|
|
SummonCommand.register(this.dispatcher, commandRegistryAccess);
|
|
- TagCommand.register(this.dispatcher);
|
|
- TeamCommand.register(this.dispatcher);
|
|
- TeamMsgCommand.register(this.dispatcher);
|
|
+ //TagCommand.register(this.dispatcher); // Folia - region threading - TODO later
|
|
+ //TeamCommand.register(this.dispatcher); // Folia - region threading - TODO later
|
|
+ //TeamMsgCommand.register(this.dispatcher); // Folia - region threading - TODO later
|
|
TeleportCommand.register(this.dispatcher);
|
|
TellRawCommand.register(this.dispatcher);
|
|
TimeCommand.register(this.dispatcher);
|
|
TitleCommand.register(this.dispatcher);
|
|
- TriggerCommand.register(this.dispatcher);
|
|
+ //TriggerCommand.register(this.dispatcher); // Folia - region threading - TODO later
|
|
WeatherCommand.register(this.dispatcher);
|
|
- WorldBorderCommand.register(this.dispatcher);
|
|
+ //WorldBorderCommand.register(this.dispatcher); // Folia - region threading - TODO later
|
|
if (JvmProfiler.INSTANCE.isAvailable()) {
|
|
JfrCommand.register(this.dispatcher);
|
|
}
|
|
@@ -215,8 +215,8 @@ public class Commands {
|
|
OpCommand.register(this.dispatcher);
|
|
PardonCommand.register(this.dispatcher);
|
|
PardonIpCommand.register(this.dispatcher);
|
|
- PerfCommand.register(this.dispatcher);
|
|
- SaveAllCommand.register(this.dispatcher);
|
|
+ //PerfCommand.register(this.dispatcher); // Folia - region threading - TODO later
|
|
+ //SaveAllCommand.register(this.dispatcher); // Folia - region threading - TODO later
|
|
SaveOffCommand.register(this.dispatcher);
|
|
SaveOnCommand.register(this.dispatcher);
|
|
SetPlayerIdleTimeoutCommand.register(this.dispatcher);
|
|
@@ -446,9 +446,12 @@ public class Commands {
|
|
}
|
|
// Paper start - Async command map building
|
|
new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent<CommandSourceStack>(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<String> bukkit, RootCommandNode<SharedSuggestionProvider> rootcommandnode) {
|
|
diff --git a/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java
|
|
index 309ad5a1da6b3a297d5526cd9247359ac5f49406..5a85fcbcd2966af95683106d4f459653983a28e6 100644
|
|
--- a/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java
|
|
+++ b/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java
|
|
@@ -33,7 +33,7 @@ public abstract class AbstractProjectileDispenseBehavior extends DefaultDispense
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) enumdirection.getStepX(), (double) ((float) enumdirection.getStepY() + 0.1F), (double) enumdirection.getStepZ()));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
|
|
index 958134519befadc27a5b647caf64acf272ee2db4..a0712ad55c8e02a88ddf55bb0e70e05dc1ddbcdc 100644
|
|
--- a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
|
|
+++ b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
|
|
@@ -58,7 +58,7 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d0, d1 + d3, d2));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java
|
|
index 1e6ba6d9cceda1d4867b183c3dbc03d317ed287f..de8cf0f0d34708b960f1c81cb10d813a797df02b 100644
|
|
--- a/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java
|
|
+++ b/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java
|
|
@@ -74,7 +74,7 @@ public class DefaultDispenseItemBehavior implements DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack);
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), CraftVector.toBukkit(entityitem.getDeltaMovement()));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
world.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java
|
|
index 9598aa381978194fee859721731196f0e6ee08fc..cb6fc325200483396d466bb694fa9217d8b21260 100644
|
|
--- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java
|
|
+++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java
|
|
@@ -222,7 +222,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
@@ -277,7 +277,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
@@ -333,7 +333,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
|
|
|
|
BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) list.get(0).getBukkitEntity());
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
world.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
@@ -389,7 +389,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
|
|
|
|
BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityhorseabstract.getBukkitEntity());
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
world.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
@@ -463,7 +463,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
|
|
|
|
BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityhorsechestedabstract.getBukkitEntity());
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
world.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
@@ -502,7 +502,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(enumdirection.getStepX(), enumdirection.getStepY(), enumdirection.getStepZ()));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
@@ -560,7 +560,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d3, d4, d5));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
@@ -634,7 +634,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
@@ -707,7 +707,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
@@ -754,7 +754,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
@@ -815,7 +815,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
@@ -833,7 +833,8 @@ public interface DispenseItemBehavior {
|
|
}
|
|
}
|
|
|
|
- worldserver.captureTreeGeneration = true;
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = worldserver.getCurrentWorldData(); // Folia - region threading
|
|
+ worldData.captureTreeGeneration = true; // Folia - region threading
|
|
// CraftBukkit end
|
|
|
|
if (!BoneMealItem.growCrop(stack, worldserver, blockposition) && !BoneMealItem.growWaterPlant(stack, worldserver, blockposition, (Direction) null)) {
|
|
@@ -842,13 +843,13 @@ public interface DispenseItemBehavior {
|
|
worldserver.levelEvent(1505, blockposition, 0);
|
|
}
|
|
// CraftBukkit start
|
|
- worldserver.captureTreeGeneration = false;
|
|
- if (worldserver.capturedBlockStates.size() > 0) {
|
|
- TreeType treeType = SaplingBlock.treeType;
|
|
- SaplingBlock.treeType = null;
|
|
+ worldData.captureTreeGeneration = false; // Folia - region threading
|
|
+ if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading
|
|
+ TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading
|
|
+ SaplingBlock.treeTypeRT.set(null); // Folia - region threading
|
|
Location location = new Location(worldserver.getWorld(), blockposition.getX(), blockposition.getY(), blockposition.getZ());
|
|
- List<org.bukkit.block.BlockState> blocks = new java.util.ArrayList<>(worldserver.capturedBlockStates.values());
|
|
- worldserver.capturedBlockStates.clear();
|
|
+ List<org.bukkit.block.BlockState> blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading
|
|
+ worldData.capturedBlockStates.clear(); // Folia - region threading
|
|
StructureGrowEvent structureEvent = null;
|
|
if (treeType != null) {
|
|
structureEvent = new StructureGrowEvent(location, treeType, false, null, blocks);
|
|
@@ -883,7 +884,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
@@ -940,7 +941,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
@@ -989,7 +990,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
@@ -1062,7 +1063,7 @@ public interface DispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - only single item in event
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java
|
|
index d1127d93a85a837933d0d73c24cacac4adc3a5b9..ac9f4f2ac817e5fe9a15759c549a57ad8473b6ac 100644
|
|
--- a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java
|
|
+++ b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java
|
|
@@ -40,7 +40,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
worldserver.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java
|
|
index 0159ed9cbc644c39fa79e62327f13375193fdc98..a930c8eb64d6c7044646d6b0156e202ea334a1f9 100644
|
|
--- a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java
|
|
+++ b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java
|
|
@@ -37,7 +37,7 @@ public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ()));
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
pointer.getLevel().getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
|
|
index fa1d325034dafdb9f1da546a6f9c5e88d2b67749..61bcbcacc1718b66f5b93548b1777c536cf3916b 100644
|
|
--- a/src/main/java/net/minecraft/network/Connection.java
|
|
+++ b/src/main/java/net/minecraft/network/Connection.java
|
|
@@ -75,7 +75,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
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<Connection.PacketHolder> queue = Queues.newConcurrentLinkedQueue();
|
|
+ private final Queue<Connection.PacketHolder> queue = new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>();
|
|
public Channel channel;
|
|
public SocketAddress address;
|
|
// Spigot Start
|
|
@@ -83,7 +83,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
public com.mojang.authlib.properties.Property[] spoofedProfile;
|
|
public boolean preparing = true;
|
|
// Spigot End
|
|
- private PacketListener packetListener;
|
|
+ private volatile PacketListener packetListener; // Folia - region threading
|
|
private Component disconnectedReason;
|
|
private boolean encrypted;
|
|
private boolean disconnectionHandled;
|
|
@@ -179,6 +179,32 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
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<DisconnectReq> disconnectReqs =
|
|
+ new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>();
|
|
+
|
|
+ /**
|
|
+ * Safely disconnects the connection while possibly on another thread. Note: This call will not block, even if on the
|
|
+ * same thread that could disconnect.
|
|
+ */
|
|
+ public final void disconnectSafely(Component disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
|
|
+ this.disconnectReqs.add(new DisconnectReq(disconnectReason, cause));
|
|
+ // We can't halt packet processing here because a plugin could cancel a kick request.
|
|
+ }
|
|
+
|
|
+ public final boolean isPlayerConnected() {
|
|
+ return this.packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
public void channelActive(ChannelHandlerContext channelhandlercontext) throws Exception {
|
|
super.channelActive(channelhandlercontext);
|
|
this.channel = channelhandlercontext.channel();
|
|
@@ -193,6 +219,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
Connection.LOGGER.error(LogUtils.FATAL_MARKER, "Failed to change protocol to handshake", throwable);
|
|
}
|
|
|
|
+ this.becomeActive = true; // Folia - region threading
|
|
}
|
|
|
|
public void setProtocol(ConnectionProtocol state) {
|
|
@@ -375,13 +402,6 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
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<Packet> extraPackets = InnerUtil.buildExtraPackets(packet);
|
|
boolean hasExtraPackets = extraPackets != null && !extraPackets.isEmpty();
|
|
@@ -503,66 +523,58 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
|
|
// 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<PacketHolder> 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<PacketHolder>)this.queue).pollIf((PacketHolder h) -> {
|
|
+ return h.packet.isReady();
|
|
+ });
|
|
+ }
|
|
+ if (holder == null) {
|
|
+ break;
|
|
+ }
|
|
+ justFlushed = (!this.canWritePackets() && (needsFlush || this.canFlush));
|
|
+ this.sendPacket(holder.packet, holder.listener, justFlushed ? Boolean.TRUE : Boolean.FALSE); // Paper - make only one flush call per sendPacketQueue() call
|
|
+ }
|
|
+
|
|
+ if (!justFlushed) {
|
|
this.flush();
|
|
}
|
|
- // Paper end - make only one flush call per sendPacketQueue() call
|
|
- return false;
|
|
- } else {
|
|
- iterator.remove();
|
|
- if (queued.tryMarkConsumed()) { // Paper - try to mark isConsumed flag for de-duplicating packet
|
|
- this.sendPacket(packet, queued.listener, (!iterator.hasNext() && (needsFlush || this.canFlush)) ? Boolean.TRUE : Boolean.FALSE); // Paper - make only one flush call per sendPacketQueue() call
|
|
- hasWrotePacket = true; // Paper - make only one flush call per sendPacketQueue() call
|
|
+ } finally {
|
|
+ if (!set) {
|
|
+ this.flushingQueue.set(false);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
- } finally { // Paper start - add pending task queue
|
|
- Runnable r;
|
|
- while ((r = this.pendingTasks.poll()) != null) {
|
|
- this.channel.eventLoop().execute(r);
|
|
- }
|
|
- } // Paper end - add pending task queue
|
|
}
|
|
// Paper end
|
|
|
|
@@ -571,21 +583,41 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
private static int currTick; // Paper
|
|
public void tick() {
|
|
this.flushQueue();
|
|
- // Paper start
|
|
- if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) {
|
|
- Connection.currTick = net.minecraft.server.MinecraftServer.currentTick;
|
|
- Connection.joinAttemptsThisTick = 0;
|
|
+ // Folia start - region threading
|
|
+ // handle disconnect requests, but only after flushQueue()
|
|
+ DisconnectReq disconnectReq;
|
|
+ while ((disconnectReq = this.disconnectReqs.poll()) != null) {
|
|
+ PacketListener packetlistener = this.packetListener;
|
|
+
|
|
+ if (packetlistener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) {
|
|
+ loginPacketListener.disconnect(disconnectReq.disconnectReason);
|
|
+ // this doesn't fail, so abort any further attempts
|
|
+ return;
|
|
+ } else if (packetlistener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl gamePacketListener) {
|
|
+ gamePacketListener.disconnect(disconnectReq.disconnectReason, disconnectReq.cause);
|
|
+ // may be cancelled by a plugin, if not cancelled then any further calls do nothing
|
|
+ continue;
|
|
+ } else {
|
|
+ // no idea what packet to send
|
|
+ this.disconnect(disconnectReq.disconnectReason);
|
|
+ this.setReadOnly();
|
|
+ return;
|
|
+ }
|
|
}
|
|
- // Paper end
|
|
+ if (!this.isConnected()) {
|
|
+ // disconnected from above
|
|
+ this.handleDisconnection();
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+ // Folia - this is broken
|
|
PacketListener packetlistener = this.packetListener;
|
|
|
|
if (packetlistener instanceof TickablePacketListener) {
|
|
TickablePacketListener tickablepacketlistener = (TickablePacketListener) packetlistener;
|
|
|
|
// Paper start - limit the number of joins which can be processed each tick
|
|
- if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener)
|
|
- || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT
|
|
- || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) {
|
|
+ if (true) { // Folia - region threading
|
|
// Paper start - detailed watchdog information
|
|
net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener);
|
|
try { // Paper end - detailed watchdog information
|
|
@@ -625,13 +657,21 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
// Paper start
|
|
public void clearPacketQueue() {
|
|
net.minecraft.server.level.ServerPlayer player = getPlayer();
|
|
- queue.forEach(queuedPacket -> {
|
|
+ java.util.List<Connection.PacketHolder> queuedPackets = new java.util.ArrayList<>();
|
|
+ // synchronise so that flushQueue does not poll values while the queue is being cleared
|
|
+ synchronized (this.queue) {
|
|
+ Connection.PacketHolder packetHolder;
|
|
+ while ((packetHolder = this.queue.poll()) != null) {
|
|
+ queuedPackets.add(packetHolder);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for (Connection.PacketHolder queuedPacket : queuedPackets) {
|
|
Packet<?> packet = queuedPacket.packet;
|
|
if (packet.hasFinishListener()) {
|
|
packet.onPacketDispatchFinish(player, null);
|
|
}
|
|
- });
|
|
- queue.clear();
|
|
+ }
|
|
}
|
|
// Paper end
|
|
public void disconnect(Component disconnectReason) {
|
|
@@ -643,6 +683,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
this.channel.close(); // We can't wait as this may be called from an event loop.
|
|
this.disconnectedReason = disconnectReason;
|
|
}
|
|
+ this.becomeActive = true; // Folia - region threading
|
|
|
|
}
|
|
|
|
@@ -802,13 +843,27 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
final net.minecraft.server.network.ServerGamePacketListenerImpl playerConnection = (net.minecraft.server.network.ServerGamePacketListenerImpl) packetListener;
|
|
new com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent(playerConnection.player.getUUID(),
|
|
playerConnection.player.getScoreboardName(), ((java.net.InetSocketAddress)address).getAddress(), false).callEvent();
|
|
+ // Note: It can be in the connection set if it is in ready to accept if handleAcceptedLogin fails
|
|
+ // Folia start - region threading
|
|
+ net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection(
|
|
+ playerConnection.player.getScoreboardName(),
|
|
+ playerConnection.player.getUUID(), this
|
|
+ );
|
|
+ // Folia end - region threading
|
|
} else if (packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl) {
|
|
/* Player is login stage */
|
|
final net.minecraft.server.network.ServerLoginPacketListenerImpl loginListener = (net.minecraft.server.network.ServerLoginPacketListenerImpl) packetListener;
|
|
- switch (loginListener.state) {
|
|
- case READY_TO_ACCEPT:
|
|
- case DELAY_ACCEPT:
|
|
- case ACCEPTED:
|
|
+ // Folia start - region threading
|
|
+ if (loginListener.state.ordinal() >= net.minecraft.server.network.ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT.ordinal()) {
|
|
+ // Note: It can be in the connection set if it is in ready to accept if handleAcceptedLogin fails
|
|
+ net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection(
|
|
+ loginListener.gameProfile.getName(),
|
|
+ net.minecraft.core.UUIDUtil.getOrCreatePlayerUUID(loginListener.gameProfile),
|
|
+ this
|
|
+ );
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+ if (loginListener.state.ordinal() >= net.minecraft.server.network.ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT.ordinal()) { // Folia - region threading - rewrite login process
|
|
final com.mojang.authlib.GameProfile profile = loginListener.gameProfile; /* Should be non-null at this stage */
|
|
new com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent(profile.getId(), profile.getName(),
|
|
((java.net.InetSocketAddress)address).getAddress(), false).callEvent();
|
|
diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java
|
|
index d2f0a0755317f5fa9a1ccf7db346aa77fd287d80..b07df826a3028c14b48b09dbaeccc9078d7cc992 100644
|
|
--- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java
|
|
+++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java
|
|
@@ -2,6 +2,7 @@ package net.minecraft.network.protocol;
|
|
|
|
import com.mojang.logging.LogUtils;
|
|
import net.minecraft.network.PacketListener;
|
|
+import net.minecraft.server.level.ServerPlayer;
|
|
import org.slf4j.Logger;
|
|
|
|
// CraftBukkit start
|
|
@@ -41,7 +42,7 @@ public class PacketUtils {
|
|
|
|
public static <T extends PacketListener> void ensureRunningOnSameThread(Packet<T> packet, T listener, BlockableEventLoop<?> engine) throws RunningOnDifferentThreadException {
|
|
if (!engine.isSameThread()) {
|
|
- engine.execute(() -> { // Paper - Fix preemptive player kick on a server shutdown.
|
|
+ Runnable run = () -> { // Folia - region threading
|
|
packetProcessing.push(listener); // Paper - detailed watchdog information
|
|
try { // Paper - detailed watchdog information
|
|
if (MinecraftServer.getServer().hasStopped() || (listener instanceof ServerGamePacketListenerImpl && ((ServerGamePacketListenerImpl) listener).processedDisconnect)) return; // CraftBukkit, MC-142590
|
|
@@ -66,7 +67,17 @@ public class PacketUtils {
|
|
}
|
|
// Paper end - detailed watchdog information
|
|
|
|
- });
|
|
+ }; // Folia start - region threading
|
|
+ ServerGamePacketListenerImpl actualListener = (ServerGamePacketListenerImpl)listener;
|
|
+ // ignore retired state, if removed then we don't want the packet to be handled
|
|
+ actualListener.player.getBukkitEntity().taskScheduler.schedule(
|
|
+ (ServerPlayer player) -> {
|
|
+ run.run();
|
|
+ },
|
|
+ null,
|
|
+ 1L
|
|
+ );
|
|
+ // Folia end - region threading
|
|
throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD;
|
|
// CraftBukkit start - SPIGOT-5477, MC-142590
|
|
} else if (MinecraftServer.getServer().hasStopped() || (listener instanceof ServerGamePacketListenerImpl && ((ServerGamePacketListenerImpl) listener).processedDisconnect)) {
|
|
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
|
|
index 8ccc92836bfda8e00eea649430174dcd5664af70..26350a185cadbb64696b2a1ac88b1a2be06fd587 100644
|
|
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
|
|
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
|
|
@@ -294,7 +294,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
public org.bukkit.command.ConsoleCommandSender console;
|
|
public org.bukkit.command.RemoteConsoleCommandSender remoteConsole;
|
|
//public ConsoleReader reader; // Paper
|
|
- public static int currentTick = 0; // Paper - Further improve tick loop
|
|
+ //public static int currentTick = 0; // Paper - Further improve tick loop // Folia - region threading
|
|
public java.util.Queue<Runnable> processQueue = new java.util.concurrent.ConcurrentLinkedQueue<Runnable>();
|
|
public int autosavePeriod;
|
|
public Commands vanillaCommandDispatcher;
|
|
@@ -307,12 +307,40 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
public final double[] recentTps = new double[ 3 ];
|
|
// Spigot end
|
|
public final io.papermc.paper.configuration.PaperConfigurations paperConfigurations;
|
|
- public static long currentTickLong = 0L; // Paper
|
|
+ //public static long currentTickLong = 0L; // Paper // Folia - threaded regions
|
|
|
|
public volatile Thread shutdownThread; // Paper
|
|
public volatile boolean abnormalExit = false; // Paper
|
|
public boolean isIteratingOverLevels = false; // Paper
|
|
|
|
+ // Folia start - regionised ticking
|
|
+ public final io.papermc.paper.threadedregions.RegionizedServer regionizedServer = new io.papermc.paper.threadedregions.RegionizedServer();
|
|
+
|
|
+ @Override
|
|
+ public void execute(Runnable runnable) {
|
|
+ if (true) {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+ super.execute(runnable);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void executeBlocking(Runnable runnable) {
|
|
+ if (true) {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+ super.executeBlocking(runnable);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void tell(TickTask runnable) {
|
|
+ if (true) {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+ super.tell(runnable);
|
|
+ }
|
|
+ // Folia end - regionised ticking
|
|
+
|
|
public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
|
|
AtomicReference<S> atomicreference = new AtomicReference();
|
|
Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system
|
|
@@ -604,7 +632,21 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
worlddata.setModdedInfo(this.getServerModName(), this.getModdedStatus().shouldReportAsModified());
|
|
this.addLevel(world); // Paper - move up
|
|
- this.initWorld(world, worlddata, worldData, worldoptions);
|
|
+ // Folia start - region threading
|
|
+ // the spawn should be within ~32 blocks, so we force add ticket levels to ensure the first thread
|
|
+ // to init spawn will not run into any ownership issues
|
|
+ // move init to start of tickServer
|
|
+ int loadRegionRadius = ((32) >> 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);
|
|
@@ -616,6 +658,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
this.forceDifficulty();
|
|
for (ServerLevel worldserver : this.getAllLevels()) {
|
|
this.prepareLevels(worldserver.getChunkSource().chunkMap.progressListener, worldserver);
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addWorld(worldserver); // Folia - region threading
|
|
//worldserver.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API // Paper - rewrite chunk system, not required to "tick" anything
|
|
this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(worldserver.getWorld()));
|
|
}
|
|
@@ -683,7 +726,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
worldProperties.setSpawn(BlockPos.ZERO.above(80), 0.0F);
|
|
} else {
|
|
ServerChunkCache chunkproviderserver = world.getChunkSource();
|
|
- ChunkPos chunkcoordintpair = new ChunkPos(chunkproviderserver.randomState().sampler().findSpawnPosition());
|
|
+ ChunkPos chunkcoordintpair = world.randomSpawnSelection; // Folia - region threading
|
|
// CraftBukkit start
|
|
if (world.generator != null) {
|
|
Random rand = new Random(world.getSeed());
|
|
@@ -704,6 +747,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
if (i < world.getMinBuildHeight()) {
|
|
BlockPos blockposition = chunkcoordintpair.getWorldPosition();
|
|
|
|
+ world.getChunk(blockposition.offset(8, 0, 8)); // Folia - region threading - sync load first
|
|
i = world.getHeight(Heightmap.Types.WORLD_SURFACE, blockposition.getX() + 8, blockposition.getZ() + 8);
|
|
}
|
|
|
|
@@ -785,7 +829,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
// Paper end
|
|
|
|
// this.nextTickTime = SystemUtils.getMillis() + 10L;
|
|
- this.executeModerately();
|
|
+ //this.executeModerately(); // Folia - region threading
|
|
// Iterator iterator = this.levels.values().iterator();
|
|
}
|
|
|
|
@@ -808,7 +852,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
// CraftBukkit start
|
|
// this.nextTickTime = SystemUtils.getMillis() + 10L;
|
|
- this.executeModerately();
|
|
+ //this.executeModerately(); // Folia - region threading
|
|
// CraftBukkit end
|
|
if (worldserver.getWorld().getKeepSpawnInMemory()) worldloadlistener.stop(); // Paper
|
|
chunkproviderserver.getLightEngine().setTaskPerBatch(worldserver.paperConfig().misc.lightQueueSize); // Paper - increase light queue size
|
|
@@ -913,7 +957,37 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
// CraftBukkit end
|
|
|
|
+ // Folia start - region threading
|
|
+ private final java.util.concurrent.atomic.AtomicBoolean hasStartedShutdownThread = new java.util.concurrent.atomic.AtomicBoolean();
|
|
+
|
|
+ private void haltServerRegionThreading() {
|
|
+ if (this.hasStartedShutdownThread.getAndSet(true)) {
|
|
+ // already started shutdown
|
|
+ return;
|
|
+ }
|
|
+ new io.papermc.paper.threadedregions.RegionShutdownThread("Region shutdown thread").start();
|
|
+ }
|
|
+
|
|
+ public void haltCurrentRegion() {
|
|
+ if (!io.papermc.paper.util.TickThread.isShutdownThread()) {
|
|
+ throw new IllegalStateException();
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
public void stopServer() {
|
|
+ // Folia start - region threading
|
|
+ // halt scheduler
|
|
+ // don't wait, we may be on a scheduler thread
|
|
+ io.papermc.paper.threadedregions.TickRegions.getScheduler().halt(false, 0L);
|
|
+ // cannot run shutdown logic on this thread, as it may be a scheduler
|
|
+ if (true) {
|
|
+ if (!io.papermc.paper.util.TickThread.isShutdownThread()) {
|
|
+ this.haltServerRegionThreading();
|
|
+ return;
|
|
+ } // else: fall through to regular stop logic
|
|
+ }
|
|
+ // Folia end - region threading
|
|
// CraftBukkit start - prevent double stopping on multiple threads
|
|
synchronized(this.stopLock) {
|
|
if (this.hasStopped) return;
|
|
@@ -923,7 +997,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
// Paper start - kill main thread, and kill it hard
|
|
shutdownThread = Thread.currentThread();
|
|
org.spigotmc.WatchdogThread.doStop(); // Paper
|
|
- if (!isSameThread()) {
|
|
+ if (false && !isSameThread()) { // Folia - region threading
|
|
MinecraftServer.LOGGER.info("Stopping main thread (Ignore any thread death message you see! - DO NOT REPORT THREAD DEATH TO PAPER)");
|
|
while (this.getRunningThread().isAlive()) {
|
|
this.getRunningThread().stop();
|
|
@@ -953,12 +1027,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
this.isSaving = true;
|
|
if (this.playerList != null) {
|
|
- MinecraftServer.LOGGER.info("Saving players");
|
|
- this.playerList.saveAll();
|
|
+ //MinecraftServer.LOGGER.info("Saving players"); // Folia - move to shutdown thread logic
|
|
+ //this.playerList.saveAll(); // Folia - move to shutdown thread logic
|
|
this.playerList.removeAll(this.isRestarting); // Paper
|
|
try { Thread.sleep(100); } catch (InterruptedException ex) {} // CraftBukkit - SPIGOT-625 - give server at least a chance to send packets
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ if (true) {
|
|
+ // the rest till part 2 is handled by the region shutdown thread
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
MinecraftServer.LOGGER.info("Saving worlds");
|
|
Iterator iterator = this.getAllLevels().iterator();
|
|
|
|
@@ -974,6 +1055,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
this.saveAllChunks(false, true, false, true); // Paper - rewrite chunk system - move closing into here
|
|
|
|
this.isSaving = false;
|
|
+ // Folia start - region threading
|
|
+ this.stopPart2();
|
|
+ }
|
|
+ public void stopPart2() {
|
|
+ // Folia end - region threading
|
|
this.resources.close();
|
|
|
|
try {
|
|
@@ -1027,6 +1113,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
if (isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Server stopped"); // Paper
|
|
// Paper end
|
|
this.running = false;
|
|
+ this.stopServer(); // Folia - region threading
|
|
if (waitForShutdown) {
|
|
try {
|
|
this.serverThread.join();
|
|
@@ -1108,6 +1195,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
this.statusIcon = (ServerStatus.Favicon) this.loadStatusIcon().orElse(null); // CraftBukkit - decompile error
|
|
this.status = this.buildServerStatus();
|
|
|
|
+ // Folia start - region threading
|
|
+ if (true) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().init(); // Folia - region threading - only after loading worlds
|
|
+ String doneTime = String.format(java.util.Locale.ROOT, "%.3fs", (double) (Util.getNanos() - serverStartTime) / 1.0E9D);
|
|
+ LOGGER.info("Done ({})! For help, type \"help\"", doneTime);
|
|
+ for (;;) {
|
|
+ try {
|
|
+ Thread.sleep(Long.MAX_VALUE);
|
|
+ } catch (final InterruptedException ex) {}
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
// Spigot start
|
|
// Paper start - move done tracking
|
|
LOGGER.info("Running delayed init tasks");
|
|
@@ -1143,8 +1243,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
this.lastOverloadWarning = this.nextTickTime;
|
|
}
|
|
|
|
- ++MinecraftServer.currentTickLong; // Paper
|
|
- if ( ++MinecraftServer.currentTick % MinecraftServer.SAMPLE_INTERVAL == 0 )
|
|
+ //++MinecraftServer.currentTickLong; // Paper // Folia - threaded regions
|
|
+ if ( false ) // Folia - region threading
|
|
{
|
|
final long diff = curTime - tickSection;
|
|
java.math.BigDecimal currentTps = TPS_BASE.divide(new java.math.BigDecimal(diff), 30, java.math.RoundingMode.HALF_UP);
|
|
@@ -1170,7 +1270,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
this.nextTickTime += 50L;
|
|
this.startMetricsRecordingTick();
|
|
this.profiler.push("tick");
|
|
- this.tickServer(this::haveTime);
|
|
+ this.tickServer(curTime, this.nextTickTime, 0L, null); // Folia - region threading
|
|
this.profiler.popPush("nextTickWait");
|
|
this.mayHaveDelayedTasks = true;
|
|
this.delayedTasksMaxNextTickTime = Math.max(Util.getMillis() + 50L, this.nextTickTime);
|
|
@@ -1308,6 +1408,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
@Override
|
|
public boolean pollTask() {
|
|
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
|
|
boolean flag = this.pollTaskInternal();
|
|
|
|
this.mayHaveDelayedTasks = flag;
|
|
@@ -1315,6 +1416,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
|
|
private boolean pollTaskInternal() {
|
|
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
|
|
if (super.pollTask()) {
|
|
this.executeMidTickTasks(); // Paper - execute chunk tasks mid tick
|
|
return true;
|
|
@@ -1337,6 +1439,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
|
|
public void doRunTask(TickTask ticktask) { // CraftBukkit - decompile error
|
|
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
|
|
this.getProfiler().incrementCounter("runTask");
|
|
super.doRunTask(ticktask);
|
|
}
|
|
@@ -1379,22 +1482,64 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
public void onServerExit() {}
|
|
|
|
- public void tickServer(BooleanSupplier shouldKeepTicking) {
|
|
+ // Folia start - region threading
|
|
+ public void tickServer(long startTime, long scheduledEnd, long targetBuffer,
|
|
+ io.papermc.paper.threadedregions.TickRegions.TickRegionData region) {
|
|
+ if (region != null) {
|
|
+ region.world.getCurrentWorldData().updateTickData();
|
|
+ if (region.world.checkInitialised.get() != ServerLevel.WORLD_INIT_CHECKED) {
|
|
+ synchronized (region.world.checkInitialised) {
|
|
+ if (region.world.checkInitialised.compareAndSet(ServerLevel.WORLD_INIT_NOT_CHECKED, ServerLevel.WORLD_INIT_CHECKING)) {
|
|
+ LOGGER.info("Initialising world '" + region.world.getWorld().getName() + "' before it can be ticked...");
|
|
+ this.initWorld(region.world, region.world.serverLevelData, worldData, region.world.serverLevelData.worldGenOptions()); // Folia - delayed until first tick of world
|
|
+ region.world.checkInitialised.set(ServerLevel.WORLD_INIT_CHECKED);
|
|
+ LOGGER.info("Initialised world '" + region.world.getWorld().getName() + "'");
|
|
+ } // else: must be checked
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ BooleanSupplier shouldKeepTicking = () -> {
|
|
+ return scheduledEnd - System.nanoTime() > targetBuffer;
|
|
+ };
|
|
+ new com.destroystokyo.paper.event.server.ServerTickStartEvent((int)region.getCurrentTick()).callEvent(); // Paper
|
|
+ // Folia end - region threading
|
|
co.aikar.timings.TimingsManager.FULL_SERVER_TICK.startTiming(); // Paper
|
|
- long i = Util.getNanos();
|
|
+ long i = startTime; // Folia - region threading
|
|
|
|
// Paper start - move oversleep into full server tick
|
|
+ if (region == null) { // Folia - region threading
|
|
isOversleep = true;MinecraftTimings.serverOversleep.startTiming();
|
|
this.managedBlock(() -> {
|
|
return !this.canOversleep();
|
|
});
|
|
isOversleep = false;MinecraftTimings.serverOversleep.stopTiming();
|
|
+ } // Folia - region threading
|
|
// Paper end
|
|
- new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper
|
|
+ // Folia - region threading - move up
|
|
+
|
|
+ // Folia start - region threading
|
|
+ if (region != null) {
|
|
+ region.getTaskQueueData().drainTasks();
|
|
+ ((io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler)Bukkit.getRegionScheduler()).tick();
|
|
+ // now run all the entity schedulers
|
|
+ // TODO there has got to be a more efficient variant of this crap
|
|
+ for (Entity entity : region.world.getCurrentWorldData().getLocalEntitiesCopy()) {
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(entity) || entity.isRemoved()) {
|
|
+ continue;
|
|
+ }
|
|
+ org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw();
|
|
+ if (bukkit != null) {
|
|
+ bukkit.taskScheduler.executeTick();
|
|
+ }
|
|
+ }
|
|
+ // now tick connections
|
|
+ region.world.getCurrentWorldData().tickConnections(); // Folia - region threading
|
|
+ }
|
|
+ // Folia end - region threading
|
|
|
|
++this.tickCount;
|
|
- this.tickChildren(shouldKeepTicking);
|
|
- if (i - this.lastServerStatus >= 5000000000L) {
|
|
+ this.tickChildren(shouldKeepTicking, region); // Folia - region threading
|
|
+ if (region == null && i - this.lastServerStatus >= 5000000000L) { // Folia - region threading - moved to global tick
|
|
this.lastServerStatus = i;
|
|
this.status = this.buildServerStatus();
|
|
}
|
|
@@ -1411,9 +1556,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
if (playerSaveInterval > 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 {
|
|
@@ -1423,16 +1568,17 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
// Paper end
|
|
io.papermc.paper.util.CachedLists.reset(); // Paper
|
|
// Paper start - move executeAll() into full server tick timing
|
|
- try (co.aikar.timings.Timing ignored = MinecraftTimings.processTasksTimer.startTiming()) {
|
|
+ if (region == null) try (co.aikar.timings.Timing ignored = MinecraftTimings.processTasksTimer.startTiming()) { // Folia - region threading
|
|
this.runAllTasks();
|
|
}
|
|
// Paper end
|
|
// Paper start
|
|
long endTime = System.nanoTime();
|
|
- long remaining = (TICK_TIME - (endTime - lastTick)) - catchupTime;
|
|
- new com.destroystokyo.paper.event.server.ServerTickEndEvent(this.tickCount, ((double)(endTime - lastTick) / 1000000D), remaining).callEvent();
|
|
+ long remaining = scheduledEnd - endTime; // Folia - region ticking
|
|
+ new com.destroystokyo.paper.event.server.ServerTickEndEvent(this.tickCount, ((double)(endTime - startTime) / 1000000D), remaining).callEvent(); // Folia - region ticking
|
|
// Paper end
|
|
this.profiler.push("tallying");
|
|
+ if (region == null) { // Folia - region threading
|
|
long j = this.tickTimes[this.tickCount % 100] = Util.getNanos() - i;
|
|
|
|
this.averageTickTime = this.averageTickTime * 0.8F + (float) j / 1000000.0F * 0.19999999F;
|
|
@@ -1444,11 +1590,18 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
tickTimes60s.add(this.tickCount, j);
|
|
// Paper end
|
|
this.frameTimer.logFrameDuration(k - i);
|
|
+ } // Folia - region threading
|
|
this.profiler.pop();
|
|
org.spigotmc.WatchdogThread.tick(); // Spigot
|
|
co.aikar.timings.TimingsManager.FULL_SERVER_TICK.stopTiming(); // Paper
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ public void rebuildServerStatus() {
|
|
+ this.status = this.buildServerStatus();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
private ServerStatus buildServerStatus() {
|
|
ServerStatus.Players serverping_serverpingplayersample = this.buildPlayerStatus();
|
|
|
|
@@ -1456,7 +1609,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
|
|
private ServerStatus.Players buildPlayerStatus() {
|
|
- List<ServerPlayer> list = this.playerList.getPlayers();
|
|
+ List<ServerPlayer> list = new java.util.ArrayList<>(this.playerList.getPlayers()); // Folia - region threading
|
|
int i = this.getMaxPlayers();
|
|
|
|
if (this.hidesOnlinePlayers()) {
|
|
@@ -1477,14 +1630,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
}
|
|
|
|
- public void tickChildren(BooleanSupplier shouldKeepTicking) {
|
|
+ public void tickChildren(BooleanSupplier shouldKeepTicking, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - region threading
|
|
MinecraftTimings.bukkitSchedulerTimer.startTiming(); // Spigot // Paper
|
|
- this.server.getScheduler().mainThreadHeartbeat(this.tickCount); // CraftBukkit
|
|
+ if (region == null) this.server.getScheduler().mainThreadHeartbeat(this.tickCount); // CraftBukkit // Folia - region threading - TODO REPLACE CRAFT SCHEDULER
|
|
MinecraftTimings.bukkitSchedulerTimer.stopTiming(); // Spigot // Paper
|
|
- io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper
|
|
+ if (region == null) io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper
|
|
this.profiler.push("commandFunctions");
|
|
MinecraftTimings.commandFunctionsTimer.startTiming(); // Spigot // Paper
|
|
- this.getFunctions().tick();
|
|
+ if (region == null) this.getFunctions().tick(); // Folia - region threading - TODO Purge functions
|
|
MinecraftTimings.commandFunctionsTimer.stopTiming(); // Spigot // Paper
|
|
this.profiler.popPush("levels");
|
|
//Iterator iterator = this.getAllLevels().iterator(); // Paper - moved down
|
|
@@ -1492,7 +1645,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
// CraftBukkit start
|
|
// Run tasks that are waiting on processing
|
|
MinecraftTimings.processQueueTimer.startTiming(); // Spigot
|
|
- while (!this.processQueue.isEmpty()) {
|
|
+ if (region == null) while (!this.processQueue.isEmpty()) { // Folia - region threading
|
|
this.processQueue.remove().run();
|
|
}
|
|
MinecraftTimings.processQueueTimer.stopTiming(); // Spigot
|
|
@@ -1500,13 +1653,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
MinecraftTimings.timeUpdateTimer.startTiming(); // Spigot // Paper
|
|
// Send time updates to everyone, it will get the right time from the world the player is in.
|
|
// Paper start - optimize time updates
|
|
- for (final ServerLevel world : this.getAllLevels()) {
|
|
+ for (final ServerLevel world : (region == null ? this.getAllLevels() : Arrays.asList(region.world))) { // Folia - region threading
|
|
final boolean doDaylight = world.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT);
|
|
final long dayTime = world.getDayTime();
|
|
long worldTime = world.getGameTime();
|
|
final ClientboundSetTimePacket worldPacket = new ClientboundSetTimePacket(worldTime, dayTime, doDaylight);
|
|
- for (Player entityhuman : world.players()) {
|
|
- if (!(entityhuman instanceof ServerPlayer) || (tickCount + entityhuman.getId()) % 20 != 0) {
|
|
+ for (Player entityhuman : world.getLocalPlayers()) { // Folia - region threading
|
|
+ if (!(entityhuman instanceof ServerPlayer) || ((region == null ? tickCount : region.getCurrentTick()) + entityhuman.getId()) % 20 != 0) { // Folia - region threading
|
|
continue;
|
|
}
|
|
ServerPlayer entityplayer = (ServerPlayer) entityhuman;
|
|
@@ -1519,13 +1672,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
// Paper end
|
|
MinecraftTimings.timeUpdateTimer.stopTiming(); // Spigot // Paper
|
|
|
|
- this.isIteratingOverLevels = true; // Paper
|
|
- Iterator iterator = this.getAllLevels().iterator(); // Paper - move down
|
|
+ if (region == null) this.isIteratingOverLevels = true; // Paper // Folia - region threading
|
|
+ Iterator iterator = region == null ? this.getAllLevels().iterator() : Arrays.asList(region.world).iterator(); // Paper - move down // Folia - region threading
|
|
while (iterator.hasNext()) {
|
|
ServerLevel worldserver = (ServerLevel) iterator.next();
|
|
- worldserver.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.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
|
|
- worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper
|
|
+ // Folia - region threading
|
|
|
|
this.profiler.push(() -> {
|
|
return worldserver + " " + worldserver.dimension().location();
|
|
@@ -1542,7 +1693,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
try {
|
|
worldserver.timings.doTick.startTiming(); // Spigot
|
|
- worldserver.tick(shouldKeepTicking);
|
|
+ worldserver.tick(shouldKeepTicking, region); // Folia - region threading
|
|
// Paper start
|
|
for (final io.papermc.paper.chunk.SingleThreadChunkRegionManager regionManager : worldserver.getChunkSource().chunkMap.regionManagers) {
|
|
regionManager.recalculateRegions();
|
|
@@ -1566,17 +1717,17 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
this.profiler.pop();
|
|
this.profiler.pop();
|
|
- worldserver.explosionDensityCache.clear(); // Paper - Optimize explosions
|
|
+ //worldserver.explosionDensityCache.clear(); // Paper - Optimize explosions // Folia - region threading
|
|
}
|
|
- this.isIteratingOverLevels = false; // Paper
|
|
+ if (region == null) this.isIteratingOverLevels = false; // Paper // Folia - region threading
|
|
|
|
this.profiler.popPush("connection");
|
|
MinecraftTimings.connectionTimer.startTiming(); // Spigot
|
|
- this.getConnection().tick();
|
|
+ if (region == null) this.getConnection().tick(); // Folia - region threading - moved into post entity scheduler tick
|
|
MinecraftTimings.connectionTimer.stopTiming(); // Spigot
|
|
this.profiler.popPush("players");
|
|
MinecraftTimings.playerListTimer.startTiming(); // Spigot // Paper
|
|
- this.playerList.tick();
|
|
+ if (false) this.playerList.tick(); // Folia - region threading
|
|
MinecraftTimings.playerListTimer.stopTiming(); // Spigot // Paper
|
|
if (SharedConstants.IS_RUNNING_IN_IDE) {
|
|
GameTestTicker.SINGLETON.tick();
|
|
@@ -1585,7 +1736,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
this.profiler.popPush("server gui refresh");
|
|
|
|
MinecraftTimings.tickablesTimer.startTiming(); // Spigot // Paper
|
|
- for (int i = 0; i < this.tickables.size(); ++i) {
|
|
+ if (region == null) for (int i = 0; i < this.tickables.size(); ++i) { // Folia - region threading - TODO WTF is this?
|
|
((Runnable) this.tickables.get(i)).run();
|
|
}
|
|
MinecraftTimings.tickablesTimer.stopTiming(); // Spigot // Paper
|
|
@@ -1976,6 +2127,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
|
|
public void invalidateStatus() {
|
|
+ // Folia start - region threading
|
|
+ if (true) {
|
|
+ // we don't need this to notify the global tick region
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTaskWithoutNotify(() -> {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().invalidateStatus();
|
|
+ });
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
this.lastServerStatus = 0L;
|
|
}
|
|
|
|
@@ -1990,6 +2150,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
@Override
|
|
public void executeIfPossible(Runnable runnable) {
|
|
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
|
|
if (this.isStopped()) {
|
|
throw new RejectedExecutionException("Server already shutting down");
|
|
} else {
|
|
@@ -2737,33 +2898,18 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
static final long TASK_EXECUTION_FAILURE_BACKOFF = 5L * 1000L; // 5us
|
|
|
|
- private static long lastMidTickExecute;
|
|
- private static long lastMidTickExecuteFailure;
|
|
-
|
|
- private boolean tickMidTickTasks() {
|
|
- // give all worlds a fair chance at by targetting them all.
|
|
- // if we execute too many tasks, that's fine - we have logic to correctly handle overuse of allocated time.
|
|
- boolean executed = false;
|
|
- for (ServerLevel world : this.getAllLevels()) {
|
|
- long currTime = System.nanoTime();
|
|
- if (currTime - world.lastMidTickExecuteFailure <= TASK_EXECUTION_FAILURE_BACKOFF) {
|
|
- continue;
|
|
- }
|
|
- if (!world.getChunkSource().pollTask()) {
|
|
- // we need to back off if this fails
|
|
- world.lastMidTickExecuteFailure = currTime;
|
|
- } else {
|
|
- executed = true;
|
|
- }
|
|
- }
|
|
+ // Folia - region threading
|
|
|
|
- return executed;
|
|
+ private boolean tickMidTickTasks(io.papermc.paper.threadedregions.RegionizedWorldData worldData) { // Folia - region threading
|
|
+ // Folia - region threading - only execute for one world
|
|
+ return worldData.world.getChunkSource().pollTask();
|
|
}
|
|
|
|
public final void executeMidTickTasks() {
|
|
org.spigotmc.AsyncCatcher.catchOp("mid tick chunk task execution");
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading
|
|
long startTime = System.nanoTime();
|
|
- if ((startTime - lastMidTickExecute) <= CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME || (startTime - lastMidTickExecuteFailure) <= TASK_EXECUTION_FAILURE_BACKOFF) {
|
|
+ if ((startTime - worldData.lastMidTickExecute) <= CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME || (startTime - worldData.lastMidTickExecuteFailure) <= TASK_EXECUTION_FAILURE_BACKOFF) { // Folia - region threading
|
|
// it's shown to be bad to constantly hit the queue (chunk loads slow to a crawl), even if no tasks are executed.
|
|
// so, backoff to prevent this
|
|
return;
|
|
@@ -2772,13 +2918,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
co.aikar.timings.MinecraftTimings.midTickChunkTasks.startTiming();
|
|
try {
|
|
for (;;) {
|
|
- boolean moreTasks = this.tickMidTickTasks();
|
|
+ boolean moreTasks = this.tickMidTickTasks(worldData); // Folia - region threading
|
|
long currTime = System.nanoTime();
|
|
long diff = currTime - startTime;
|
|
|
|
if (!moreTasks || diff >= MAX_CHUNK_EXEC_TIME) {
|
|
if (!moreTasks) {
|
|
- lastMidTickExecuteFailure = currTime;
|
|
+ worldData.lastMidTickExecuteFailure = currTime; // Folia - region threading
|
|
}
|
|
|
|
// note: negative values reduce the time
|
|
@@ -2791,7 +2937,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME;
|
|
long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME);
|
|
|
|
- lastMidTickExecute = currTime + extraSleep;
|
|
+ worldData.lastMidTickExecute = currTime + extraSleep; // Folia - region threading
|
|
return;
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/commands/AdvancementCommands.java b/src/main/java/net/minecraft/server/commands/AdvancementCommands.java
|
|
index 147c256e794f3e579e70927329fd1905ad1b33af..10f3c46334c77241f3cb6bbd833b381d7e68d527 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/AdvancementCommands.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/AdvancementCommands.java
|
|
@@ -61,7 +61,11 @@ public class AdvancementCommands {
|
|
int i = 0;
|
|
|
|
for(ServerPlayer serverPlayer : targets) {
|
|
- i += operation.perform(serverPlayer, selection);
|
|
+ i += 1; // Folia - region threading
|
|
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // 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> 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> 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> 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> 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> 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> 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> attribute) {
|
|
diff --git a/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java b/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java
|
|
index 74623df731de543d3ef5832e818b10adec7b0f01..74a5e35c66e4d6aeae61733ad3ef1e51c0cfd593 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java
|
|
@@ -46,9 +46,12 @@ public class ClearInventoryCommands {
|
|
int i = 0;
|
|
|
|
for(ServerPlayer serverPlayer : targets) {
|
|
- i += serverPlayer.getInventory().clearOrCountMatchingItems(item, maxCount, serverPlayer.inventoryMenu.getCraftSlots());
|
|
+ ++i;
|
|
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading
|
|
+ serverPlayer.getInventory().clearOrCountMatchingItems(item, maxCount, serverPlayer.inventoryMenu.getCraftSlots());
|
|
serverPlayer.containerMenu.broadcastChanges();
|
|
serverPlayer.inventoryMenu.slotsChanged(serverPlayer.getInventory());
|
|
+ }, null, 1L); // Folia - region threading
|
|
}
|
|
|
|
if (i == 0) {
|
|
diff --git a/src/main/java/net/minecraft/server/commands/DamageCommand.java b/src/main/java/net/minecraft/server/commands/DamageCommand.java
|
|
index 95d11f48d201ccf5d9f73a3fcf78782669612f75..c96355360a379849a2f2285e41d7bd7ee6a22fa8 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/DamageCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/DamageCommand.java
|
|
@@ -34,12 +34,28 @@ public class DamageCommand {
|
|
})))))))));
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) {
|
|
+ src.sendFailure((Component)ex.getRawMessage());
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
private static int damage(CommandSourceStack source, Entity target, float amount, DamageSource damageSource) throws CommandSyntaxException {
|
|
- if (target.hurt(damageSource, amount)) {
|
|
- source.sendSuccess(Component.translatable("commands.damage.success", amount, target.getDisplayName()), true);
|
|
- return 1;
|
|
+ // Folia start - region threading
|
|
+ target.getBukkitEntity().taskScheduler.schedule((Entity t) -> {
|
|
+ try { // Folia end - region threading
|
|
+ if (t.hurt(damageSource, amount)) { // Folia - region threading
|
|
+ source.sendSuccess(Component.translatable("commands.damage.success", amount, t.getDisplayName()), true); // Folia - region threading
|
|
+ return; // Folia - region threading
|
|
} else {
|
|
throw ERROR_INVULNERABLE.create();
|
|
}
|
|
+ // Folia start - region threading
|
|
+ } catch (CommandSyntaxException ex) {
|
|
+ sendMessage(source, ex);
|
|
+ }
|
|
+ }, null, 1L);
|
|
+ return 0;
|
|
+ // Folia end - region threading
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java b/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java
|
|
index 1bf4c5b36f53ef1e71d50d1a9af8e1410e5dff60..fd455c794fa52b565a5741b376bc394ac8dda07c 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java
|
|
@@ -25,12 +25,14 @@ public class DefaultGameModeCommands {
|
|
GameType gameType = minecraftServer.getForcedGameType();
|
|
if (gameType != null) {
|
|
for(ServerPlayer serverPlayer : minecraftServer.getPlayerList().getPlayers()) {
|
|
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading
|
|
// Paper start - extend PlayerGameModeChangeEvent
|
|
org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty());
|
|
if (event != null && event.isCancelled()) {
|
|
source.sendSuccess(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), false);
|
|
}
|
|
// Paper end
|
|
+ }, null, 1L); // Folia - region threading
|
|
++i;
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/commands/EffectCommands.java b/src/main/java/net/minecraft/server/commands/EffectCommands.java
|
|
index 959b82e38f3442a6b19a1f9d7615b6ddab967819..6633e7a3946e06438633a3a6276c6eaa9062778c 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/EffectCommands.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/EffectCommands.java
|
|
@@ -84,7 +84,15 @@ public class EffectCommands {
|
|
if (entity instanceof LivingEntity) {
|
|
MobEffectInstance mobeffect = new MobEffectInstance(mobeffectlist, k, amplifier, false, showParticles);
|
|
|
|
- if (((LivingEntity) entity).addEffect(mobeffect, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit
|
|
+ // Folia start - region threading
|
|
+ entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> {
|
|
+ if (!(nmsEntity instanceof LivingEntity)) {
|
|
+ return;
|
|
+ }
|
|
+ ((LivingEntity) nmsEntity).addEffect(mobeffect, null, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND);
|
|
+ }, null, 1L);
|
|
+ // Folia end - region threading
|
|
+ if (true) { // CraftBukkit // Folia - region threading
|
|
++j;
|
|
}
|
|
}
|
|
@@ -110,8 +118,16 @@ public class EffectCommands {
|
|
while (iterator.hasNext()) {
|
|
Entity entity = (Entity) iterator.next();
|
|
|
|
- if (entity instanceof LivingEntity && ((LivingEntity) entity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit
|
|
+ if (entity instanceof LivingEntity) { // CraftBukkit // Folia - region threading
|
|
++i;
|
|
+ // Folia start - region threading
|
|
+ entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> {
|
|
+ if (!(nmsEntity instanceof LivingEntity)) {
|
|
+ return;
|
|
+ }
|
|
+ ((LivingEntity) nmsEntity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND);
|
|
+ }, null, 1L);
|
|
+ // Folia end - region threading
|
|
}
|
|
}
|
|
|
|
@@ -136,8 +152,16 @@ public class EffectCommands {
|
|
while (iterator.hasNext()) {
|
|
Entity entity = (Entity) iterator.next();
|
|
|
|
- if (entity instanceof LivingEntity && ((LivingEntity) entity).removeEffect(mobeffectlist, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit
|
|
+ if (entity instanceof LivingEntity) { // CraftBukkit // Folia - region threading
|
|
++i;
|
|
+ // Folia start - region threading
|
|
+ entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> {
|
|
+ if (!(nmsEntity instanceof LivingEntity)) {
|
|
+ return;
|
|
+ }
|
|
+ ((LivingEntity) nmsEntity).removeEffect(mobeffectlist, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND);
|
|
+ }, null, 1L);
|
|
+ // Folia end - region threading
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/commands/EnchantCommand.java b/src/main/java/net/minecraft/server/commands/EnchantCommand.java
|
|
index e639c0ec642910e66b1d68ae0b9208ef58d91fce..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<? extends Entity> targets, Holder<Enchantment> 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<? extends ServerPlayer> targets, int amount, ExperienceCommand.Type component) {
|
|
for(ServerPlayer serverPlayer : targets) {
|
|
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading
|
|
component.add.accept(serverPlayer, amount);
|
|
+ }, null, 1L); // Folia - region threading
|
|
}
|
|
|
|
if (targets.size() == 1) {
|
|
@@ -69,9 +73,12 @@ public class ExperienceCommand {
|
|
int i = 0;
|
|
|
|
for(ServerPlayer serverPlayer : targets) {
|
|
+ ++i; // Folia - region threading
|
|
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading
|
|
if (component.set.test(serverPlayer, amount)) {
|
|
- ++i;
|
|
+ // Folia - region threading
|
|
}
|
|
+ }, null, 1L); // Folia - region threading
|
|
}
|
|
|
|
if (i == 0) {
|
|
diff --git a/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java b/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java
|
|
index c8977042f26ea8f78cee3a115d2d7dec99b291ee..83a4b72b7f895d29d1e3e0b6b9f288bd5f3cb52c 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java
|
|
@@ -69,6 +69,12 @@ public class FillBiomeCommand {
|
|
};
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) {
|
|
+ src.sendFailure((Component)ex.getRawMessage());
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
private static int fill(CommandSourceStack source, BlockPos from, BlockPos to, Holder.Reference<Biome> biome, Predicate<Holder<Biome>> filter) throws CommandSyntaxException {
|
|
BlockPos blockPos = quantize(from);
|
|
BlockPos blockPos2 = quantize(to);
|
|
@@ -79,8 +85,19 @@ public class FillBiomeCommand {
|
|
throw ERROR_VOLUME_TOO_LARGE.create(j, i);
|
|
} else {
|
|
ServerLevel serverLevel = source.getLevel();
|
|
+ // Folia start - region threading
|
|
+ int buffer = 0;
|
|
+ // no buffer, we do not touch neighbours
|
|
+ serverLevel.loadChunksAsync(
|
|
+ (boundingBox.minX() - buffer) >> 4,
|
|
+ (boundingBox.maxX() + buffer) >> 4,
|
|
+ (boundingBox.minZ() - buffer) >> 4,
|
|
+ (boundingBox.maxZ() + buffer) >> 4,
|
|
+ net.minecraft.world.level.chunk.ChunkStatus.FULL,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL,
|
|
+ (chunks) -> {
|
|
+ try { // Folia end - region threading
|
|
List<ChunkAccess> list = new ArrayList<>();
|
|
-
|
|
for(int k = SectionPos.blockToSectionCoord(boundingBox.minZ()); k <= SectionPos.blockToSectionCoord(boundingBox.maxZ()); ++k) {
|
|
for(int l = SectionPos.blockToSectionCoord(boundingBox.minX()); l <= SectionPos.blockToSectionCoord(boundingBox.maxX()); ++l) {
|
|
ChunkAccess chunkAccess = serverLevel.getChunk(l, k, ChunkStatus.FULL, false);
|
|
@@ -101,7 +118,11 @@ public class FillBiomeCommand {
|
|
|
|
serverLevel.getChunkSource().chunkMap.resendBiomesForChunks(list);
|
|
source.sendSuccess(Component.translatable("commands.fillbiome.success.count", mutableInt.getValue(), boundingBox.minX(), boundingBox.minY(), boundingBox.minZ(), boundingBox.maxX(), boundingBox.maxY(), boundingBox.maxZ()), true);
|
|
- return mutableInt.getValue();
|
|
+ // Folia start - region threading
|
|
+ } catch (CommandSyntaxException ex) {
|
|
+ sendMessage(source, ex);
|
|
+ }
|
|
+ }); return 0; // Folia end - region threading
|
|
}
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/commands/FillCommand.java b/src/main/java/net/minecraft/server/commands/FillCommand.java
|
|
index b25b2ed0eb3c6716a7c92cb1f1066a1447d88bcf..3b55e806204cbe2df2cadcec91fbc84eed5f15b0 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/FillCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/FillCommand.java
|
|
@@ -57,6 +57,12 @@ public class FillCommand {
|
|
}))))));
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) {
|
|
+ src.sendFailure((Component)ex.getRawMessage());
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
private static int fillBlocks(CommandSourceStack source, BoundingBox range, BlockInput block, FillCommand.Mode mode, @Nullable Predicate<BlockInWorld> filter) throws CommandSyntaxException {
|
|
int i = range.getXSpan() * range.getYSpan() * range.getZSpan();
|
|
int j = source.getLevel().getGameRules().getInt(GameRules.RULE_COMMAND_MODIFICATION_BLOCK_LIMIT);
|
|
@@ -65,6 +71,18 @@ public class FillCommand {
|
|
} else {
|
|
List<BlockPos> list = Lists.newArrayList();
|
|
ServerLevel serverLevel = source.getLevel();
|
|
+ // Folia start - region threading
|
|
+ int buffer = 32;
|
|
+ // physics may spill into neighbour chunks, so use a buffer
|
|
+ serverLevel.loadChunksAsync(
|
|
+ (range.minX() - buffer) >> 4,
|
|
+ (range.maxX() + buffer) >> 4,
|
|
+ (range.minZ() - buffer) >> 4,
|
|
+ (range.maxZ() + buffer) >> 4,
|
|
+ net.minecraft.world.level.chunk.ChunkStatus.FULL,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL,
|
|
+ (chunks) -> {
|
|
+ try { // Folia end - region threading
|
|
int k = 0;
|
|
|
|
for(BlockPos blockPos : BlockPos.betweenClosed(range.minX(), range.minY(), range.minZ(), range.maxX(), range.maxY(), range.maxZ())) {
|
|
@@ -90,8 +108,13 @@ public class FillCommand {
|
|
throw ERROR_FAILED.create();
|
|
} else {
|
|
source.sendSuccess(Component.translatable("commands.fill.success", k), true);
|
|
- return k;
|
|
+ return; // Folia - region threading
|
|
}
|
|
+ // Folia start - region threading
|
|
+ } catch (CommandSyntaxException ex) {
|
|
+ sendMessage(source, ex);
|
|
+ }
|
|
+ }); return 0; // Folia end - region threading
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java b/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java
|
|
index de484336165891d16220fdc0363e5283ba92b75d..494292dd2aa45771d42e5f271675f9ca267e8458 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java
|
|
@@ -49,96 +49,126 @@ public class ForceLoadCommand {
|
|
}))));
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) {
|
|
+ src.sendFailure((Component)ex.getRawMessage());
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
private static int queryForceLoad(CommandSourceStack source, ColumnPos pos) throws CommandSyntaxException {
|
|
ChunkPos chunkPos = pos.toChunkPos();
|
|
ServerLevel serverLevel = source.getLevel();
|
|
ResourceKey<Level> resourceKey = serverLevel.dimension();
|
|
- boolean bl = serverLevel.getForcedChunks().contains(chunkPos.toLong());
|
|
- if (bl) {
|
|
- source.sendSuccess(Component.translatable("commands.forceload.query.success", chunkPos, resourceKey.location()), false);
|
|
- return 1;
|
|
- } else {
|
|
- throw ERROR_NOT_TICKING.create(chunkPos, resourceKey.location());
|
|
- }
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
|
|
+ try {
|
|
+ boolean bl = serverLevel.getForcedChunks().contains(chunkPos.toLong());
|
|
+ if (bl) {
|
|
+ source.sendSuccess(Component.translatable("commands.forceload.query.success", chunkPos, resourceKey.location()), false);
|
|
+ return;
|
|
+ } else {
|
|
+ throw ERROR_NOT_TICKING.create(chunkPos, resourceKey.location());
|
|
+ }
|
|
+ } catch (CommandSyntaxException ex) {
|
|
+ sendMessage(source, ex);
|
|
+ }
|
|
+ });
|
|
+ return 1;
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
private static int listForceLoad(CommandSourceStack source) {
|
|
ServerLevel serverLevel = source.getLevel();
|
|
ResourceKey<Level> resourceKey = serverLevel.dimension();
|
|
- LongSet longSet = serverLevel.getForcedChunks();
|
|
- int i = longSet.size();
|
|
- if (i > 0) {
|
|
- String string = Joiner.on(", ").join(longSet.stream().sorted().map(ChunkPos::new).map(ChunkPos::toString).iterator());
|
|
- if (i == 1) {
|
|
- source.sendSuccess(Component.translatable("commands.forceload.list.single", resourceKey.location(), string), false);
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
|
|
+ LongSet longSet = serverLevel.getForcedChunks();
|
|
+ int i = longSet.size();
|
|
+ if (i > 0) {
|
|
+ String string = Joiner.on(", ").join(longSet.stream().sorted().map(ChunkPos::new).map(ChunkPos::toString).iterator());
|
|
+ if (i == 1) {
|
|
+ source.sendSuccess(Component.translatable("commands.forceload.list.single", resourceKey.location(), string), false);
|
|
+ } else {
|
|
+ source.sendSuccess(Component.translatable("commands.forceload.list.multiple", i, resourceKey.location(), string), false);
|
|
+ }
|
|
} else {
|
|
- source.sendSuccess(Component.translatable("commands.forceload.list.multiple", i, resourceKey.location(), string), false);
|
|
+ source.sendFailure(Component.translatable("commands.forceload.added.none", resourceKey.location()));
|
|
}
|
|
- } else {
|
|
- source.sendFailure(Component.translatable("commands.forceload.added.none", resourceKey.location()));
|
|
- }
|
|
|
|
- return i;
|
|
+ });
|
|
+ return 1;
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
private static int removeAll(CommandSourceStack source) {
|
|
ServerLevel serverLevel = source.getLevel();
|
|
ResourceKey<Level> resourceKey = serverLevel.dimension();
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
|
|
LongSet longSet = serverLevel.getForcedChunks();
|
|
longSet.forEach((chunkPos) -> {
|
|
serverLevel.setChunkForced(ChunkPos.getX(chunkPos), ChunkPos.getZ(chunkPos), false);
|
|
});
|
|
source.sendSuccess(Component.translatable("commands.forceload.removed.all", resourceKey.location()), true);
|
|
+ }); // Folia - region threading
|
|
return 0;
|
|
}
|
|
|
|
private static int changeForceLoad(CommandSourceStack source, ColumnPos from, ColumnPos to, boolean forceLoaded) throws CommandSyntaxException {
|
|
- int i = Math.min(from.x(), to.x());
|
|
- int j = Math.min(from.z(), to.z());
|
|
- int k = Math.max(from.x(), to.x());
|
|
- int l = Math.max(from.z(), to.z());
|
|
- if (i >= -30000000 && j >= -30000000 && k < 30000000 && l < 30000000) {
|
|
- int m = SectionPos.blockToSectionCoord(i);
|
|
- int n = SectionPos.blockToSectionCoord(j);
|
|
- int o = SectionPos.blockToSectionCoord(k);
|
|
- int p = SectionPos.blockToSectionCoord(l);
|
|
- long q = ((long)(o - m) + 1L) * ((long)(p - n) + 1L);
|
|
- if (q > 256L) {
|
|
- throw ERROR_TOO_MANY_CHUNKS.create(256, q);
|
|
- } else {
|
|
- ServerLevel serverLevel = source.getLevel();
|
|
- ResourceKey<Level> resourceKey = serverLevel.dimension();
|
|
- ChunkPos chunkPos = null;
|
|
- int r = 0;
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
|
|
+ try {
|
|
+ int i = Math.min(from.x(), to.x());
|
|
+ int j = Math.min(from.z(), to.z());
|
|
+ int k = Math.max(from.x(), to.x());
|
|
+ int l = Math.max(from.z(), to.z());
|
|
+ if (i >= -30000000 && j >= -30000000 && k < 30000000 && l < 30000000) {
|
|
+ int m = SectionPos.blockToSectionCoord(i);
|
|
+ int n = SectionPos.blockToSectionCoord(j);
|
|
+ int o = SectionPos.blockToSectionCoord(k);
|
|
+ int p = SectionPos.blockToSectionCoord(l);
|
|
+ long q = ((long)(o - m) + 1L) * ((long)(p - n) + 1L);
|
|
+ if (q > 256L) {
|
|
+ throw ERROR_TOO_MANY_CHUNKS.create(256, q);
|
|
+ } else {
|
|
+ ServerLevel serverLevel = source.getLevel();
|
|
+ ResourceKey<Level> resourceKey = serverLevel.dimension();
|
|
+ ChunkPos chunkPos = null;
|
|
+ int r = 0;
|
|
|
|
- for(int s = m; s <= o; ++s) {
|
|
- for(int t = n; t <= p; ++t) {
|
|
- boolean bl = serverLevel.setChunkForced(s, t, forceLoaded);
|
|
- if (bl) {
|
|
- ++r;
|
|
- if (chunkPos == null) {
|
|
- chunkPos = new ChunkPos(s, t);
|
|
+ for(int s = m; s <= o; ++s) {
|
|
+ for(int t = n; t <= p; ++t) {
|
|
+ boolean bl = serverLevel.setChunkForced(s, t, forceLoaded);
|
|
+ if (bl) {
|
|
+ ++r;
|
|
+ if (chunkPos == null) {
|
|
+ chunkPos = new ChunkPos(s, t);
|
|
+ }
|
|
+ }
|
|
}
|
|
}
|
|
- }
|
|
- }
|
|
|
|
- if (r == 0) {
|
|
- throw (forceLoaded ? ERROR_ALL_ADDED : ERROR_NONE_REMOVED).create();
|
|
- } else {
|
|
- if (r == 1) {
|
|
- source.sendSuccess(Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".single", chunkPos, resourceKey.location()), true);
|
|
- } else {
|
|
- ChunkPos chunkPos2 = new ChunkPos(m, n);
|
|
- ChunkPos chunkPos3 = new ChunkPos(o, p);
|
|
- source.sendSuccess(Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".multiple", r, resourceKey.location(), chunkPos2, chunkPos3), true);
|
|
- }
|
|
+ if (r == 0) {
|
|
+ throw (forceLoaded ? ERROR_ALL_ADDED : ERROR_NONE_REMOVED).create();
|
|
+ } else {
|
|
+ if (r == 1) {
|
|
+ source.sendSuccess(Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".single", chunkPos, resourceKey.location()), true);
|
|
+ } else {
|
|
+ ChunkPos chunkPos2 = new ChunkPos(m, n);
|
|
+ ChunkPos chunkPos3 = new ChunkPos(o, p);
|
|
+ source.sendSuccess(Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".multiple", r, resourceKey.location(), chunkPos2, chunkPos3), true);
|
|
+ }
|
|
|
|
- return r;
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ } else {
|
|
+ throw BlockPosArgument.ERROR_OUT_OF_WORLD.create();
|
|
}
|
|
+ } catch (CommandSyntaxException ex) {
|
|
+ sendMessage(source, ex);
|
|
}
|
|
- } else {
|
|
- throw BlockPosArgument.ERROR_OUT_OF_WORLD.create();
|
|
- }
|
|
+ });
|
|
+ return 1;
|
|
+ // Folia end - region threading
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/commands/GameModeCommand.java b/src/main/java/net/minecraft/server/commands/GameModeCommand.java
|
|
index 27c0aaf123c3e945eb24e8a3892bd8ac42115733..2f9f73e75b6c730a9cf327767ba1c34e34c64ed8 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/GameModeCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/GameModeCommand.java
|
|
@@ -44,15 +44,18 @@ public class GameModeCommand {
|
|
int i = 0;
|
|
|
|
for(ServerPlayer serverPlayer : targets) {
|
|
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading
|
|
// Paper start - extend PlayerGameModeChangeEvent
|
|
org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty());
|
|
if (event != null && !event.isCancelled()) {
|
|
logGamemodeChange(context.getSource(), serverPlayer, gameMode);
|
|
- ++i;
|
|
+ // Folia - region threading
|
|
} else if (event != null && event.cancelMessage() != null) {
|
|
context.getSource().sendSuccess(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), true);
|
|
// Paper end
|
|
}
|
|
+ }, null, 1L); // Folia - region threading
|
|
+ ++i; // Folia - region threading
|
|
}
|
|
|
|
return i;
|
|
diff --git a/src/main/java/net/minecraft/server/commands/GiveCommand.java b/src/main/java/net/minecraft/server/commands/GiveCommand.java
|
|
index ee7d29d85c8b024c9b23cba8ecd4192aa7e8aa7b..9fc4e60750552dae8412ed07982ea025f934af61 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/GiveCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/GiveCommand.java
|
|
@@ -55,6 +55,7 @@ public class GiveCommand {
|
|
|
|
l -= i1;
|
|
ItemStack itemstack = item.createItemStack(i1, false);
|
|
+ entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading
|
|
boolean flag = entityplayer.getInventory().add(itemstack);
|
|
ItemEntity entityitem;
|
|
|
|
@@ -74,6 +75,7 @@ public class GiveCommand {
|
|
entityitem.setTarget(entityplayer.getUUID());
|
|
}
|
|
}
|
|
+ }, null, 1L); // Folia - region threading
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/commands/KillCommand.java b/src/main/java/net/minecraft/server/commands/KillCommand.java
|
|
index a6e4bd9243dab7feaed1bd968108a324d6c37ed7..4637e60292128e8c4053fb3a5fed48e53ec6553f 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/KillCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/KillCommand.java
|
|
@@ -22,7 +22,9 @@ public class KillCommand {
|
|
|
|
private static int kill(CommandSourceStack source, Collection<? extends Entity> targets) {
|
|
for(Entity entity : targets) {
|
|
- entity.kill();
|
|
+ entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading
|
|
+ nmsEntity.kill(); // Folia - region threading
|
|
+ }, null, 1L); // Folia - region threading
|
|
}
|
|
|
|
if (targets.size() == 1) {
|
|
diff --git a/src/main/java/net/minecraft/server/commands/PlaceCommand.java b/src/main/java/net/minecraft/server/commands/PlaceCommand.java
|
|
index c9ee0b8a0f72c21f29448c3a8fd53a7007c740f8..6ab0bbfb5e04b13082999d825e8445fbbbc184a3 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/PlaceCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/PlaceCommand.java
|
|
@@ -83,82 +83,130 @@ public class PlaceCommand {
|
|
})))))))));
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) {
|
|
+ src.sendFailure((Component)ex.getRawMessage());
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
public static int placeFeature(CommandSourceStack source, Holder.Reference<ConfiguredFeature<?, ?>> 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<StructureTemplatePool> structurePool, ResourceLocation id, int maxDepth, BlockPos pos) throws CommandSyntaxException {
|
|
ServerLevel serverLevel = source.getLevel();
|
|
- if (!JigsawPlacement.generateJigsaw(serverLevel, structurePool, id, maxDepth, pos, false)) {
|
|
- throw ERROR_JIGSAW_FAILED.create();
|
|
- } else {
|
|
- source.sendSuccess(Component.translatable("commands.place.jigsaw.success", pos.getX(), pos.getY(), pos.getZ()), true);
|
|
- return 1;
|
|
- }
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ serverLevel, pos.getX() >> 4, pos.getZ() >> 4, () -> {
|
|
+ try {
|
|
+ if (!JigsawPlacement.generateJigsaw(serverLevel, structurePool, id, maxDepth, pos, false)) {
|
|
+ throw ERROR_JIGSAW_FAILED.create();
|
|
+ } else {
|
|
+ source.sendSuccess(Component.translatable("commands.place.jigsaw.success", pos.getX(), pos.getY(), pos.getZ()), true);
|
|
+ }
|
|
+ } catch (CommandSyntaxException ex) {
|
|
+ sendMessage(source, ex);
|
|
+ }
|
|
+ }
|
|
+ );
|
|
+ return 1;
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
public static int placeStructure(CommandSourceStack source, Holder.Reference<Structure> structure, BlockPos pos) throws CommandSyntaxException {
|
|
ServerLevel serverLevel = source.getLevel();
|
|
Structure structure2 = structure.value();
|
|
ChunkGenerator chunkGenerator = serverLevel.getChunkSource().getGenerator();
|
|
- StructureStart structureStart = structure2.generate(source.registryAccess(), chunkGenerator, chunkGenerator.getBiomeSource(), serverLevel.getChunkSource().randomState(), serverLevel.getStructureManager(), serverLevel.getSeed(), new ChunkPos(pos), 0, serverLevel, (biome) -> {
|
|
- return true;
|
|
- });
|
|
- if (!structureStart.isValid()) {
|
|
- throw ERROR_STRUCTURE_FAILED.create();
|
|
- } else {
|
|
- BoundingBox boundingBox = structureStart.getBoundingBox();
|
|
- ChunkPos chunkPos = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.minX()), SectionPos.blockToSectionCoord(boundingBox.minZ()));
|
|
- ChunkPos chunkPos2 = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.maxX()), SectionPos.blockToSectionCoord(boundingBox.maxZ()));
|
|
- checkLoaded(serverLevel, chunkPos, chunkPos2);
|
|
- ChunkPos.rangeClosed(chunkPos, chunkPos2).forEach((chunkPosx) -> {
|
|
- structureStart.placeInChunk(serverLevel, serverLevel.structureManager(), chunkGenerator, serverLevel.getRandom(), new BoundingBox(chunkPosx.getMinBlockX(), serverLevel.getMinBuildHeight(), chunkPosx.getMinBlockZ(), chunkPosx.getMaxBlockX(), serverLevel.getMaxBuildHeight(), chunkPosx.getMaxBlockZ()), chunkPosx);
|
|
- });
|
|
- String string = structure.key().location().toString();
|
|
- source.sendSuccess(Component.translatable("commands.place.structure.success", string, pos.getX(), pos.getY(), pos.getZ()), true);
|
|
- return 1;
|
|
- }
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ serverLevel, pos.getX() >> 4, pos.getZ() >> 4, () -> {
|
|
+ try {
|
|
+ StructureStart structureStart = structure2.generate(source.registryAccess(), chunkGenerator, chunkGenerator.getBiomeSource(), serverLevel.getChunkSource().randomState(), serverLevel.getStructureManager(), serverLevel.getSeed(), new ChunkPos(pos), 0, serverLevel, (biome) -> {
|
|
+ return true;
|
|
+ });
|
|
+ if (!structureStart.isValid()) {
|
|
+ throw ERROR_STRUCTURE_FAILED.create();
|
|
+ } else {
|
|
+ BoundingBox boundingBox = structureStart.getBoundingBox();
|
|
+ ChunkPos chunkPos = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.minX()), SectionPos.blockToSectionCoord(boundingBox.minZ()));
|
|
+ ChunkPos chunkPos2 = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.maxX()), SectionPos.blockToSectionCoord(boundingBox.maxZ()));
|
|
+ checkLoaded(serverLevel, chunkPos, chunkPos2);
|
|
+ ChunkPos.rangeClosed(chunkPos, chunkPos2).forEach((chunkPosx) -> {
|
|
+ structureStart.placeInChunk(serverLevel, serverLevel.structureManager(), chunkGenerator, serverLevel.getRandom(), new BoundingBox(chunkPosx.getMinBlockX(), serverLevel.getMinBuildHeight(), chunkPosx.getMinBlockZ(), chunkPosx.getMaxBlockX(), serverLevel.getMaxBuildHeight(), chunkPosx.getMaxBlockZ()), chunkPosx);
|
|
+ });
|
|
+ String string = structure.key().location().toString();
|
|
+ source.sendSuccess(Component.translatable("commands.place.structure.success", string, pos.getX(), pos.getY(), pos.getZ()), true);
|
|
+ }
|
|
+ } catch (CommandSyntaxException ex) {
|
|
+ sendMessage(source, ex);
|
|
+ }
|
|
+ }
|
|
+ );
|
|
+ return 1;
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
public static int placeTemplate(CommandSourceStack source, ResourceLocation id, BlockPos pos, Rotation rotation, Mirror mirror, float integrity, int seed) throws CommandSyntaxException {
|
|
ServerLevel serverLevel = source.getLevel();
|
|
- StructureTemplateManager structureTemplateManager = serverLevel.getStructureManager();
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ serverLevel, pos.getX() >> 4, pos.getZ() >> 4, () -> {
|
|
+ try {
|
|
+ StructureTemplateManager structureTemplateManager = serverLevel.getStructureManager();
|
|
|
|
- Optional<StructureTemplate> optional;
|
|
- try {
|
|
- optional = structureTemplateManager.get(id);
|
|
- } catch (ResourceLocationException var13) {
|
|
- throw ERROR_TEMPLATE_INVALID.create(id);
|
|
- }
|
|
+ Optional<StructureTemplate> optional;
|
|
+ try {
|
|
+ optional = structureTemplateManager.get(id);
|
|
+ } catch (ResourceLocationException var13) {
|
|
+ throw ERROR_TEMPLATE_INVALID.create(id);
|
|
+ }
|
|
|
|
- if (optional.isEmpty()) {
|
|
- throw ERROR_TEMPLATE_INVALID.create(id);
|
|
- } else {
|
|
- StructureTemplate structureTemplate = optional.get();
|
|
- checkLoaded(serverLevel, new ChunkPos(pos), new ChunkPos(pos.offset(structureTemplate.getSize())));
|
|
- StructurePlaceSettings structurePlaceSettings = (new StructurePlaceSettings()).setMirror(mirror).setRotation(rotation);
|
|
- if (integrity < 1.0F) {
|
|
- structurePlaceSettings.clearProcessors().addProcessor(new BlockRotProcessor(integrity)).setRandom(StructureBlockEntity.createRandom((long)seed));
|
|
- }
|
|
+ if (optional.isEmpty()) {
|
|
+ throw ERROR_TEMPLATE_INVALID.create(id);
|
|
+ } else {
|
|
+ StructureTemplate structureTemplate = optional.get();
|
|
+ checkLoaded(serverLevel, new ChunkPos(pos), new ChunkPos(pos.offset(structureTemplate.getSize())));
|
|
+ StructurePlaceSettings structurePlaceSettings = (new StructurePlaceSettings()).setMirror(mirror).setRotation(rotation);
|
|
+ if (integrity < 1.0F) {
|
|
+ structurePlaceSettings.clearProcessors().addProcessor(new BlockRotProcessor(integrity)).setRandom(StructureBlockEntity.createRandom((long)seed));
|
|
+ }
|
|
|
|
- boolean bl = structureTemplate.placeInWorld(serverLevel, pos, pos, structurePlaceSettings, StructureBlockEntity.createRandom((long)seed), 2);
|
|
- if (!bl) {
|
|
- throw ERROR_TEMPLATE_FAILED.create();
|
|
- } else {
|
|
- source.sendSuccess(Component.translatable("commands.place.template.success", id, pos.getX(), pos.getY(), pos.getZ()), true);
|
|
- return 1;
|
|
+ boolean bl = structureTemplate.placeInWorld(serverLevel, pos, pos, structurePlaceSettings, StructureBlockEntity.createRandom((long)seed), 2);
|
|
+ if (!bl) {
|
|
+ throw ERROR_TEMPLATE_FAILED.create();
|
|
+ } else {
|
|
+ source.sendSuccess(Component.translatable("commands.place.template.success", id, pos.getX(), pos.getY(), pos.getZ()), true);
|
|
+ }
|
|
+ }
|
|
+ } catch (CommandSyntaxException ex) {
|
|
+ sendMessage(source, ex);
|
|
+ }
|
|
}
|
|
- }
|
|
+ );
|
|
+ return 1;
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
private static void checkLoaded(ServerLevel world, ChunkPos pos1, ChunkPos pos2) throws CommandSyntaxException {
|
|
diff --git a/src/main/java/net/minecraft/server/commands/RecipeCommand.java b/src/main/java/net/minecraft/server/commands/RecipeCommand.java
|
|
index 2a92e542e4b3e4dfb26adfc4b21490a629b79382..d3405192a705637daba66735c717d64708362bd1 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/RecipeCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/RecipeCommand.java
|
|
@@ -36,7 +36,12 @@ public class RecipeCommand {
|
|
int i = 0;
|
|
|
|
for(ServerPlayer serverPlayer : targets) {
|
|
- i += serverPlayer.awardRecipes(recipes);
|
|
+ // Folia start - region threading
|
|
+ ++i;
|
|
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> {
|
|
+ serverPlayer.awardRecipes(recipes);
|
|
+ }, null, 1L);
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
if (i == 0) {
|
|
@@ -56,7 +61,12 @@ public class RecipeCommand {
|
|
int i = 0;
|
|
|
|
for(ServerPlayer serverPlayer : targets) {
|
|
- i += serverPlayer.resetRecipes(recipes);
|
|
+ // Folia start - region threading
|
|
+ ++i;
|
|
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> {
|
|
+ serverPlayer.resetRecipes(recipes);
|
|
+ }, null, 1L);
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
if (i == 0) {
|
|
diff --git a/src/main/java/net/minecraft/server/commands/SetBlockCommand.java b/src/main/java/net/minecraft/server/commands/SetBlockCommand.java
|
|
index ad435815e56ca5a8d5ea6046ee4a3ed4d3673a48..98f314064987e3c0b87e415459f6c263e75ea48d 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/SetBlockCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/SetBlockCommand.java
|
|
@@ -38,29 +38,45 @@ public class SetBlockCommand {
|
|
})))));
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) {
|
|
+ src.sendFailure((Component)ex.getRawMessage());
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
private static int setBlock(CommandSourceStack source, BlockPos pos, BlockInput block, SetBlockCommand.Mode mode, @Nullable Predicate<BlockInWorld> condition) throws CommandSyntaxException {
|
|
ServerLevel serverLevel = source.getLevel();
|
|
- if (condition != null && !condition.test(new BlockInWorld(serverLevel, pos, true))) {
|
|
- throw ERROR_FAILED.create();
|
|
- } else {
|
|
- boolean bl;
|
|
- if (mode == SetBlockCommand.Mode.DESTROY) {
|
|
- serverLevel.destroyBlock(pos, true);
|
|
- bl = !block.getState().isAir() || !serverLevel.getBlockState(pos).isAir();
|
|
- } else {
|
|
- BlockEntity blockEntity = serverLevel.getBlockEntity(pos);
|
|
- Clearable.tryClear(blockEntity);
|
|
- bl = true;
|
|
- }
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ serverLevel, pos.getX() >> 4, pos.getZ() >> 4, () -> {
|
|
+ try {
|
|
+ if (condition != null && !condition.test(new BlockInWorld(serverLevel, pos, true))) {
|
|
+ throw ERROR_FAILED.create();
|
|
+ } else {
|
|
+ boolean bl;
|
|
+ if (mode == SetBlockCommand.Mode.DESTROY) {
|
|
+ serverLevel.destroyBlock(pos, true);
|
|
+ bl = !block.getState().isAir() || !serverLevel.getBlockState(pos).isAir();
|
|
+ } else {
|
|
+ BlockEntity blockEntity = serverLevel.getBlockEntity(pos);
|
|
+ Clearable.tryClear(blockEntity);
|
|
+ bl = true;
|
|
+ }
|
|
|
|
- if (bl && !block.place(serverLevel, pos, 2)) {
|
|
- throw ERROR_FAILED.create();
|
|
- } else {
|
|
- serverLevel.blockUpdated(pos, block.getState().getBlock());
|
|
- source.sendSuccess(Component.translatable("commands.setblock.success", pos.getX(), pos.getY(), pos.getZ()), true);
|
|
- return 1;
|
|
+ if (bl && !block.place(serverLevel, pos, 2)) {
|
|
+ throw ERROR_FAILED.create();
|
|
+ } else {
|
|
+ serverLevel.blockUpdated(pos, block.getState().getBlock());
|
|
+ source.sendSuccess(Component.translatable("commands.setblock.success", pos.getX(), pos.getY(), pos.getZ()), true);
|
|
+ }
|
|
+ }
|
|
+ } catch (CommandSyntaxException ex) {
|
|
+ sendMessage(source, ex);
|
|
+ }
|
|
}
|
|
- }
|
|
+ );
|
|
+ return 1;
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
public interface Filter {
|
|
diff --git a/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java b/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java
|
|
index b0ed00b95b273f0916cbade0e3bac47aa464d3fd..ebabc7919562acc1e43014aa702e65765567175f 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java
|
|
@@ -35,7 +35,12 @@ public class SetSpawnCommand {
|
|
final Collection<ServerPlayer> actualTargets = new java.util.ArrayList<>(); // Paper
|
|
for(ServerPlayer serverPlayer : targets) {
|
|
// Paper start - PlayerSetSpawnEvent
|
|
- if (serverPlayer.setRespawnPosition(resourceKey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND)) {
|
|
+ // Folia start - region threading
|
|
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> {
|
|
+ player.setRespawnPosition(resourceKey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND);
|
|
+ }, null, 1L);
|
|
+ // Folia end - region threading
|
|
+ if (true) { // Folia - region threading
|
|
actualTargets.add(serverPlayer);
|
|
}
|
|
// Paper end
|
|
diff --git a/src/main/java/net/minecraft/server/commands/SummonCommand.java b/src/main/java/net/minecraft/server/commands/SummonCommand.java
|
|
index 108b8ddbb724c6a2aca97bd7d5b29bea87662e70..6da8e88a64e0904a9e5f27da21559ab60659dd88 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/SummonCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/SummonCommand.java
|
|
@@ -63,11 +63,18 @@ public class SummonCommand {
|
|
if (entity == null) {
|
|
throw SummonCommand.ERROR_FAILED.create();
|
|
} else {
|
|
- if (initialize && entity instanceof Mob) {
|
|
- ((Mob) entity).finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), MobSpawnType.COMMAND, (SpawnGroupData) null, (CompoundTag) null);
|
|
- }
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ worldserver, entity.chunkPosition().x, entity.chunkPosition().z, () -> {
|
|
+ if (initialize && entity instanceof Mob) {
|
|
+ ((Mob) entity).finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), MobSpawnType.COMMAND, (SpawnGroupData) null, (CompoundTag) null);
|
|
+ }
|
|
+ worldserver.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND);
|
|
+ }
|
|
+ );
|
|
+ // Folia end - region threading
|
|
|
|
- if (!worldserver.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND)) { // CraftBukkit - pass a spawn reason of "COMMAND"
|
|
+ if (false) { // CraftBukkit - pass a spawn reason of "COMMAND" // Folia - region threading
|
|
throw SummonCommand.ERROR_DUPLICATE_UUID.create();
|
|
} else {
|
|
return entity;
|
|
diff --git a/src/main/java/net/minecraft/server/commands/TeleportCommand.java b/src/main/java/net/minecraft/server/commands/TeleportCommand.java
|
|
index 6511af08d8db1f8fc62bdb4a8f357941f524886d..8dee4f9a408194f2bbbd9028517d33504cbe188d 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/TeleportCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/TeleportCommand.java
|
|
@@ -77,7 +77,7 @@ public class TeleportCommand {
|
|
while (iterator.hasNext()) {
|
|
Entity entity1 = (Entity) iterator.next();
|
|
|
|
- TeleportCommand.performTeleport(source, entity1, (ServerLevel) destination.level, destination.getX(), destination.getY(), destination.getZ(), EnumSet.noneOf(RelativeMovement.class), destination.getYRot(), destination.getXRot(), (TeleportCommand.LookAt) null);
|
|
+ io.papermc.paper.threadedregions.TeleportUtils.teleport(entity1, false, destination, Float.valueOf(destination.getYRot()), Float.valueOf(destination.getXRot()), Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND, null); // Folia - region threading
|
|
}
|
|
|
|
if (targets.size() == 1) {
|
|
@@ -153,6 +153,24 @@ public class TeleportCommand {
|
|
float f2 = Mth.wrapDegrees(yaw);
|
|
float f3 = Mth.wrapDegrees(pitch);
|
|
|
|
+ // Folia start - region threading
|
|
+ if (true) {
|
|
+ ServerLevel worldFinal = world;
|
|
+ Vec3 posFinal = new Vec3(x, y, z);
|
|
+ Float yawFinal = Float.valueOf(f2);
|
|
+ Float pitchFinal = Float.valueOf(f3);
|
|
+ target.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> {
|
|
+ nmsEntity.unRide();
|
|
+ nmsEntity.teleportAsync(
|
|
+ worldFinal, posFinal, yawFinal, pitchFinal, Vec3.ZERO,
|
|
+ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND,
|
|
+ Entity.TELEPORT_FLAG_LOAD_CHUNK,
|
|
+ null
|
|
+ );
|
|
+ }, null, 1L);
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
// CraftBukkit start - Teleport event
|
|
boolean result;
|
|
if (target instanceof ServerPlayer player) {
|
|
diff --git a/src/main/java/net/minecraft/server/commands/TimeCommand.java b/src/main/java/net/minecraft/server/commands/TimeCommand.java
|
|
index f0a7a8df3caa2ea765bb0a87cfede71d0995d276..b666adf0d0eddea1beb59d6f7a2968b5c0296b37 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/TimeCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/TimeCommand.java
|
|
@@ -56,6 +56,7 @@ public class TimeCommand {
|
|
while (iterator.hasNext()) {
|
|
ServerLevel worldserver = (ServerLevel) iterator.next();
|
|
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
|
|
// CraftBukkit start
|
|
TimeSkipEvent event = new TimeSkipEvent(worldserver.getWorld(), TimeSkipEvent.SkipReason.COMMAND, time - worldserver.getDayTime());
|
|
Bukkit.getPluginManager().callEvent(event);
|
|
@@ -63,6 +64,7 @@ public class TimeCommand {
|
|
worldserver.setDayTime((long) worldserver.getDayTime() + event.getSkipAmount());
|
|
}
|
|
// CraftBukkit end
|
|
+ }); // Folia - region threading
|
|
}
|
|
|
|
source.sendSuccess(Component.translatable("commands.time.set", time), true);
|
|
@@ -75,6 +77,7 @@ public class TimeCommand {
|
|
while (iterator.hasNext()) {
|
|
ServerLevel worldserver = (ServerLevel) iterator.next();
|
|
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
|
|
// CraftBukkit start
|
|
TimeSkipEvent event = new TimeSkipEvent(worldserver.getWorld(), TimeSkipEvent.SkipReason.COMMAND, time);
|
|
Bukkit.getPluginManager().callEvent(event);
|
|
@@ -82,11 +85,14 @@ public class TimeCommand {
|
|
worldserver.setDayTime(worldserver.getDayTime() + event.getSkipAmount());
|
|
}
|
|
// CraftBukkit end
|
|
+ }); // Folia - region threading
|
|
}
|
|
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
|
|
int j = TimeCommand.getDayTime(source.getLevel());
|
|
|
|
source.sendSuccess(Component.translatable("commands.time.set", j), true);
|
|
- return j;
|
|
+ }); // Folia - region threading
|
|
+ return 0; // Folia - region threading
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/commands/WeatherCommand.java b/src/main/java/net/minecraft/server/commands/WeatherCommand.java
|
|
index 471e9a2d8ad92c8f45856487a2919e4113ed4468..67c401bb45c1c972f1cd249547c094c854d50cc3 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/WeatherCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/WeatherCommand.java
|
|
@@ -35,20 +35,26 @@ public class WeatherCommand {
|
|
}
|
|
|
|
private static int setClear(CommandSourceStack source, int duration) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
|
|
source.getLevel().setWeatherParameters(getDuration(source, duration, ServerLevel.RAIN_DELAY), 0, false, false);
|
|
source.sendSuccess(Component.translatable("commands.weather.set.clear"), true);
|
|
+ }); // Folia - region threading
|
|
return duration;
|
|
}
|
|
|
|
private static int setRain(CommandSourceStack source, int duration) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
|
|
source.getLevel().setWeatherParameters(0, getDuration(source, duration, ServerLevel.RAIN_DURATION), true, false);
|
|
source.sendSuccess(Component.translatable("commands.weather.set.rain"), true);
|
|
+ }); // Folia - region threading
|
|
return duration;
|
|
}
|
|
|
|
private static int setThunder(CommandSourceStack source, int duration) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
|
|
source.getLevel().setWeatherParameters(0, getDuration(source, duration, ServerLevel.THUNDER_DURATION), true, true);
|
|
source.sendSuccess(Component.translatable("commands.weather.set.thunder"), true);
|
|
+ }); // Folia - region threading
|
|
return duration;
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
|
|
index a7e133f3495e9132a5fdae2c24f225e7b026295a..7abd4f38ae59a6019137345af960fd60a3c7adf0 100644
|
|
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
|
|
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
|
|
@@ -443,9 +443,9 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
|
|
}
|
|
|
|
@Override
|
|
- public void tickChildren(BooleanSupplier shouldKeepTicking) {
|
|
- super.tickChildren(shouldKeepTicking);
|
|
- this.handleConsoleInputs();
|
|
+ public void tickChildren(BooleanSupplier shouldKeepTicking, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - region threading
|
|
+ super.tickChildren(shouldKeepTicking, region); // Folia - region threading
|
|
+ if (region == null) this.handleConsoleInputs(); // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
@@ -748,6 +748,12 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
|
|
|
|
@Override
|
|
public String runCommand(String command) {
|
|
+ // Folia start - region threading
|
|
+ // RIP RCON
|
|
+ if (true) {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
Waitable[] waitableArray = new Waitable[1];
|
|
this.rconConsoleSource.prepareForCommand();
|
|
this.executeBlocking(() -> {
|
|
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
index 6fce2a9bce051e21eba8f331007a9752607f69f2..a880cbd4afdf2e540bbe6b62587091c9b5a76ef0 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
@@ -85,18 +85,18 @@ public class ChunkHolder {
|
|
public void onChunkAdd() {
|
|
// Paper start - optimise anyPlayerCloseEnoughForSpawning
|
|
long key = io.papermc.paper.util.MCUtil.getCoordinateKey(this.pos);
|
|
- this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key);
|
|
- this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key);
|
|
+ this.playersInMobSpawnRange = null; // Folia - region threading
|
|
+ this.playersInChunkTickRange = null; // Folia - region threading
|
|
// Paper end - optimise anyPlayerCloseEnoughForSpawning
|
|
// Paper start - optimise chunk tick iteration
|
|
if (this.needsBroadcastChanges()) {
|
|
- this.chunkMap.needsChangeBroadcasting.add(this);
|
|
+ this.chunkMap.level.needsChangeBroadcasting.add(this); // Folia - region threading
|
|
}
|
|
// Paper end - optimise chunk tick iteration
|
|
// Paper start - optimise checkDespawn
|
|
LevelChunk chunk = this.getFullChunkNowUnchecked();
|
|
if (chunk != null) {
|
|
- chunk.updateGeneralAreaCache();
|
|
+ //chunk.updateGeneralAreaCache(); // Folia - region threading
|
|
}
|
|
// Paper end - optimise checkDespawn
|
|
}
|
|
@@ -108,13 +108,13 @@ public class ChunkHolder {
|
|
// Paper end - optimise anyPlayerCloseEnoughForSpawning
|
|
// Paper start - optimise chunk tick iteration
|
|
if (this.needsBroadcastChanges()) {
|
|
- this.chunkMap.needsChangeBroadcasting.remove(this);
|
|
+ this.chunkMap.level.needsChangeBroadcasting.remove(this); // Folia - region threading
|
|
}
|
|
// Paper end - optimise chunk tick iteration
|
|
// Paper start - optimise checkDespawn
|
|
LevelChunk chunk = this.getFullChunkNowUnchecked();
|
|
if (chunk != null) {
|
|
- chunk.removeGeneralAreaCache();
|
|
+ //chunk.removeGeneralAreaCache(); // Folia - region threading
|
|
}
|
|
// Paper end - optimise checkDespawn
|
|
}
|
|
@@ -302,8 +302,8 @@ public class ChunkHolder {
|
|
}
|
|
|
|
private void addToBroadcastMap() {
|
|
- org.spigotmc.AsyncCatcher.catchOp("ChunkHolder update");
|
|
- this.chunkMap.needsChangeBroadcasting.add(this);
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this.chunkMap.level, this.pos, "Cannot update chunk holder asynchronously"); // Folia - region threading
|
|
+ this.chunkMap.level.needsChangeBroadcasting.add(this); // Folia - region threading
|
|
}
|
|
// Paper end - optimise chunk tick iteration
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
|
|
index 2d133ae656f7420d6ceec14bc591721cff815479..6f12d91f73e04c25fb0bc9054dc7d49de16e614a 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
|
|
@@ -149,21 +149,21 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
private final AtomicInteger tickingGenerated;
|
|
public final StructureTemplateManager structureTemplateManager; // Paper - rewrite chunk system
|
|
private final String storageName;
|
|
- private final PlayerMap playerMap;
|
|
- public final Int2ObjectMap<ChunkMap.TrackedEntity> entityMap;
|
|
+ //private final PlayerMap playerMap; // Folia - region threading
|
|
+ //public final Int2ObjectMap<ChunkMap.TrackedEntity> entityMap; // Folia - region threading
|
|
private final Long2ByteMap chunkTypeCache;
|
|
private final Long2LongMap chunkSaveCooldowns;
|
|
private final Queue<Runnable> unloadQueue;
|
|
int viewDistance;
|
|
- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobDistanceMap; // Paper
|
|
- public final ReferenceOpenHashSet<ChunkHolder> needsChangeBroadcasting = new ReferenceOpenHashSet<>();
|
|
+ //public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobDistanceMap; // Paper // Folia - region threading
|
|
+ //public final ReferenceOpenHashSet<ChunkHolder> needsChangeBroadcasting = new ReferenceOpenHashSet<>(); // Folia - region threading
|
|
|
|
// Paper - rewrite chunk system
|
|
// Paper start - optimise checkDespawn
|
|
public static final int GENERAL_AREA_MAP_SQUARE_RADIUS = 40;
|
|
public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE = 16.0 * (GENERAL_AREA_MAP_SQUARE_RADIUS - 1);
|
|
public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE_SQUARED = GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE * GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE;
|
|
- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerGeneralAreaMap;
|
|
+ //public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerGeneralAreaMap; // Folia - region threading
|
|
// Paper end - optimise checkDespawn
|
|
|
|
// Paper start - distance maps
|
|
@@ -177,8 +177,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
// obviously this means a spawn range > 8 cannot be implemented
|
|
|
|
// these maps are named after spigot's uses
|
|
- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap; // this map is absent from updateMaps since it's controlled at the start of the chunkproviderserver tick
|
|
- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap;
|
|
+ //public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap; // this map is absent from updateMaps since it's controlled at the start of the chunkproviderserver tick // Folia - region threading
|
|
+ //public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap; // Folia - region threading
|
|
// Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
|
|
// Paper start - use distance map to optimise tracker
|
|
public static boolean isLegacyTrackingEntity(Entity entity) {
|
|
@@ -187,11 +187,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
|
|
// inlined EnumMap, TrackingRange.TrackingRangeType
|
|
static final org.spigotmc.TrackingRange.TrackingRangeType[] TRACKING_RANGE_TYPES = org.spigotmc.TrackingRange.TrackingRangeType.values();
|
|
- public final com.destroystokyo.paper.util.misc.PlayerAreaMap[] playerEntityTrackerTrackMaps;
|
|
- final int[] entityTrackerTrackRanges;
|
|
- public final int getEntityTrackerRange(final int ordinal) {
|
|
- return this.entityTrackerTrackRanges[ordinal];
|
|
- }
|
|
+ // Folia - region threading
|
|
|
|
private int convertSpigotRangeToVanilla(final int vanilla) {
|
|
return MinecraftServer.getServer().getScaledTrackingDistance(vanilla);
|
|
@@ -203,40 +199,29 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
int chunkX = MCUtil.getChunkCoordinate(player.getX());
|
|
int chunkZ = MCUtil.getChunkCoordinate(player.getZ());
|
|
// Note: players need to be explicitly added to distance maps before they can be updated
|
|
- this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
|
|
+ //this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning // Folia - region threading
|
|
// Paper start - per player mob spawning
|
|
- if (this.playerMobDistanceMap != null) {
|
|
- this.playerMobDistanceMap.add(player, chunkX, chunkZ, io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(player));
|
|
- }
|
|
+ // Folia - region threading
|
|
// Paper end - per player mob spawning
|
|
// Paper start - use distance map to optimise entity tracker
|
|
- for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) {
|
|
- com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i];
|
|
- int trackRange = this.entityTrackerTrackRanges[i];
|
|
-
|
|
- trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player)));
|
|
- }
|
|
+ // Folia - region threading
|
|
// Paper end - use distance map to optimise entity tracker
|
|
- this.playerGeneralAreaMap.add(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn
|
|
+ //this.playerGeneralAreaMap.add(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn // Folia - region threading
|
|
}
|
|
|
|
void removePlayerFromDistanceMaps(ServerPlayer player) {
|
|
this.level.playerChunkLoader.removePlayer(player); // Paper - replace chunk loader
|
|
|
|
// Paper start - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
|
|
- this.playerMobSpawnMap.remove(player);
|
|
- this.playerChunkTickRangeMap.remove(player);
|
|
+ this.level.getCurrentWorldData().mobSpawnMap.remove(player); // Folia - region threading
|
|
+ //this.playerChunkTickRangeMap.remove(player); // Folia - region threading
|
|
// Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
|
|
- this.playerGeneralAreaMap.remove(player); // Paper - optimise checkDespawns
|
|
+ //this.playerGeneralAreaMap.remove(player); // Paper - optimise checkDespawns // Folia - region threading
|
|
// Paper start - per player mob spawning
|
|
- if (this.playerMobDistanceMap != null) {
|
|
- this.playerMobDistanceMap.remove(player);
|
|
- }
|
|
+ // Folia - region threading
|
|
// Paper end - per player mob spawning
|
|
// Paper start - use distance map to optimise tracker
|
|
- for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) {
|
|
- this.playerEntityTrackerTrackMaps[i].remove(player);
|
|
- }
|
|
+ // Folia - region threading
|
|
// Paper end - use distance map to optimise tracker
|
|
}
|
|
|
|
@@ -245,21 +230,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
int chunkZ = MCUtil.getChunkCoordinate(player.getZ());
|
|
// Note: players need to be explicitly added to distance maps before they can be updated
|
|
this.level.playerChunkLoader.updatePlayer(player); // Paper - replace chunk loader
|
|
- this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
|
|
+ //this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning // Folia - region threading
|
|
// Paper start - per player mob spawning
|
|
- if (this.playerMobDistanceMap != null) {
|
|
- this.playerMobDistanceMap.update(player, chunkX, chunkZ, io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(player));
|
|
- }
|
|
+ // Folia - region threading
|
|
// Paper end - per player mob spawning
|
|
// Paper start - use distance map to optimise entity tracker
|
|
- for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) {
|
|
- com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i];
|
|
- int trackRange = this.entityTrackerTrackRanges[i];
|
|
-
|
|
- trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player)));
|
|
- }
|
|
+ // Folia - region threading
|
|
// Paper end - use distance map to optimise entity tracker
|
|
- this.playerGeneralAreaMap.update(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn
|
|
+ //this.playerGeneralAreaMap.update(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn // Folia - region threading
|
|
}
|
|
// Paper end
|
|
// Paper start
|
|
@@ -297,8 +275,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
super(session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
|
|
// Paper - rewrite chunk system
|
|
this.tickingGenerated = new AtomicInteger();
|
|
- this.playerMap = new PlayerMap();
|
|
- this.entityMap = new Int2ObjectOpenHashMap();
|
|
+ //this.playerMap = new PlayerMap(); // Folia - region threading
|
|
+ //this.entityMap = new Int2ObjectOpenHashMap(); // Folia - region threading
|
|
this.chunkTypeCache = new Long2ByteOpenHashMap();
|
|
this.chunkSaveCooldowns = new Long2LongOpenHashMap();
|
|
this.unloadQueue = Queues.newConcurrentLinkedQueue();
|
|
@@ -343,96 +321,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
this.setViewDistance(viewDistance);
|
|
// Paper start
|
|
this.dataRegionManager = new io.papermc.paper.chunk.SingleThreadChunkRegionManager(this.level, 2, (1.0 / 3.0), 1, 6, "Data", DataRegionData::new, DataRegionSectionData::new);
|
|
- this.regionManagers.add(this.dataRegionManager);
|
|
+ //this.regionManagers.add(this.dataRegionManager); // Folia - region threading
|
|
// Paper end
|
|
- this.playerMobDistanceMap = this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets) : null; // Paper
|
|
+ //this.playerMobDistanceMap = this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets) : null; // Paper // Folia - region threading
|
|
// Paper start - use distance map to optimise entity tracker
|
|
- this.playerEntityTrackerTrackMaps = new com.destroystokyo.paper.util.misc.PlayerAreaMap[TRACKING_RANGE_TYPES.length];
|
|
- this.entityTrackerTrackRanges = new int[TRACKING_RANGE_TYPES.length];
|
|
-
|
|
- org.spigotmc.SpigotWorldConfig spigotWorldConfig = this.level.spigotConfig;
|
|
-
|
|
- for (int ordinal = 0, len = TRACKING_RANGE_TYPES.length; ordinal < len; ++ordinal) {
|
|
- org.spigotmc.TrackingRange.TrackingRangeType trackingRangeType = TRACKING_RANGE_TYPES[ordinal];
|
|
- int configuredSpigotValue;
|
|
- switch (trackingRangeType) {
|
|
- case PLAYER:
|
|
- configuredSpigotValue = spigotWorldConfig.playerTrackingRange;
|
|
- break;
|
|
- case ANIMAL:
|
|
- configuredSpigotValue = spigotWorldConfig.animalTrackingRange;
|
|
- break;
|
|
- case MONSTER:
|
|
- configuredSpigotValue = spigotWorldConfig.monsterTrackingRange;
|
|
- break;
|
|
- case MISC:
|
|
- configuredSpigotValue = spigotWorldConfig.miscTrackingRange;
|
|
- break;
|
|
- case OTHER:
|
|
- configuredSpigotValue = spigotWorldConfig.otherTrackingRange;
|
|
- break;
|
|
- case ENDERDRAGON:
|
|
- configuredSpigotValue = EntityType.ENDER_DRAGON.clientTrackingRange() * 16;
|
|
- break;
|
|
- default:
|
|
- throw new IllegalStateException("Missing case for enum " + trackingRangeType);
|
|
- }
|
|
- configuredSpigotValue = convertSpigotRangeToVanilla(configuredSpigotValue);
|
|
-
|
|
- int trackRange = (configuredSpigotValue >>> 4) + ((configuredSpigotValue & 15) != 0 ? 1 : 0);
|
|
- this.entityTrackerTrackRanges[ordinal] = trackRange;
|
|
-
|
|
- this.playerEntityTrackerTrackMaps[ordinal] = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets);
|
|
- }
|
|
+ // Folia - region threading
|
|
// Paper end - use distance map to optimise entity tracker
|
|
// Paper start - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
|
|
- this.playerChunkTickRangeMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets,
|
|
- (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
|
|
- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> newState) -> {
|
|
- LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ);
|
|
- if (chunk != null) {
|
|
- chunk.updateGeneralAreaCache(newState);
|
|
- }
|
|
- });
|
|
+ // Folia - region threading
|
|
// Paper end - optimise checkDespawn
|
|
}
|
|
|
|
@@ -460,28 +360,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
}
|
|
|
|
// Paper start
|
|
- public void updatePlayerMobTypeMap(Entity entity) {
|
|
- if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
|
|
- return;
|
|
- }
|
|
- int index = entity.getType().getCategory().ordinal();
|
|
-
|
|
- final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> inRange = this.playerMobDistanceMap.getObjectsInRange(entity.chunkPosition());
|
|
- if (inRange == null) {
|
|
- return;
|
|
- }
|
|
- final Object[] backingSet = inRange.getBackingSet();
|
|
- for (int i = 0; i < backingSet.length; i++) {
|
|
- if (!(backingSet[i] instanceof final ServerPlayer player)) {
|
|
- continue;
|
|
- }
|
|
- ++player.mobCounts[index];
|
|
- }
|
|
- }
|
|
-
|
|
- public int getMobCountNear(ServerPlayer entityPlayer, net.minecraft.world.entity.MobCategory mobCategory) {
|
|
- return entityPlayer.mobCounts[mobCategory.ordinal()];
|
|
- }
|
|
+ // Folia - region threading - revert per player mob caps
|
|
// Paper end
|
|
|
|
private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) {
|
|
@@ -750,6 +629,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
// Paper start
|
|
// rets true if to prevent the entity from being added
|
|
public static boolean checkDupeUUID(ServerLevel level, Entity entity) {
|
|
+ // Folia start - region threading
|
|
+ if (true) {
|
|
+ // TODO fix this shit later
|
|
+ return false;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode mode = level.paperConfig().entities.spawning.duplicateUuid.mode;
|
|
if (mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.WARN
|
|
&& mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.DELETE
|
|
@@ -1010,6 +895,38 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
}
|
|
|
|
final boolean anyPlayerCloseEnoughForSpawning(ChunkHolder playerchunk, ChunkPos chunkcoordintpair, boolean reducedRange) {
|
|
+ // Folia start - region threading
|
|
+ if (true) {
|
|
+ java.util.List<ServerPlayer> 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<ServerPlayer> playersInRange = reducedRange ? playerchunk.playersInMobSpawnRange : playerchunk.playersInChunkTickRange;
|
|
@@ -1055,7 +972,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
return List.of();
|
|
} else {
|
|
Builder<ServerPlayer> builder = ImmutableList.builder();
|
|
- Iterator iterator = this.playerMap.getPlayers(i).iterator();
|
|
+ Iterator iterator = this.level.getLocalPlayers().iterator(); // Folia - region threading
|
|
|
|
while (iterator.hasNext()) {
|
|
ServerPlayer entityplayer = (ServerPlayer) iterator.next();
|
|
@@ -1084,25 +1001,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
}
|
|
|
|
void updatePlayerStatus(ServerPlayer player, boolean added) {
|
|
- boolean flag1 = this.skipPlayer(player);
|
|
- boolean flag2 = this.playerMap.ignoredOrUnknown(player);
|
|
- int i = SectionPos.blockToSectionCoord(player.getBlockX());
|
|
- int j = SectionPos.blockToSectionCoord(player.getBlockZ());
|
|
+ // Folia - region threading
|
|
|
|
if (added) {
|
|
- this.playerMap.addPlayer(ChunkPos.asLong(i, j), player, flag1);
|
|
+ // Folia - region threading
|
|
this.updatePlayerPos(player);
|
|
- if (!flag1) {
|
|
- this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player);
|
|
- }
|
|
+ // Folia - region threading
|
|
this.addPlayerToDistanceMaps(player); // Paper - distance maps
|
|
} else {
|
|
SectionPos sectionposition = player.getLastSectionPos();
|
|
|
|
- this.playerMap.removePlayer(sectionposition.chunk().toLong(), player);
|
|
- if (!flag2) {
|
|
- this.distanceManager.removePlayer(sectionposition, player);
|
|
- }
|
|
+ // Folia - region threading
|
|
+ // Folia - region threading
|
|
this.removePlayerFromDistanceMaps(player); // Paper - distance maps
|
|
}
|
|
|
|
@@ -1121,43 +1031,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
public void move(ServerPlayer player) {
|
|
// Paper - delay this logic for the entity tracker tick, no need to duplicate it
|
|
|
|
- int i = SectionPos.blockToSectionCoord(player.getBlockX());
|
|
- int j = SectionPos.blockToSectionCoord(player.getBlockZ());
|
|
- SectionPos sectionposition = player.getLastSectionPos();
|
|
- SectionPos sectionposition1 = SectionPos.of((EntityAccess) player);
|
|
- long k = sectionposition.chunk().toLong();
|
|
- long l = sectionposition1.chunk().toLong();
|
|
- boolean flag = this.playerMap.ignored(player);
|
|
- boolean flag1 = this.skipPlayer(player);
|
|
- boolean flag2 = sectionposition.asLong() != sectionposition1.asLong();
|
|
-
|
|
- if (flag2 || flag != flag1) {
|
|
- this.updatePlayerPos(player);
|
|
- if (!flag) {
|
|
- this.distanceManager.removePlayer(sectionposition, player);
|
|
- }
|
|
-
|
|
- if (!flag1) {
|
|
- this.distanceManager.addPlayer(sectionposition1, player);
|
|
- }
|
|
-
|
|
- if (!flag && flag1) {
|
|
- this.playerMap.ignorePlayer(player);
|
|
- }
|
|
-
|
|
- if (flag && !flag1) {
|
|
- this.playerMap.unIgnorePlayer(player);
|
|
- }
|
|
-
|
|
- if (k != l) {
|
|
- this.playerMap.updatePlayer(k, l, player);
|
|
- }
|
|
- }
|
|
-
|
|
- int i1 = sectionposition.x();
|
|
- int j1 = sectionposition.z();
|
|
- int k1;
|
|
- int l1;
|
|
+ // Folia - region threading - none of this logic is relevant anymore thanks to the player chunk loader
|
|
|
|
// Paper - replaced by PlayerChunkLoader
|
|
|
|
@@ -1180,9 +1054,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
public void addEntity(Entity entity) {
|
|
org.spigotmc.AsyncCatcher.catchOp("entity track"); // Spigot
|
|
// Paper start - ignore and warn about illegal addEntity calls instead of crashing server
|
|
- if (!entity.valid || entity.level != this.level || this.entityMap.containsKey(entity.getId())) {
|
|
+ if (!entity.valid || entity.level != this.level || entity.tracker != null) { // Folia - region threading
|
|
LOGGER.error("Illegal ChunkMap::addEntity for world " + this.level.getWorld().getName()
|
|
- + ": " + entity + (this.entityMap.containsKey(entity.getId()) ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable());
|
|
+ + ": " + entity + (entity.tracker != null ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable());
|
|
return;
|
|
}
|
|
if (entity instanceof ServerPlayer && ((ServerPlayer) entity).supressTrackerForLogin) return; // Delay adding to tracker until after list packets
|
|
@@ -1195,27 +1069,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
if (i != 0) {
|
|
int j = entitytypes.updateInterval();
|
|
|
|
- if (this.entityMap.containsKey(entity.getId())) {
|
|
+ if (entity.tracker != null) { // Folia - region threading
|
|
throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Entity is already tracked!"));
|
|
} else {
|
|
ChunkMap.TrackedEntity playerchunkmap_entitytracker = new ChunkMap.TrackedEntity(entity, i, j, entitytypes.trackDeltas());
|
|
|
|
entity.tracker = playerchunkmap_entitytracker; // Paper - Fast access to tracker
|
|
- this.entityMap.put(entity.getId(), playerchunkmap_entitytracker);
|
|
- playerchunkmap_entitytracker.updatePlayers(entity.getPlayersInTrackRange()); // Paper - don't search all players
|
|
+ // Folia - region threading
|
|
+ playerchunkmap_entitytracker.updatePlayers(this.level.getLocalPlayers()); // Paper - don't search all players // Folia - region threading
|
|
if (entity instanceof ServerPlayer) {
|
|
ServerPlayer entityplayer = (ServerPlayer) entity;
|
|
|
|
this.updatePlayerStatus(entityplayer, true);
|
|
- ObjectIterator objectiterator = this.entityMap.values().iterator();
|
|
-
|
|
- while (objectiterator.hasNext()) {
|
|
- ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = (ChunkMap.TrackedEntity) objectiterator.next();
|
|
-
|
|
- if (playerchunkmap_entitytracker1.entity != entityplayer) {
|
|
- playerchunkmap_entitytracker1.updatePlayer(entityplayer);
|
|
+ // Folia start - region threading
|
|
+ for (Entity possible : this.level.getCurrentWorldData().getLocalEntities()) {
|
|
+ if (possible.tracker != null) {
|
|
+ possible.tracker.updatePlayer(entityplayer);
|
|
}
|
|
}
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
}
|
|
@@ -1229,16 +1101,16 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
ServerPlayer entityplayer = (ServerPlayer) entity;
|
|
|
|
this.updatePlayerStatus(entityplayer, false);
|
|
- ObjectIterator objectiterator = this.entityMap.values().iterator();
|
|
-
|
|
- while (objectiterator.hasNext()) {
|
|
- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next();
|
|
-
|
|
- playerchunkmap_entitytracker.removePlayer(entityplayer);
|
|
+ // Folia start - region threading
|
|
+ for (Entity possible : this.level.getCurrentWorldData().getLocalEntities()) {
|
|
+ if (possible.tracker != null) {
|
|
+ possible.tracker.removePlayer(entityplayer);
|
|
+ }
|
|
}
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
- ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = (ChunkMap.TrackedEntity) this.entityMap.remove(entity.getId());
|
|
+ ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = entity.tracker; // Folia - region threading
|
|
|
|
if (playerchunkmap_entitytracker1 != null) {
|
|
playerchunkmap_entitytracker1.broadcastRemoved();
|
|
@@ -1248,25 +1120,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
|
|
// Paper start - optimised tracker
|
|
private final void processTrackQueue() {
|
|
- this.level.timings.tracker1.startTiming();
|
|
- try {
|
|
- for (TrackedEntity tracker : this.entityMap.values()) {
|
|
- // update tracker entry
|
|
- tracker.updatePlayers(tracker.entity.getPlayersInTrackRange());
|
|
- }
|
|
- } finally {
|
|
- this.level.timings.tracker1.stopTiming();
|
|
- }
|
|
-
|
|
-
|
|
- this.level.timings.tracker2.startTiming();
|
|
- try {
|
|
- for (TrackedEntity tracker : this.entityMap.values()) {
|
|
- tracker.serverEntity.sendChanges();
|
|
+ // Folia start - region threading
|
|
+ List<ServerPlayer> players = this.level.getLocalPlayers(); // Folia - region threading
|
|
+ for (Entity entity : this.level.getCurrentWorldData().getLocalEntities()) {
|
|
+ TrackedEntity tracker = entity.tracker;
|
|
+ if (tracker == null) {
|
|
+ continue;
|
|
}
|
|
- } finally {
|
|
- this.level.timings.tracker2.stopTiming();
|
|
+ tracker.updatePlayers(players);
|
|
+ tracker.removeNonTickThreadPlayers();
|
|
+ tracker.serverEntity.sendChanges();
|
|
}
|
|
+ // Folia end - region threading
|
|
}
|
|
// Paper end - optimised tracker
|
|
|
|
@@ -1277,51 +1142,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
return;
|
|
}
|
|
// Paper end - optimized tracker
|
|
- List<ServerPlayer> list = Lists.newArrayList();
|
|
- List<ServerPlayer> list1 = this.level.players();
|
|
- ObjectIterator objectiterator = this.entityMap.values().iterator();
|
|
- level.timings.tracker1.startTiming(); // Paper
|
|
-
|
|
- ChunkMap.TrackedEntity playerchunkmap_entitytracker;
|
|
-
|
|
- while (objectiterator.hasNext()) {
|
|
- playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next();
|
|
- SectionPos sectionposition = playerchunkmap_entitytracker.lastSectionPos;
|
|
- SectionPos sectionposition1 = SectionPos.of((EntityAccess) playerchunkmap_entitytracker.entity);
|
|
- boolean flag = !Objects.equals(sectionposition, sectionposition1);
|
|
-
|
|
- if (flag) {
|
|
- playerchunkmap_entitytracker.updatePlayers(list1);
|
|
- Entity entity = playerchunkmap_entitytracker.entity;
|
|
-
|
|
- if (entity instanceof ServerPlayer) {
|
|
- list.add((ServerPlayer) entity);
|
|
- }
|
|
-
|
|
- playerchunkmap_entitytracker.lastSectionPos = sectionposition1;
|
|
- }
|
|
-
|
|
- if (flag || this.distanceManager.inEntityTickingRange(sectionposition1.chunk().toLong())) {
|
|
- playerchunkmap_entitytracker.serverEntity.sendChanges();
|
|
- }
|
|
- }
|
|
- level.timings.tracker1.stopTiming(); // Paper
|
|
-
|
|
- if (!list.isEmpty()) {
|
|
- objectiterator = this.entityMap.values().iterator();
|
|
-
|
|
- level.timings.tracker2.startTiming(); // Paper
|
|
- while (objectiterator.hasNext()) {
|
|
- playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next();
|
|
- playerchunkmap_entitytracker.updatePlayers(list);
|
|
- }
|
|
- level.timings.tracker2.stopTiming(); // Paper
|
|
- }
|
|
+ // Folia - region threading
|
|
|
|
}
|
|
|
|
public void broadcast(Entity entity, Packet<?> packet) {
|
|
- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) this.entityMap.get(entity.getId());
|
|
+ ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) entity.tracker; // Folia - region threading
|
|
|
|
if (playerchunkmap_entitytracker != null) {
|
|
playerchunkmap_entitytracker.broadcast(packet);
|
|
@@ -1330,7 +1156,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
}
|
|
|
|
protected void broadcastAndSend(Entity entity, Packet<?> packet) {
|
|
- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) this.entityMap.get(entity.getId());
|
|
+ ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) entity.tracker; // Folia - region threading
|
|
|
|
if (playerchunkmap_entitytracker != null) {
|
|
playerchunkmap_entitytracker.broadcastAndSend(packet);
|
|
@@ -1517,41 +1343,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
this.lastSectionPos = SectionPos.of((EntityAccess) entity);
|
|
}
|
|
|
|
- // Paper start - use distance map to optimise tracker
|
|
- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> lastTrackerCandidates;
|
|
-
|
|
- final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newTrackerCandidates) {
|
|
- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> oldTrackerCandidates = this.lastTrackerCandidates;
|
|
- this.lastTrackerCandidates = newTrackerCandidates;
|
|
-
|
|
- if (newTrackerCandidates != null) {
|
|
- Object[] rawData = newTrackerCandidates.getBackingSet();
|
|
- for (int i = 0, len = rawData.length; i < len; ++i) {
|
|
- Object raw = rawData[i];
|
|
- if (!(raw instanceof ServerPlayer)) {
|
|
- continue;
|
|
- }
|
|
- ServerPlayer player = (ServerPlayer)raw;
|
|
- this.updatePlayer(player);
|
|
- }
|
|
- }
|
|
-
|
|
- if (oldTrackerCandidates == newTrackerCandidates) {
|
|
- // this is likely the case.
|
|
- // means there has been no range changes, so we can just use the above for tracking.
|
|
- return;
|
|
- }
|
|
-
|
|
- // stuff could have been removed, so we need to check the trackedPlayers set
|
|
- // for players that were removed
|
|
-
|
|
- for (ServerPlayerConnection conn : this.seenBy.toArray(new ServerPlayerConnection[0])) { // avoid CME
|
|
- if (newTrackerCandidates == null || !newTrackerCandidates.contains(conn.getPlayer())) {
|
|
- this.updatePlayer(conn.getPlayer());
|
|
- }
|
|
- }
|
|
- }
|
|
- // Paper end - use distance map to optimise tracker
|
|
+ // Folia - region threading
|
|
|
|
public boolean equals(Object object) {
|
|
return object instanceof ChunkMap.TrackedEntity ? ((ChunkMap.TrackedEntity) object).entity.getId() == this.entity.getId() : false;
|
|
@@ -1598,6 +1390,28 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
}
|
|
|
|
}
|
|
+ // Folia start - region threading
|
|
+ public void removeNonTickThreadPlayers() {
|
|
+ boolean foundToRemove = false;
|
|
+ for (ServerPlayerConnection conn : this.seenBy) {
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(conn.getPlayer())) {
|
|
+ foundToRemove = true;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (!foundToRemove) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ for (ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) {
|
|
+ ServerPlayer player = conn.getPlayer();
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(player)) {
|
|
+ this.removePlayer(player);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
|
|
public void updatePlayer(ServerPlayer player) {
|
|
org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot
|
|
@@ -1613,10 +1427,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
boolean flag = d1 <= d2 && this.entity.broadcastToPlayer(player);
|
|
|
|
// CraftBukkit start - respect vanish API
|
|
- if (!player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) {
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(player) || !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Folia - region threading
|
|
flag = false;
|
|
}
|
|
// CraftBukkit end
|
|
+ // Folia start - region threading
|
|
+ if ((this.entity instanceof ServerPlayer thisEntity) && thisEntity.broadcastedDeath) {
|
|
+ flag = false;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
if (flag) {
|
|
if (this.seenBy.add(player.connection)) {
|
|
this.serverEntity.addPairing(player);
|
|
diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
|
|
index 88fca8b160df6804f30ed2cf8cf1f645085434e2..341650384498eebe3f7a3315c398bec994a3195b 100644
|
|
--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
|
|
+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
|
|
@@ -200,14 +200,14 @@ public abstract class DistanceManager {
|
|
public int getNaturalSpawnChunkCount() {
|
|
// Paper start - use distance map to implement
|
|
// note: this is the spawn chunk count
|
|
- return this.chunkMap.playerChunkTickRangeMap.size();
|
|
+ return this.chunkMap.level.getCurrentWorldData().mobSpawnMap.size(); // Folia - region threading
|
|
// Paper end - use distance map to implement
|
|
}
|
|
|
|
public boolean hasPlayersNearby(long chunkPos) {
|
|
// Paper start - use distance map to implement
|
|
// note: this is the is spawn chunk method
|
|
- return this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(chunkPos) != null;
|
|
+ return this.chunkMap.level.getCurrentWorldData().mobSpawnMap.getObjectsInRange(chunkPos) != null; // Folia - region threading
|
|
// Paper end - use distance map to implement
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
index 736f37979c882e41e7571202df38eb6a2923fcb0..a493d8f2677c776951fbc20ebf1e61c91b9864ee 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
@@ -61,73 +61,42 @@ public class ServerChunkCache extends ChunkSource {
|
|
public final ServerChunkCache.MainThreadExecutor mainThreadProcessor;
|
|
public final ChunkMap chunkMap;
|
|
private final DimensionDataStorage dataStorage;
|
|
- private long lastInhabitedUpdate;
|
|
+ //private long lastInhabitedUpdate; // Folia - region threading
|
|
public boolean spawnEnemies = true;
|
|
public boolean spawnFriendlies = true;
|
|
private static final int CACHE_SIZE = 4;
|
|
private final long[] lastChunkPos = new long[4];
|
|
private final ChunkStatus[] lastChunkStatus = new ChunkStatus[4];
|
|
private final ChunkAccess[] lastChunk = new ChunkAccess[4];
|
|
- @Nullable
|
|
- @VisibleForDebug
|
|
- private NaturalSpawner.SpawnState lastSpawnState;
|
|
+ // Folia - moved to regionised world data
|
|
// Paper start
|
|
- final com.destroystokyo.paper.util.concurrent.WeakSeqLock loadedChunkMapSeqLock = new com.destroystokyo.paper.util.concurrent.WeakSeqLock();
|
|
- final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<LevelChunk> loadedChunkMap = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(8192, 0.5f);
|
|
+ // Folia - region threading
|
|
+ final ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable<LevelChunk> loadedChunkMap = new ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable<>(8192, 0.5f); // Folia - region threading
|
|
|
|
- private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4];
|
|
+ // Folia - region threading
|
|
|
|
private static int getChunkCacheKey(int x, int z) {
|
|
return x & 3 | ((z & 3) << 2);
|
|
}
|
|
|
|
public void addLoadedChunk(LevelChunk chunk) {
|
|
- this.loadedChunkMapSeqLock.acquireWrite();
|
|
- try {
|
|
+ synchronized (this.loadedChunkMap) { // Folia - region threading
|
|
this.loadedChunkMap.put(chunk.coordinateKey, chunk);
|
|
- } finally {
|
|
- this.loadedChunkMapSeqLock.releaseWrite();
|
|
- }
|
|
-
|
|
- // rewrite cache if we have to
|
|
- // we do this since we also cache null chunks
|
|
- int cacheKey = getChunkCacheKey(chunk.locX, chunk.locZ);
|
|
+ } // Folia - region threading
|
|
|
|
- this.lastLoadedChunks[cacheKey] = chunk;
|
|
+ // Folia - region threading
|
|
}
|
|
|
|
public void removeLoadedChunk(LevelChunk chunk) {
|
|
- this.loadedChunkMapSeqLock.acquireWrite();
|
|
- try {
|
|
+ synchronized (this.loadedChunkMap) { // Folia - region threading
|
|
this.loadedChunkMap.remove(chunk.coordinateKey);
|
|
- } finally {
|
|
- this.loadedChunkMapSeqLock.releaseWrite();
|
|
- }
|
|
-
|
|
- // rewrite cache if we have to
|
|
- // we do this since we also cache null chunks
|
|
- int cacheKey = getChunkCacheKey(chunk.locX, chunk.locZ);
|
|
+ } // Folia - region threading
|
|
|
|
- LevelChunk cachedChunk = this.lastLoadedChunks[cacheKey];
|
|
- if (cachedChunk != null && cachedChunk.coordinateKey == chunk.coordinateKey) {
|
|
- this.lastLoadedChunks[cacheKey] = null;
|
|
- }
|
|
+ // Folia - region threading
|
|
}
|
|
|
|
public final LevelChunk getChunkAtIfLoadedMainThread(int x, int z) {
|
|
- int cacheKey = getChunkCacheKey(x, z);
|
|
-
|
|
- LevelChunk cachedChunk = this.lastLoadedChunks[cacheKey];
|
|
- if (cachedChunk != null && cachedChunk.locX == x & cachedChunk.locZ == z) {
|
|
- return cachedChunk;
|
|
- }
|
|
-
|
|
- long chunkKey = ChunkPos.asLong(x, z);
|
|
-
|
|
- cachedChunk = this.loadedChunkMap.get(chunkKey);
|
|
- // Skipping a null check to avoid extra instructions to improve inline capability
|
|
- this.lastLoadedChunks[cacheKey] = cachedChunk;
|
|
- return cachedChunk;
|
|
+ return this.loadedChunkMap.get(ChunkPos.asLong(x, z)); // Folia - region threading
|
|
}
|
|
|
|
public final LevelChunk getChunkAtIfLoadedMainThreadNoCache(int x, int z) {
|
|
@@ -142,7 +111,7 @@ public class ServerChunkCache extends ChunkSource {
|
|
return (LevelChunk)this.getChunk(x, z, ChunkStatus.FULL, true);
|
|
}
|
|
|
|
- long chunkFutureAwaitCounter; // Paper - private -> package private
|
|
+ final java.util.concurrent.atomic.AtomicLong chunkFutureAwaitCounter = new java.util.concurrent.atomic.AtomicLong(); // Paper - private -> package private // Folia - region threading - TODO MERGE INTO CHUNK SYSTEM PATCH
|
|
|
|
public void getEntityTickingChunkAsync(int x, int z, java.util.function.Consumer<LevelChunk> onLoad) {
|
|
io.papermc.paper.chunk.system.ChunkSystem.scheduleTickingState(
|
|
@@ -293,8 +262,7 @@ public class ServerChunkCache extends ChunkSource {
|
|
this.distanceManager.removeTicket(ticketType, chunkPos, ticketLevel, identifier);
|
|
}
|
|
|
|
- public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> tickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
|
|
- public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
|
|
+ // 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<DimensionDataStorage> persistentStateManagerFactory) {
|
|
@@ -368,26 +336,7 @@ public class ServerChunkCache extends ChunkSource {
|
|
public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) {
|
|
long k = ChunkPos.asLong(x, z);
|
|
|
|
- if (io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system
|
|
- return this.getChunkAtIfLoadedMainThread(x, z);
|
|
- }
|
|
-
|
|
- LevelChunk ret = null;
|
|
- long readlock;
|
|
- do {
|
|
- readlock = this.loadedChunkMapSeqLock.acquireRead();
|
|
- try {
|
|
- ret = this.loadedChunkMap.get(k);
|
|
- } catch (Throwable thr) {
|
|
- if (thr instanceof ThreadDeath) {
|
|
- throw (ThreadDeath)thr;
|
|
- }
|
|
- // re-try, this means a CME occurred...
|
|
- continue;
|
|
- }
|
|
- } while (!this.loadedChunkMapSeqLock.tryReleaseRead(readlock));
|
|
-
|
|
- return ret;
|
|
+ return this.loadedChunkMap.get(k); // Folia - region threading
|
|
}
|
|
// Paper end
|
|
// Paper start - async chunk io
|
|
@@ -483,6 +432,7 @@ public class ServerChunkCache extends ChunkSource {
|
|
}
|
|
|
|
public CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> getChunkFuture(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
|
|
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
|
|
boolean flag1 = io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system
|
|
CompletableFuture completablefuture;
|
|
|
|
@@ -659,10 +609,11 @@ public class ServerChunkCache extends ChunkSource {
|
|
}
|
|
|
|
private void tickChunks() {
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading
|
|
long i = this.level.getGameTime();
|
|
- long j = i - this.lastInhabitedUpdate;
|
|
+ long j = 1; // Folia - region threading
|
|
|
|
- this.lastInhabitedUpdate = i;
|
|
+ //this.lastInhabitedUpdate = i; // Folia - region threading
|
|
boolean flag = this.level.isDebug();
|
|
|
|
if (flag) {
|
|
@@ -670,9 +621,11 @@ public class ServerChunkCache extends ChunkSource {
|
|
} else {
|
|
// Paper start - optimize isOutisdeRange
|
|
ChunkMap playerChunkMap = this.chunkMap;
|
|
- for (ServerPlayer player : this.level.players) {
|
|
+ // Folia - region threading
|
|
+
|
|
+ for (ServerPlayer player : this.level.getLocalPlayers()) { // Folia - region threading
|
|
if (!player.affectsSpawning || player.isSpectator()) {
|
|
- playerChunkMap.playerMobSpawnMap.remove(player);
|
|
+ regionizedWorldData.mobSpawnMap.remove(player); // Folia - region threading
|
|
continue;
|
|
}
|
|
|
|
@@ -685,8 +638,9 @@ public class ServerChunkCache extends ChunkSource {
|
|
|
|
com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(player.getBukkitEntity(), (byte)chunkRange);
|
|
event.callEvent();
|
|
- if (event.isCancelled() || event.getSpawnRadius() < 0 || playerChunkMap.playerChunkTickRangeMap.getLastViewDistance(player) == -1) {
|
|
- playerChunkMap.playerMobSpawnMap.remove(player);
|
|
+ if (event.isCancelled() || event.getSpawnRadius() < 0) { // Folia - region threading
|
|
+ player.lastEntitySpawnRadiusSquared = -1.0; player.playerNaturallySpawnedEvent = null; // Folia - region threading
|
|
+ regionizedWorldData.mobSpawnMap.remove(player); // Folia - region threading
|
|
continue;
|
|
}
|
|
|
|
@@ -694,7 +648,7 @@ public class ServerChunkCache extends ChunkSource {
|
|
int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX());
|
|
int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ());
|
|
|
|
- playerChunkMap.playerMobSpawnMap.addOrUpdate(player, chunkX, chunkZ, range);
|
|
+ regionizedWorldData.mobSpawnMap.addOrUpdate(player, chunkX, chunkZ, range); // Folia - region threading
|
|
player.lastEntitySpawnRadiusSquared = (double)((range << 4) * (range << 4)); // used in anyPlayerCloseEnoughForSpawning
|
|
player.playerNaturallySpawnedEvent = event;
|
|
}
|
|
@@ -704,26 +658,19 @@ public class ServerChunkCache extends ChunkSource {
|
|
|
|
gameprofilerfiller.push("pollingChunks");
|
|
int k = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING);
|
|
- boolean flag1 = level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && worlddata.getGameTime() % level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit
|
|
+ boolean flag1 = level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getRedstoneGameTime() % level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit // Folia - region threading
|
|
|
|
gameprofilerfiller.push("naturalSpawnCount");
|
|
this.level.timings.countNaturalMobs.startTiming(); // Paper - timings
|
|
int l = this.distanceManager.getNaturalSpawnChunkCount();
|
|
// Paper start - per player mob spawning
|
|
NaturalSpawner.SpawnState spawnercreature_d; // moved down
|
|
- if ((this.spawnFriendlies || this.spawnEnemies) && this.chunkMap.playerMobDistanceMap != null) { // don't count mobs when animals and monsters are disabled
|
|
- // re-set mob counts
|
|
- for (ServerPlayer player : this.level.players) {
|
|
- Arrays.fill(player.mobCounts, 0);
|
|
- }
|
|
- spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, null, true);
|
|
- } else {
|
|
- spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, this.chunkMap.playerMobDistanceMap == null ? new LocalMobCapCalculator(this.chunkMap) : null, false);
|
|
- }
|
|
- // Paper end
|
|
+ // Folia start - threaded regions - revert per-player mob caps
|
|
+ spawnercreature_d = this.spawnFriendlies || this.spawnEnemies ? NaturalSpawner.createState(l, regionizedWorldData.getLocalEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap)) : null; // Folia - region threading
|
|
+ // Folia end - threaded regions - revert per-player mob caps
|
|
this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings
|
|
|
|
- this.lastSpawnState = spawnercreature_d;
|
|
+ regionizedWorldData.lastSpawnState = spawnercreature_d; // Folia - region threading
|
|
gameprofilerfiller.popPush("filteringLoadedChunks");
|
|
// Paper - moved down
|
|
this.level.timings.chunkTicks.startTiming(); // Paper
|
|
@@ -731,17 +678,17 @@ public class ServerChunkCache extends ChunkSource {
|
|
// Paper - moved down
|
|
|
|
gameprofilerfiller.popPush("spawnAndTick");
|
|
- boolean flag2 = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit
|
|
+ boolean flag2 = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !regionizedWorldData.getLocalPlayers().isEmpty(); // CraftBukkit // Folia - region threading
|
|
|
|
// Paper - only shuffle if per-player mob spawning is disabled
|
|
// Paper - moved natural spawn event up
|
|
// Paper start - optimise chunk tick iteration
|
|
Iterator<LevelChunk> iterator1;
|
|
- if (this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
|
|
- iterator1 = this.entityTickingChunks.iterator();
|
|
+ if (true) { // Folia - region threading - revert per player mob caps, except for this - WTF are they doing?
|
|
+ iterator1 = regionizedWorldData.getEntityTickingChunks().iterator(); // Folia - region threading
|
|
} else {
|
|
- iterator1 = this.entityTickingChunks.unsafeIterator();
|
|
- List<LevelChunk> shuffled = Lists.newArrayListWithCapacity(this.entityTickingChunks.size());
|
|
+ iterator1 = regionizedWorldData.getEntityTickingChunks().unsafeIterator(); // Folia - region threading
|
|
+ List<LevelChunk> shuffled = Lists.newArrayListWithCapacity(regionizedWorldData.getEntityTickingChunks().size()); // Folia - region threading
|
|
while (iterator1.hasNext()) {
|
|
shuffled.add(iterator1.next());
|
|
}
|
|
@@ -791,14 +738,19 @@ public class ServerChunkCache extends ChunkSource {
|
|
// Paper start - use set of chunks requiring updates, rather than iterating every single one loaded
|
|
gameprofilerfiller.popPush("broadcast");
|
|
this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing
|
|
- if (!this.chunkMap.needsChangeBroadcasting.isEmpty()) {
|
|
- ReferenceOpenHashSet<ChunkHolder> copy = this.chunkMap.needsChangeBroadcasting.clone();
|
|
- this.chunkMap.needsChangeBroadcasting.clear();
|
|
- for (ChunkHolder holder : copy) {
|
|
+ // Folia start - region threading
|
|
+ if (!this.level.needsChangeBroadcasting.isEmpty()) {
|
|
+ for (Iterator<ChunkHolder> iterator = this.level.needsChangeBroadcasting.iterator(); iterator.hasNext();) {
|
|
+ ChunkHolder holder = iterator.next();
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(holder.newChunkHolder.world, holder.pos)) {
|
|
+ continue;
|
|
+ }
|
|
+ // don't need to worry about chunk holder remove, as that can only be done by this tick thread
|
|
+ // Folia end - region threading
|
|
holder.broadcastChanges(holder.getFullChunkNowUnchecked()); // LevelChunks are NEVER unloaded
|
|
- if (holder.needsBroadcastChanges()) {
|
|
+ if (!holder.needsBroadcastChanges()) { // Folia - region threading
|
|
// I DON'T want to KNOW what DUMB plugins might be doing.
|
|
- this.chunkMap.needsChangeBroadcasting.add(holder);
|
|
+ iterator.remove(); // Folia - region threading
|
|
}
|
|
}
|
|
}
|
|
@@ -806,8 +758,8 @@ public class ServerChunkCache extends ChunkSource {
|
|
gameprofilerfiller.pop();
|
|
// Paper end - use set of chunks requiring updates, rather than iterating every single one loaded
|
|
// Paper start - controlled flush for entity tracker packets
|
|
- List<net.minecraft.network.Connection> disabledFlushes = new java.util.ArrayList<>(this.level.players.size());
|
|
- for (ServerPlayer player : this.level.players) {
|
|
+ List<net.minecraft.network.Connection> disabledFlushes = new java.util.ArrayList<>(regionizedWorldData.getLocalPlayers().size()); // Folia - region threading
|
|
+ for (ServerPlayer player : regionizedWorldData.getLocalPlayers()) { // Folia - region threading
|
|
net.minecraft.server.network.ServerGamePacketListenerImpl connection = player.connection;
|
|
if (connection != null) {
|
|
connection.connection.disableAutomaticFlush();
|
|
@@ -880,14 +832,19 @@ public class ServerChunkCache extends ChunkSource {
|
|
|
|
@Override
|
|
public void onLightUpdate(LightLayer type, SectionPos pos) {
|
|
- this.mainThreadProcessor.execute(() -> {
|
|
+ Runnable run = () -> { // Folia - region threading
|
|
ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.chunk().toLong());
|
|
|
|
if (playerchunk != null) {
|
|
playerchunk.sectionLightChanged(type, pos.y());
|
|
}
|
|
|
|
- });
|
|
+ }; // Folia - region threading
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(
|
|
+ this.level, pos.getX(), pos.getZ(), run
|
|
+ );
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
public <T> void addRegionTicket(TicketType<T> ticketType, ChunkPos pos, int radius, T argument) {
|
|
@@ -959,7 +916,8 @@ public class ServerChunkCache extends ChunkSource {
|
|
@Nullable
|
|
@VisibleForDebug
|
|
public NaturalSpawner.SpawnState getLastSpawnState() {
|
|
- return this.lastSpawnState;
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading
|
|
+ return worldData == null ? null : worldData.lastSpawnState; // Folia - region threading
|
|
}
|
|
|
|
public void removeTicketsOnClosing() {
|
|
@@ -992,8 +950,43 @@ public class ServerChunkCache extends ChunkSource {
|
|
return ServerChunkCache.this.mainThread;
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ @Override
|
|
+ public void tell(Runnable runnable) {
|
|
+ if (true) {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+ super.tell(runnable);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void executeBlocking(Runnable runnable) {
|
|
+ if (true) {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+ super.executeBlocking(runnable);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void execute(Runnable runnable) {
|
|
+ if (true) {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+ super.execute(runnable);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void executeIfPossible(Runnable runnable) {
|
|
+ if (true) {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+ super.executeIfPossible(runnable);
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Override
|
|
protected void doRunTask(Runnable task) {
|
|
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
|
|
ServerChunkCache.this.level.getProfiler().incrementCounter("runTask");
|
|
super.doRunTask(task);
|
|
}
|
|
@@ -1001,11 +994,16 @@ public class ServerChunkCache extends ChunkSource {
|
|
@Override
|
|
// CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
|
|
public boolean pollTask() {
|
|
+ // Folia start - region threading
|
|
+ if (ServerChunkCache.this.level != io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().world) {
|
|
+ throw new IllegalStateException("Polling tasks from non-owned region");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
ServerChunkCache.this.chunkMap.level.playerChunkLoader.tickMidTick(); // Paper - replace player chunk loader
|
|
if (ServerChunkCache.this.runDistanceManagerUpdates()) {
|
|
return true;
|
|
}
|
|
- return super.pollTask() | ServerChunkCache.this.level.chunkTaskScheduler.executeMainThreadTask(); // Paper - rewrite chunk system
|
|
+ return io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getData().getTaskQueueData().executeChunkTask(); // Paper - rewrite chunk system // Folia - region threading
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
index bf1a77cf9bbea4e2104b2a8c61309e740f28d51b..6c615f4f5d47f7d9cc31274800dc25824faa4a66 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
@@ -190,35 +190,34 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
public final ServerChunkCache chunkSource;
|
|
private final MinecraftServer server;
|
|
public final PrimaryLevelData serverLevelData; // CraftBukkit - type
|
|
- final EntityTickList entityTickList;
|
|
+ //final EntityTickList entityTickList; // Folia - region threading
|
|
//public final PersistentEntitySectionManager<Entity> 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<Block> blockTicks;
|
|
- private final LevelTicks<Fluid> fluidTicks;
|
|
+ //private final LevelTicks<Block> blockTicks; // Folia - region threading
|
|
+ //private final LevelTicks<Fluid> fluidTicks; // Folia - region threading
|
|
final Set<Mob> navigatingMobs;
|
|
volatile boolean isUpdatingNavigations;
|
|
protected final Raids raids;
|
|
- private final ObjectLinkedOpenHashSet<BlockEventData> blockEvents;
|
|
- private final List<BlockEventData> blockEventsToReschedule;
|
|
- private boolean handlingTick;
|
|
+ //private final ObjectLinkedOpenHashSet<BlockEventData> blockEvents; // Folia - region threading
|
|
+ //private final List<BlockEventData> blockEventsToReschedule; // Folia - region threading
|
|
+ //private boolean handlingTick; // Folia - region threading
|
|
private final List<CustomSpawner> customSpawners;
|
|
@Nullable
|
|
private final EndDragonFight dragonFight;
|
|
final Int2ObjectMap<EnderDragonPart> dragonParts;
|
|
private final StructureManager structureManager;
|
|
private final StructureCheck structureCheck;
|
|
- private final boolean tickTime;
|
|
- public long lastMidTickExecuteFailure; // Paper - execute chunk tasks mid tick
|
|
+ public final boolean tickTime; // Folia - region threading
|
|
+ // Folia - region threading
|
|
|
|
// CraftBukkit start
|
|
public final LevelStorageSource.LevelStorageAccess convertable;
|
|
public final UUID uuid;
|
|
- public boolean hasPhysicsEvent = true; // Paper
|
|
- public boolean hasEntityMoveEvent = false; // Paper
|
|
+ // Folia - region threading
|
|
private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current)
|
|
public static Throwable getAddToWorldStackTrace(Entity entity) {
|
|
final Throwable thr = new Throwable(entity + " Added to world at " + new java.util.Date());
|
|
@@ -254,6 +253,13 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
|
|
ServerChunkCache chunkProvider = this.getChunkSource();
|
|
|
|
+ // Folia start - region threading
|
|
+ // don't let players move into regions not owned
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(this, minChunkX, minChunkZ, maxChunkX, maxChunkZ)) {
|
|
+ return false;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
|
|
for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
|
|
if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) {
|
|
@@ -265,50 +271,64 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
return true;
|
|
}
|
|
|
|
- public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
|
|
- java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> 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<List<net.minecraft.world.level.chunk.ChunkAccess>> 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<List<net.minecraft.world.level.chunk.ChunkAccess>> 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<List<net.minecraft.world.level.chunk.ChunkAccess>> 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<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
|
|
List<net.minecraft.world.level.chunk.ChunkAccess> 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<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> {
|
|
if (chunk != null) {
|
|
- int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel());
|
|
ret.add(chunk);
|
|
- ticketLevels.add(ticketLevel);
|
|
chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier);
|
|
}
|
|
- if (++loadedChunks[0] == requiredChunks) {
|
|
+ if (loadedChunks.incrementAndGet() == requiredChunks) {
|
|
try {
|
|
onLoad.accept(java.util.Collections.unmodifiableList(ret));
|
|
} finally {
|
|
for (int i = 0, len = ret.size(); i < len; ++i) {
|
|
ChunkPos chunkPos = ret.get(i).getPos();
|
|
- int ticketLevel = ticketLevels.getInt(i);
|
|
|
|
chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos);
|
|
chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier);
|
|
@@ -320,11 +340,31 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
|
|
for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
|
|
io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(
|
|
- this, cx, cz, net.minecraft.world.level.chunk.ChunkStatus.FULL, true, priority, consumer
|
|
+ this, cx, cz, chunkStatus, true, priority, consumer
|
|
);
|
|
}
|
|
}
|
|
}
|
|
+ // Folia end - region threading - TODO rebase
|
|
+
|
|
+ public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
|
|
+ java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
|
|
+ // Folia - region threading - TODO MERGE INTO CHUNK SYSTEM PATCH
|
|
+
|
|
+ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
|
|
+ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
|
|
+
|
|
+ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
|
|
+ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
|
|
+
|
|
+ int minChunkX = minBlockX >> 4;
|
|
+ int maxChunkX = maxBlockX >> 4;
|
|
+
|
|
+ int minChunkZ = minBlockZ >> 4;
|
|
+ int maxChunkZ = maxBlockZ >> 4;
|
|
+
|
|
+ this.loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, priority, onLoad); // Folia - region threading - move into own function TODO rebase
|
|
+ }
|
|
|
|
// Paper start - rewrite chunk system
|
|
public final io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler chunkTaskScheduler;
|
|
@@ -444,81 +484,16 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
// Paper end
|
|
|
|
// Paper start - optimise checkDespawn
|
|
- public final List<ServerPlayer> 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<ServerPlayer> nearby;
|
|
- nearby = this.getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(Mth.floor(centerX) >> 4, Mth.floor(centerZ) >> 4);
|
|
-
|
|
- if (nearby == null) {
|
|
- return null;
|
|
- }
|
|
-
|
|
- Object[] backingSet = nearby.getBackingSet();
|
|
-
|
|
- double closestDistanceSquared = Double.MAX_VALUE;
|
|
- ServerPlayer closest = null;
|
|
-
|
|
- for (int i = 0, len = backingSet.length; i < len; ++i) {
|
|
- Object _player = backingSet[i];
|
|
- if (!(_player instanceof ServerPlayer)) {
|
|
- continue;
|
|
- }
|
|
- ServerPlayer player = (ServerPlayer)_player;
|
|
-
|
|
- double distanceSquared = player.distanceToSqr(centerX, centerY, centerZ);
|
|
- if (distanceSquared < closestDistanceSquared && condition.test(source, player)) {
|
|
- closest = player;
|
|
- closestDistanceSquared = distanceSquared;
|
|
- }
|
|
- }
|
|
-
|
|
- return closest;
|
|
- }
|
|
-
|
|
- @Override
|
|
- public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions pathfindertargetcondition, LivingEntity entityliving) {
|
|
- return this.getNearestPlayer(pathfindertargetcondition, entityliving, entityliving.getX(), entityliving.getY(), entityliving.getZ());
|
|
- }
|
|
-
|
|
- @Override
|
|
- public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions pathfindertargetcondition,
|
|
- double d0, double d1, double d2) {
|
|
- return this.getNearestPlayer(pathfindertargetcondition, null, d0, d1, d2);
|
|
- }
|
|
-
|
|
- @Override
|
|
- public List<Player> getNearbyPlayers(net.minecraft.world.entity.ai.targeting.TargetingConditions condition, LivingEntity source, AABB axisalignedbb) {
|
|
- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> nearby;
|
|
- double centerX = (axisalignedbb.maxX + axisalignedbb.minX) * 0.5;
|
|
- double centerZ = (axisalignedbb.maxZ + axisalignedbb.minZ) * 0.5;
|
|
- nearby = this.getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(Mth.floor(centerX) >> 4, Mth.floor(centerZ) >> 4);
|
|
+ // Folia - region threading
|
|
|
|
- List<Player> ret = new java.util.ArrayList<>();
|
|
+ // Folia - region threading
|
|
|
|
- if (nearby == null) {
|
|
- return ret;
|
|
- }
|
|
-
|
|
- Object[] backingSet = nearby.getBackingSet();
|
|
-
|
|
- for (int i = 0, len = backingSet.length; i < len; ++i) {
|
|
- Object _player = backingSet[i];
|
|
- if (!(_player instanceof ServerPlayer)) {
|
|
- continue;
|
|
- }
|
|
- ServerPlayer player = (ServerPlayer)_player;
|
|
-
|
|
- if (axisalignedbb.contains(player.getX(), player.getY(), player.getZ()) && condition.test(source, player)) {
|
|
- ret.add(player);
|
|
- }
|
|
- }
|
|
+ // Folia - region threading
|
|
|
|
- return ret;
|
|
- }
|
|
+ // Folia - region threading
|
|
// Paper end - optimise get nearest players for entity AI
|
|
|
|
public final io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader playerChunkLoader = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader(this);
|
|
@@ -563,6 +538,59 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
});
|
|
}
|
|
|
|
+ // Folia start - regionised ticking
|
|
+ public final io.papermc.paper.threadedregions.TickRegions tickRegions = new io.papermc.paper.threadedregions.TickRegions();
|
|
+ public final io.papermc.paper.threadedregions.ThreadedRegionizer<io.papermc.paper.threadedregions.TickRegions.TickRegionData, io.papermc.paper.threadedregions.TickRegions.TickRegionSectionData> regioniser;
|
|
+ {
|
|
+ this.regioniser = new io.papermc.paper.threadedregions.ThreadedRegionizer<>(
|
|
+ 3*9,
|
|
+ (2.0 / 3.0),
|
|
+ 1,
|
|
+ 1,
|
|
+ io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(),
|
|
+ this,
|
|
+ this.tickRegions
|
|
+ );
|
|
+ }
|
|
+ public final io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData taskQueueRegionData = new io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData(this);
|
|
+ public static final int WORLD_INIT_NOT_CHECKED = 0;
|
|
+ public static final int WORLD_INIT_CHECKING = 1;
|
|
+ public static final int WORLD_INIT_CHECKED = 2;
|
|
+ public final java.util.concurrent.atomic.AtomicInteger checkInitialised = new java.util.concurrent.atomic.AtomicInteger(WORLD_INIT_NOT_CHECKED);
|
|
+ public ChunkPos randomSpawnSelection;
|
|
+
|
|
+ public static final record PendingTeleport(Entity.EntityTreeNode rootVehicle, Vec3 to) {}
|
|
+ private final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<PendingTeleport> pendingTeleports = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>();
|
|
+
|
|
+ public void pushPendingTeleport(final PendingTeleport teleport) {
|
|
+ synchronized (this.pendingTeleports) {
|
|
+ this.pendingTeleports.add(teleport);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public boolean removePendingTeleport(final PendingTeleport teleport) {
|
|
+ synchronized (this.pendingTeleports) {
|
|
+ return this.pendingTeleports.remove(teleport);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public List<PendingTeleport> removeAllRegionTeleports() {
|
|
+ final List<PendingTeleport> ret = new ArrayList<>();
|
|
+
|
|
+ synchronized (this.pendingTeleports) {
|
|
+ for (final Iterator<PendingTeleport> iterator = this.pendingTeleports.iterator(); iterator.hasNext();) {
|
|
+ final PendingTeleport pendingTeleport = iterator.next();
|
|
+ if (io.papermc.paper.util.TickThread.isTickThreadFor(this, pendingTeleport.to())) {
|
|
+ ret.add(pendingTeleport);
|
|
+ iterator.remove();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ // Folia end - regionised ticking
|
|
+
|
|
// Add env and gen to constructor, IWorldDataServer -> WorldDataServer
|
|
public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
|
|
// IRegistryCustom.Dimension iregistrycustom_dimension = minecraftserver.registryAccess(); // CraftBukkit - decompile error
|
|
@@ -574,13 +602,13 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
this.convertable = convertable_conversionsession;
|
|
this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelDirectory.path().toFile());
|
|
// CraftBukkit end
|
|
- this.players = Lists.newArrayList();
|
|
- this.entityTickList = new EntityTickList();
|
|
- this.blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier());
|
|
- this.fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier());
|
|
+ this.players = new java.util.concurrent.CopyOnWriteArrayList<>(); // Folia - region threading
|
|
+ //this.entityTickList = new EntityTickList(); // Folia - region threading
|
|
+ //this.blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); // Folia - moved to RegioniedWorldData
|
|
+ //this.fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); // Folia - moved to RegioniedWorldData
|
|
this.navigatingMobs = new ObjectOpenHashSet();
|
|
- this.blockEvents = new ObjectLinkedOpenHashSet();
|
|
- this.blockEventsToReschedule = new ArrayList(64);
|
|
+ //this.blockEvents = new ObjectLinkedOpenHashSet(); // Folia - moved to RegioniedWorldData
|
|
+ //this.blockEventsToReschedule = new ArrayList(64); // Folia - moved to RegioniedWorldData
|
|
this.dragonParts = new Int2ObjectOpenHashMap();
|
|
this.tickTime = flag1;
|
|
this.server = minecraftserver;
|
|
@@ -619,7 +647,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
});
|
|
this.chunkSource.getGeneratorState().ensureStructuresGenerated();
|
|
this.portalForcer = new PortalForcer(this);
|
|
- this.updateSkyBrightness();
|
|
+ //this.updateSkyBrightness(); // Folia - region threading - delay until first tick
|
|
this.prepareWeather();
|
|
this.getWorldBorder().setAbsoluteMaxSize(minecraftserver.getAbsoluteMaxWorldSize());
|
|
this.raids = (Raids) this.getDataStorage().computeIfAbsent((nbttagcompound) -> {
|
|
@@ -647,7 +675,14 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
|
|
this.chunkTaskScheduler = new io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler(this, io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.workerThreads); // Paper - rewrite chunk system
|
|
this.entityLookup = new io.papermc.paper.chunk.system.entity.EntityLookup(this, new EntityCallbacks()); // Paper - rewrite chunk system
|
|
+ this.updateTickData(); // Folia - region threading - make sure it is initialised before ticked
|
|
+ }
|
|
+
|
|
+ // Folia start - region threading
|
|
+ public void updateTickData() {
|
|
+ this.tickData = new io.papermc.paper.threadedregions.RegionizedServer.WorldLevelData(this, this.serverLevelData.getGameTime(), this.serverLevelData.getDayTime());
|
|
}
|
|
+ // Folia end - region threading
|
|
|
|
public void setWeatherParameters(int clearDuration, int rainDuration, boolean raining, boolean thundering) {
|
|
this.serverLevelData.setClearWeatherTime(clearDuration);
|
|
@@ -666,55 +701,31 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
return this.structureManager;
|
|
}
|
|
|
|
- public void tick(BooleanSupplier shouldKeepTicking) {
|
|
- // Paper start - optimise checkDespawn
|
|
- this.playersAffectingSpawning.clear();
|
|
- for (ServerPlayer player : this.players) {
|
|
- if (net.minecraft.world.entity.EntitySelector.PLAYER_AFFECTS_SPAWNING.test(player)) {
|
|
- this.playersAffectingSpawning.add(player);
|
|
- }
|
|
- }
|
|
- // Paper end - optimise checkDespawn
|
|
+ public void tick(BooleanSupplier shouldKeepTicking, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - regionised ticking
|
|
+ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking
|
|
+ // Folia - region threading
|
|
ProfilerFiller gameprofilerfiller = this.getProfiler();
|
|
|
|
- this.handlingTick = true;
|
|
+ regionizedWorldData.setHandlingTick(true); // Folia - regionised ticking
|
|
gameprofilerfiller.push("world border");
|
|
- this.getWorldBorder().tick();
|
|
+ if (region == null) this.getWorldBorder().tick(); // Folia - regionised ticking - moved into global tick
|
|
gameprofilerfiller.popPush("weather");
|
|
- this.advanceWeatherCycle();
|
|
- int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE);
|
|
+ if (region == null) this.advanceWeatherCycle();
|
|
+ //int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); // Folia - region threading - move intotickSleep
|
|
long j;
|
|
|
|
- if (this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) {
|
|
- // CraftBukkit start
|
|
- j = this.levelData.getDayTime() + 24000L;
|
|
- TimeSkipEvent event = new TimeSkipEvent(this.getWorld(), TimeSkipEvent.SkipReason.NIGHT_SKIP, (j - j % 24000L) - this.getDayTime());
|
|
- if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
|
|
- getCraftServer().getPluginManager().callEvent(event);
|
|
- if (!event.isCancelled()) {
|
|
- this.setDayTime(this.getDayTime() + event.getSkipAmount());
|
|
- }
|
|
- }
|
|
-
|
|
- if (!event.isCancelled()) {
|
|
- this.wakeUpAllPlayers();
|
|
- }
|
|
- // CraftBukkit end
|
|
- if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) {
|
|
- this.resetWeatherCycle();
|
|
- }
|
|
- }
|
|
+ if (region == null) this.tickSleep(); // Folia - region threading
|
|
|
|
- this.updateSkyBrightness();
|
|
+ if (region == null) this.updateSkyBrightness(); // Folia - region threading
|
|
this.tickTime();
|
|
gameprofilerfiller.popPush("tickPending");
|
|
timings.scheduledBlocks.startTiming(); // Paper
|
|
if (!this.isDebug()) {
|
|
- j = this.getGameTime();
|
|
+ j = regionizedWorldData.getRedstoneGameTime(); // Folia - region threading
|
|
gameprofilerfiller.push("blockTicks");
|
|
- this.blockTicks.tick(j, 65536, this::tickBlock);
|
|
+ regionizedWorldData.getBlockLevelTicks().tick(j, 65536, this::tickBlock); // Folia - region ticking
|
|
gameprofilerfiller.popPush("fluidTicks");
|
|
- this.fluidTicks.tick(j, 65536, this::tickFluid);
|
|
+ regionizedWorldData.getFluidLevelTicks().tick(j, 65536, this::tickFluid); // Folia - region ticking
|
|
gameprofilerfiller.pop();
|
|
}
|
|
timings.scheduledBlocks.stopTiming(); // Paper
|
|
@@ -731,7 +742,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
timings.doSounds.startTiming(); // Spigot
|
|
this.runBlockEvents();
|
|
timings.doSounds.stopTiming(); // Spigot
|
|
- this.handlingTick = false;
|
|
+ regionizedWorldData.setHandlingTick(false); // Folia - regionised ticking
|
|
gameprofilerfiller.pop();
|
|
boolean flag = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players
|
|
|
|
@@ -743,20 +754,30 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
gameprofilerfiller.push("entities");
|
|
timings.tickEntities.startTiming(); // Spigot
|
|
if (this.dragonFight != null) {
|
|
+ if (io.papermc.paper.util.TickThread.isTickThreadFor(this, 0, 0)) { // Folia - region threading
|
|
gameprofilerfiller.push("dragonFight");
|
|
this.dragonFight.tick();
|
|
gameprofilerfiller.pop();
|
|
+ } else { // Folia start - region threading
|
|
+ // try to load dragon fight
|
|
+ ChunkPos fightCenter = new ChunkPos(0, 0);
|
|
+ this.chunkSource.addTicketAtLevel(
|
|
+ TicketType.UNKNOWN, fightCenter, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL,
|
|
+ fightCenter
|
|
+ );
|
|
+ } // Folia end - region threading
|
|
}
|
|
|
|
org.spigotmc.ActivationRange.activateEntities(this); // Spigot
|
|
timings.entityTick.startTiming(); // Spigot
|
|
- this.entityTickList.forEach((entity) -> {
|
|
+ regionizedWorldData.forEachTickingEntity((entity) -> { // Folia - regionised ticking
|
|
if (!entity.isRemoved()) {
|
|
if (false && this.shouldDiscardEntity(entity)) { // CraftBukkit - We prevent spawning in general, so this butchering is not needed
|
|
entity.discard();
|
|
} else {
|
|
gameprofilerfiller.push("checkDespawn");
|
|
entity.checkDespawn();
|
|
+ if (entity.isRemoved()) return; // Folia - region threading - if we despawned, DON'T TICK IT!
|
|
gameprofilerfiller.pop();
|
|
if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - now always true if in the ticking list
|
|
Entity entity1 = entity.getVehicle();
|
|
@@ -787,6 +808,31 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
gameprofilerfiller.pop();
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ public void tickSleep() {
|
|
+ int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); long j; // Folia moved from tick loop
|
|
+ if (this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { // Folia - region threading - moved to global tick
|
|
+ // CraftBukkit start
|
|
+ j = this.levelData.getDayTime() + 24000L;
|
|
+ TimeSkipEvent event = new TimeSkipEvent(this.getWorld(), TimeSkipEvent.SkipReason.NIGHT_SKIP, (j - j % 24000L) - this.getDayTime());
|
|
+ if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
|
|
+ getCraftServer().getPluginManager().callEvent(event);
|
|
+ if (!event.isCancelled()) {
|
|
+ this.setDayTime(this.getDayTime() + event.getSkipAmount());
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (!event.isCancelled()) {
|
|
+ this.wakeUpAllPlayers();
|
|
+ }
|
|
+ // CraftBukkit end
|
|
+ if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) {
|
|
+ this.resetWeatherCycle();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Override
|
|
public boolean shouldTickBlocksAt(long chunkPos) {
|
|
// Paper start - replace player chunk loader system
|
|
@@ -797,11 +843,12 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
|
|
protected void tickTime() {
|
|
if (this.tickTime) {
|
|
- long i = this.levelData.getGameTime() + 1L;
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - region threading
|
|
+ long i = regionizedWorldData.getRedstoneGameTime() + 1L; // Folia - region threading
|
|
|
|
- this.serverLevelData.setGameTime(i);
|
|
- this.serverLevelData.getScheduledEvents().tick(this.server, i);
|
|
- if (this.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
|
|
+ regionizedWorldData.setRedstoneGameTime(i); // Folia - region threading
|
|
+ if (false) this.serverLevelData.getScheduledEvents().tick(this.server, i); // Folia - region threading - TODO any way to bring this in?
|
|
+ if (false && this.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { // Folia - region threading
|
|
this.setDayTime(this.levelData.getDayTime() + 1L);
|
|
}
|
|
|
|
@@ -830,15 +877,23 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
private void wakeUpAllPlayers() {
|
|
this.sleepStatus.removeAllSleepers();
|
|
(this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList())).forEach((entityplayer) -> { // CraftBukkit - decompile error
|
|
- entityplayer.stopSleepInBed(false, false);
|
|
+ // Folia start - region threading
|
|
+ entityplayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> {
|
|
+ if (player.level != ServerLevel.this || !player.isSleeping()) {
|
|
+ return;
|
|
+ }
|
|
+ player.stopSleepInBed(false, false);
|
|
+ }, null, 1L);
|
|
+ // Folia end - region threading
|
|
});
|
|
}
|
|
// Paper start - optimise random block ticking
|
|
- private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos();
|
|
- private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong());
|
|
+ private final ThreadLocal<BlockPos.MutableBlockPos> chunkTickMutablePosition = ThreadLocal.withInitial(() -> new BlockPos.MutableBlockPos()); // Folia - region threading
|
|
+ private final ThreadLocal<io.papermc.paper.util.math.ThreadUnsafeRandom> randomTickRandom = ThreadLocal.withInitial(() -> new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong())); // Folia - region threading
|
|
// Paper end
|
|
|
|
public void tickChunk(LevelChunk chunk, int randomTickSpeed) {
|
|
+ io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = this.randomTickRandom.get(); // Folia - region threading
|
|
ChunkPos chunkcoordintpair = chunk.getPos();
|
|
boolean flag = this.isRaining();
|
|
int j = chunkcoordintpair.getMinBlockX();
|
|
@@ -846,7 +901,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
ProfilerFiller gameprofilerfiller = this.getProfiler();
|
|
|
|
gameprofilerfiller.push("thunder");
|
|
- final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change
|
|
+ final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition.get(); // Paper - use mutable to reduce allocation rate, final to force compile fail on change // Folia - region threading
|
|
|
|
if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - disable thunder
|
|
blockposition.set(this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15))); // Paper
|
|
@@ -940,7 +995,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
int yPos = (sectionIndex + minSection) << 4;
|
|
for (int a = 0; a < randomTickSpeed; ++a) {
|
|
int tickingBlocks = section.tickingList.size();
|
|
- int index = this.randomTickRandom.nextInt(16 * 16 * 16);
|
|
+ int index = randomTickRandom.nextInt(16 * 16 * 16); // Folia - region threading
|
|
if (index >= tickingBlocks) {
|
|
continue;
|
|
}
|
|
@@ -954,7 +1009,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
BlockPos blockposition2 = blockposition.set(j + randomX, randomY, k + randomZ);
|
|
BlockState iblockdata = com.destroystokyo.paper.util.maplist.IBlockDataList.getBlockDataFromRaw(raw);
|
|
|
|
- iblockdata.randomTick(this, blockposition2, this.randomTickRandom);
|
|
+ iblockdata.randomTick(this, blockposition2, randomTickRandom); // Folia - region threading
|
|
// We drop the fluid tick since LAVA is ALREADY TICKED by the above method (See LiquidBlock).
|
|
// TODO CHECK ON UPDATE (ping the Canadian)
|
|
}
|
|
@@ -1008,7 +1063,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
}
|
|
|
|
public boolean isHandlingTick() {
|
|
- return this.handlingTick;
|
|
+ return this.getCurrentWorldData().isHandlingTick(); // Folia - regionised ticking
|
|
}
|
|
|
|
public boolean canSleepThroughNights() {
|
|
@@ -1040,6 +1095,14 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
}
|
|
|
|
public void updateSleepingPlayerList() {
|
|
+ // Folia start - region threading
|
|
+ if (!io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
|
|
+ ServerLevel.this.updateSleepingPlayerList();
|
|
+ });
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
if (!this.players.isEmpty() && this.sleepStatus.update(this.players)) {
|
|
this.announceSleepStatus();
|
|
}
|
|
@@ -1051,7 +1114,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
return this.server.getScoreboard();
|
|
}
|
|
|
|
- private void advanceWeatherCycle() {
|
|
+ public void advanceWeatherCycle() { // Folia - region threading - public
|
|
boolean flag = this.isRaining();
|
|
|
|
if (this.dimensionType().hasSkyLight()) {
|
|
@@ -1137,23 +1200,24 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
this.server.getPlayerList().broadcastAll(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.THUNDER_LEVEL_CHANGE, this.thunderLevel));
|
|
}
|
|
// */
|
|
- for (int idx = 0; idx < this.players.size(); ++idx) {
|
|
- if (((ServerPlayer) this.players.get(idx)).level == this) {
|
|
- ((ServerPlayer) this.players.get(idx)).tickWeather();
|
|
+ ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Folia - region threading
|
|
+ for (ServerPlayer player : players) { // Folia - region threading
|
|
+ if (player.level == this) { // Folia - region threading
|
|
+ player.tickWeather(); // Folia - region threading
|
|
}
|
|
}
|
|
|
|
if (flag != this.isRaining()) {
|
|
// Only send weather packets to those affected
|
|
- for (int idx = 0; idx < this.players.size(); ++idx) {
|
|
- if (((ServerPlayer) this.players.get(idx)).level == this) {
|
|
- ((ServerPlayer) this.players.get(idx)).setPlayerWeather((!flag ? WeatherType.DOWNFALL : WeatherType.CLEAR), false);
|
|
+ for (ServerPlayer player : players) { // Folia - region threading
|
|
+ if (player.level == this) { // Folia - region threading
|
|
+ player.setPlayerWeather((!flag ? WeatherType.DOWNFALL : WeatherType.CLEAR), false); // Folia - region threading
|
|
}
|
|
}
|
|
}
|
|
- for (int idx = 0; idx < this.players.size(); ++idx) {
|
|
- if (((ServerPlayer) this.players.get(idx)).level == this) {
|
|
- ((ServerPlayer) this.players.get(idx)).updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel);
|
|
+ for (ServerPlayer player : players) { // Folia - region threading
|
|
+ if (player.level == this) { // Folia - region threading
|
|
+ player.updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel); // Folia - region threading
|
|
}
|
|
}
|
|
// CraftBukkit end
|
|
@@ -1217,7 +1281,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
|
|
public void tickNonPassenger(Entity entity) {
|
|
// Paper start - log detailed entity tick information
|
|
- io.papermc.paper.util.TickThread.ensureTickThread("Cannot tick an entity off-main");
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(entity, "Cannot tick an entity off-main"); // Folia - region threading
|
|
try {
|
|
if (currentlyTickingEntity.get() == null) {
|
|
currentlyTickingEntity.lazySet(entity);
|
|
@@ -1250,7 +1314,16 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
if (isActive) { // Paper - EAR 2
|
|
TimingHistory.activatedEntityTicks++;
|
|
entity.tick();
|
|
- entity.postTick(); // CraftBukkit
|
|
+ // Folia start - region threading
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(entity)) {
|
|
+ // removed from region while ticking
|
|
+ return;
|
|
+ }
|
|
+ if (entity.doPortalLogic()) {
|
|
+ // portalled
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
} else { entity.inactiveTick(); } // Paper - EAR 2
|
|
this.getProfiler().pop();
|
|
} finally { timer.stopTiming(); } // Paper - timings
|
|
@@ -1273,7 +1346,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
|
|
private void tickPassenger(Entity vehicle, Entity passenger) {
|
|
if (!passenger.isRemoved() && passenger.getVehicle() == vehicle) {
|
|
- if (passenger instanceof Player || this.entityTickList.contains(passenger)) {
|
|
+ if (passenger instanceof Player || this.getCurrentWorldData().hasEntityTickingEntity(passenger)) { // Folia - region threading
|
|
// Paper - EAR 2
|
|
final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(passenger);
|
|
co.aikar.timings.Timing timer = isActive ? passenger.getType().passengerTickTimer.startTiming() : passenger.getType().passengerInactiveTickTimer.startTiming(); // Paper
|
|
@@ -1290,7 +1363,16 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
// Paper start - EAR 2
|
|
if (isActive) {
|
|
passenger.rideTick();
|
|
- passenger.postTick(); // CraftBukkit
|
|
+ // Folia start - region threading
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(passenger)) {
|
|
+ // removed from region while ticking
|
|
+ return;
|
|
+ }
|
|
+ if (passenger.doPortalLogic()) {
|
|
+ // portalled
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
} else {
|
|
passenger.setDeltaMovement(Vec3.ZERO);
|
|
passenger.inactiveTick();
|
|
@@ -1378,7 +1460,15 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
// Paper - rewrite chunk system - entity saving moved into ChunkHolder
|
|
|
|
} else if (close) { chunkproviderserver.close(false); } // Paper - rewrite chunk system
|
|
+ // Folia - move into saveLevelData()
|
|
+ }
|
|
|
|
+ public void saveLevelData() { // Folia - region threading
|
|
+ if (this.dragonFight != null) {
|
|
+ this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit
|
|
+ }
|
|
+ // Folia start - region threading
|
|
+ // moved from save
|
|
// CraftBukkit start - moved from MinecraftServer.saveChunks
|
|
ServerLevel worldserver1 = this;
|
|
|
|
@@ -1386,12 +1476,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save());
|
|
this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData());
|
|
// CraftBukkit end
|
|
- }
|
|
-
|
|
- private void saveLevelData() {
|
|
- if (this.dragonFight != null) {
|
|
- this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit
|
|
- }
|
|
+ // Folia end - region threading
|
|
|
|
this.getChunkSource().getDataStorage().save();
|
|
}
|
|
@@ -1446,6 +1531,19 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
return list;
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ @Nullable
|
|
+ public ServerPlayer getRandomLocalPlayer() {
|
|
+ List<ServerPlayer> 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<ServerPlayer> list = this.getPlayers(LivingEntity::isAlive);
|
|
@@ -1547,8 +1645,8 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
} else {
|
|
if (entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity && itemEntity.getItem().isEmpty()) return false; // Paper - Prevent empty items from being added
|
|
// Paper start - capture all item additions to the world
|
|
- if (captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) {
|
|
- captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity);
|
|
+ if (this.getCurrentWorldData().captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) { // Folia - region threading
|
|
+ this.getCurrentWorldData().captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity); // Folia - region threading
|
|
return true;
|
|
}
|
|
// Paper end
|
|
@@ -1687,7 +1785,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
|
|
@Override
|
|
public void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags) {
|
|
- if (this.isUpdatingNavigations) {
|
|
+ if (false && this.isUpdatingNavigations) { // Folia - region threading
|
|
String s = "recursive call to sendBlockUpdated";
|
|
|
|
Util.logAndPauseIfInIde("recursive call to sendBlockUpdated", new IllegalStateException("recursive call to sendBlockUpdated"));
|
|
@@ -1700,7 +1798,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
|
|
if (Shapes.joinIsNotEmpty(voxelshape, voxelshape1, BooleanOp.NOT_SAME)) {
|
|
List<PathNavigation> list = new ObjectArrayList();
|
|
- Iterator iterator = this.navigatingMobs.iterator();
|
|
+ Iterator iterator = this.getCurrentWorldData().getNavigatingMobs(); // Folia - region threading
|
|
|
|
while (iterator.hasNext()) {
|
|
// CraftBukkit start - fix SPIGOT-6362
|
|
@@ -1723,7 +1821,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
}
|
|
|
|
try {
|
|
- this.isUpdatingNavigations = true;
|
|
+ //this.isUpdatingNavigations = true; // Folia - region threading
|
|
iterator = list.iterator();
|
|
|
|
while (iterator.hasNext()) {
|
|
@@ -1732,7 +1830,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
navigationabstract1.recomputePath();
|
|
}
|
|
} finally {
|
|
- this.isUpdatingNavigations = false;
|
|
+ //this.isUpdatingNavigations = false; // Folia - region threading
|
|
}
|
|
|
|
}
|
|
@@ -1741,23 +1839,23 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
|
|
@Override
|
|
public void updateNeighborsAt(BlockPos pos, Block sourceBlock) {
|
|
- if (captureBlockStates) { return; } // Paper - Cancel all physics during placement
|
|
- this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, (Direction) null);
|
|
+ if (this.getCurrentWorldData().captureBlockStates) { return; } // Paper - Cancel all physics during placement // Folia - region threading
|
|
+ this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, (Direction) null); // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public void updateNeighborsAtExceptFromFacing(BlockPos pos, Block sourceBlock, Direction direction) {
|
|
- this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, direction);
|
|
+ this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, direction); // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public void neighborChanged(BlockPos pos, Block sourceBlock, BlockPos sourcePos) {
|
|
- this.neighborUpdater.neighborChanged(pos, sourceBlock, sourcePos);
|
|
+ this.getCurrentWorldData().neighborUpdater.neighborChanged(pos, sourceBlock, sourcePos); // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public void neighborChanged(BlockState state, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) {
|
|
- this.neighborUpdater.neighborChanged(state, pos, sourceBlock, sourcePos, notify);
|
|
+ this.getCurrentWorldData().neighborUpdater.neighborChanged(state, pos, sourceBlock, sourcePos, notify); // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
@@ -1788,7 +1886,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
explosion.clearToBlow();
|
|
}
|
|
|
|
- Iterator iterator = this.players.iterator();
|
|
+ Iterator iterator = this.getLocalPlayers().iterator(); // Folia - region thraeding
|
|
|
|
while (iterator.hasNext()) {
|
|
ServerPlayer entityplayer = (ServerPlayer) iterator.next();
|
|
@@ -1803,25 +1901,28 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
|
|
@Override
|
|
public void blockEvent(BlockPos pos, Block block, int type, int data) {
|
|
- this.blockEvents.add(new BlockEventData(pos, block, type, data));
|
|
+ this.getCurrentWorldData().pushBlockEvent(new BlockEventData(pos, block, type, data)); // Folia - regionised ticking
|
|
}
|
|
|
|
private void runBlockEvents() {
|
|
- this.blockEventsToReschedule.clear();
|
|
+ List<BlockEventData> blockEventsToReschedule = new ArrayList<>(64); // Folia - regionised ticking
|
|
|
|
- while (!this.blockEvents.isEmpty()) {
|
|
- BlockEventData blockactiondata = (BlockEventData) this.blockEvents.removeFirst();
|
|
+ // Folia start - regionised ticking
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldRegionData = this.getCurrentWorldData();
|
|
+ BlockEventData blockactiondata;
|
|
+ while ((blockactiondata = worldRegionData.removeFirstBlockEvent()) != null) {
|
|
+ // Folia end - regionised ticking
|
|
|
|
if (this.shouldTickBlocksAt(blockactiondata.pos())) {
|
|
if (this.doBlockEvent(blockactiondata)) {
|
|
this.server.getPlayerList().broadcast((Player) null, (double) blockactiondata.pos().getX(), (double) blockactiondata.pos().getY(), (double) blockactiondata.pos().getZ(), 64.0D, this.dimension(), new ClientboundBlockEventPacket(blockactiondata.pos(), blockactiondata.block(), blockactiondata.paramA(), blockactiondata.paramB()));
|
|
}
|
|
} else {
|
|
- this.blockEventsToReschedule.add(blockactiondata);
|
|
+ blockEventsToReschedule.add(blockactiondata); // Folia - regionised ticking
|
|
}
|
|
}
|
|
|
|
- this.blockEvents.addAll(this.blockEventsToReschedule);
|
|
+ worldRegionData.pushBlockEvents(blockEventsToReschedule); // Folia - regionised ticking
|
|
}
|
|
|
|
private boolean doBlockEvent(BlockEventData event) {
|
|
@@ -1832,12 +1933,12 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
|
|
@Override
|
|
public LevelTicks<Block> getBlockTicks() {
|
|
- return this.blockTicks;
|
|
+ return this.getCurrentWorldData().getBlockLevelTicks(); // Folia - region ticking
|
|
}
|
|
|
|
@Override
|
|
public LevelTicks<Fluid> getFluidTicks() {
|
|
- return this.fluidTicks;
|
|
+ return this.getCurrentWorldData().getFluidLevelTicks(); // Folia - region ticking
|
|
}
|
|
|
|
@Nonnull
|
|
@@ -1861,7 +1962,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
|
|
public <T extends ParticleOptions> 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 <T extends ParticleOptions> int sendParticles(List<ServerPlayer> receivers, ServerPlayer sender, T t0, double d0, double d1, double d2, int i, double d3, double d4, double d5, double d6, boolean force) {
|
|
// Paper end
|
|
@@ -1914,7 +2015,14 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
public Entity getEntityOrPart(int id) {
|
|
Entity entity = (Entity) this.getEntities().get(id);
|
|
|
|
- return entity != null ? entity : (Entity) this.dragonParts.get(id);
|
|
+ // Folia start - region threading
|
|
+ if (entity != null) {
|
|
+ return entity;
|
|
+ }
|
|
+ synchronized (this.dragonParts) {
|
|
+ return this.dragonParts.get(id);
|
|
+ }
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
@Nullable
|
|
@@ -1922,6 +2030,61 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
return (Entity) this.getEntities().get(uuid);
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ private final java.util.concurrent.atomic.AtomicLong nonFullSyncLoadIdGenerator = new java.util.concurrent.atomic.AtomicLong();
|
|
+
|
|
+ private ChunkAccess getIfAboveStatus(int chunkX, int chunkZ, net.minecraft.world.level.chunk.ChunkStatus status) {
|
|
+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder loaded =
|
|
+ this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkX, chunkZ);
|
|
+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder.ChunkCompletion loadedCompletion;
|
|
+ if (loaded != null && (loadedCompletion = loaded.getLastChunkCompletion()) != null && loadedCompletion.genStatus().isOrAfter(status)) {
|
|
+ return loadedCompletion.chunk();
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ChunkAccess syncLoadNonFull(int chunkX, int chunkZ, net.minecraft.world.level.chunk.ChunkStatus status) {
|
|
+ if (status == null || status.isOrAfter(net.minecraft.world.level.chunk.ChunkStatus.FULL)) {
|
|
+ throw new IllegalArgumentException("Status: " + status.getName());
|
|
+ }
|
|
+ ChunkAccess loaded = this.getIfAboveStatus(chunkX, chunkZ, status);
|
|
+ if (loaded != null) {
|
|
+ return loaded;
|
|
+ }
|
|
+
|
|
+ Long ticketId = Long.valueOf(this.nonFullSyncLoadIdGenerator.getAndIncrement());
|
|
+ int ticketLevel = 33 + net.minecraft.world.level.chunk.ChunkStatus.getDistance(status);
|
|
+ this.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel(
|
|
+ TicketType.NON_FULL_SYNC_LOAD, chunkX, chunkZ, ticketLevel, ticketId
|
|
+ );
|
|
+ this.chunkTaskScheduler.chunkHolderManager.processTicketUpdates();
|
|
+
|
|
+ this.chunkTaskScheduler.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING);
|
|
+
|
|
+ // we could do a simple spinwait here, since we do not need to process tasks while performing this load
|
|
+ // but we process tasks only because it's a better use of the time spent
|
|
+ this.chunkSource.mainThreadProcessor.managedBlock(() -> {
|
|
+ return ServerLevel.this.getIfAboveStatus(chunkX, chunkZ, status) != null;
|
|
+ });
|
|
+
|
|
+ loaded = ServerLevel.this.getIfAboveStatus(chunkX, chunkZ, status);
|
|
+ if (loaded == null) {
|
|
+ throw new IllegalStateException("Expected chunk to be loaded for status " + status);
|
|
+ }
|
|
+
|
|
+ // let the next process ticket updates call pick this up
|
|
+ this.chunkTaskScheduler.chunkHolderManager.pushDelayedTicketUpdate(
|
|
+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.TicketOperation.removeOp(
|
|
+ chunkX, chunkZ, TicketType.NON_FULL_SYNC_LOAD, ticketLevel, ticketId
|
|
+ )
|
|
+ );
|
|
+
|
|
+ return loaded;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Nullable
|
|
public BlockPos findNearestMapStructure(TagKey<Structure> structureTag, BlockPos pos, int radius, boolean skipReferencedStructures) {
|
|
if (!this.serverLevelData.worldGenOptions().generateStructures()) { // CraftBukkit
|
|
@@ -2082,7 +2245,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 +2273,18 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
BlockPos blockposition1 = pos.immutable();
|
|
|
|
optional.ifPresent((holder) -> {
|
|
- this.getServer().execute(() -> {
|
|
+ Runnable run = () -> { // Folia - region threading
|
|
this.getPoiManager().remove(blockposition1);
|
|
DebugPackets.sendPoiRemovedPacket(this, blockposition1);
|
|
- });
|
|
+ }; // Folia - region threading
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(
|
|
+ this, blockposition1.getX() >> 4, blockposition1.getZ() >> 4, run
|
|
+ );
|
|
+ // Folia end - region threading
|
|
});
|
|
optional1.ifPresent((holder) -> {
|
|
- this.getServer().execute(() -> {
|
|
+ Runnable run = () -> { // Folia - region threading
|
|
// Paper start
|
|
if (optional.isEmpty() && this.getPoiManager().exists(blockposition1, poiType -> true)) {
|
|
this.getPoiManager().remove(blockposition1);
|
|
@@ -2124,7 +2292,12 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
// Paper end
|
|
this.getPoiManager().add(blockposition1, holder);
|
|
DebugPackets.sendPoiAddedPacket(this, blockposition1);
|
|
- });
|
|
+ }; // Folia - region threading
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(
|
|
+ this, blockposition1.getX() >> 4, blockposition1.getZ() >> 4, run
|
|
+ );
|
|
+ // Folia end - region threading
|
|
});
|
|
}
|
|
}
|
|
@@ -2171,7 +2344,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 +2358,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 +2504,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 +2517,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 +2526,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 +2569,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 <T> String getTypeCount(Iterable<T> items, Function<T, String> classifier) {
|
|
@@ -2431,6 +2602,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 +2620,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<org.bukkit.block.BlockState>) (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 +2641,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 +2670,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 +2718,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 +2755,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 +2766,9 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
for (int j = 0; j < i; ++j) {
|
|
EnderDragonPart entitycomplexpart = aentitycomplexpart[j];
|
|
|
|
+ synchronized (ServerLevel.this.dragonParts) { // Folia - region threading
|
|
ServerLevel.this.dragonParts.put(entitycomplexpart.getId(), entitycomplexpart);
|
|
+ } // Folia - region threading
|
|
}
|
|
}
|
|
|
|
@@ -2618,11 +2794,18 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
{
|
|
com.google.common.collect.Streams.stream( ServerLevel.this.getServer().getAllLevels() ).map( ServerLevel::getDataStorage ).forEach( (worldData) ->
|
|
{
|
|
- for (Object o : worldData.cache.values() )
|
|
+ // Folia start - make map data thread-safe
|
|
+ List<Object> worldDataCache;
|
|
+ synchronized (worldData.cache) {
|
|
+ worldDataCache = new java.util.ArrayList<>(worldData.cache.values());
|
|
+ }
|
|
+ for (Object o : worldDataCache )
|
|
+ // Folia end - make map data thread-safe
|
|
{
|
|
if ( o instanceof MapItemSavedData )
|
|
{
|
|
MapItemSavedData map = (MapItemSavedData) o;
|
|
+ synchronized (map) { // Folia - make map data thread-safe
|
|
map.carriedByPlayers.remove( (Player) entity );
|
|
for ( Iterator<MapItemSavedData.HoldingPlayer> iter = (Iterator<MapItemSavedData.HoldingPlayer>) map.carriedBy.iterator(); iter.hasNext(); )
|
|
{
|
|
@@ -2632,6 +2815,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
iter.remove();
|
|
}
|
|
}
|
|
+ } // Folia - make map data thread-safe
|
|
}
|
|
}
|
|
} );
|
|
@@ -2666,7 +2850,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 +2861,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 a3cef477646abf6172d4e50a28ed30f04c30d667..7c04fc2b9ba50d5a7e35a04a17e28db472fff697 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
|
|
@@ -187,7 +187,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;
|
|
@@ -248,11 +248,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<ServerPlayer> cachedSingleMobDistanceMap;
|
|
- // Paper end
|
|
+ // Folia - region threading - revert per player mob caps
|
|
|
|
// CraftBukkit start
|
|
public String displayName;
|
|
@@ -414,7 +410,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.
|
|
@@ -456,11 +452,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<Location> 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) {
|
|
@@ -474,33 +470,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<java.util.UUID> 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;
|
|
}
|
|
|
|
@@ -1159,6 +1198,343 @@ public class ServerPlayer extends Player {
|
|
}
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ /**
|
|
+ * Teleport flag indicating that the player is to be respawned, expected to only be used
|
|
+ * internally for {@link #respawn(java.util.function.Consumer)}.
|
|
+ */
|
|
+ public static final long TELEPORT_FLAGS_PLAYER_RESPAWN = Long.MIN_VALUE >>> 0;
|
|
+ /**
|
|
+ * Teleport flag indicating the player should be placed at the highest y-value that
|
|
+ * provides no collisions for the player's bounding box. Note that this setting
|
|
+ * does not imply {@link Entity#TELEPORT_FLAG_LOAD_CHUNK}, so it may
|
|
+ * sync load chunks unless the load chunk flag is provided.
|
|
+ */
|
|
+ public static final long TELEPORT_FLAGS_AVOID_SUFFOCATION = Long.MIN_VALUE >>> 1;
|
|
+
|
|
+ private void avoidSuffocation() {
|
|
+ while (!this.getLevel().noCollision(this, this.getBoundingBox(), true) && this.getY() < (double)this.getLevel().getMaxBuildHeight()) { // Folia - make sure this loads chunks, we default to NOT loading now
|
|
+ this.setPos(this.getX(), this.getY() + 1.0D, this.getZ());
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void exitEndCredits() {
|
|
+ if (!this.wonGame) {
|
|
+ // not in the end credits anymore
|
|
+ return;
|
|
+ }
|
|
+ this.wonGame = false;
|
|
+
|
|
+ this.respawn((player) -> {
|
|
+ CriteriaTriggers.CHANGED_DIMENSION.trigger(player, Level.END, Level.OVERWORLD);
|
|
+ }, true);
|
|
+ }
|
|
+
|
|
+ public void respawn(java.util.function.Consumer<ServerPlayer> respawnComplete) {
|
|
+ this.respawn(respawnComplete, false);
|
|
+ }
|
|
+
|
|
+ private void respawn(java.util.function.Consumer<ServerPlayer> respawnComplete, boolean alive) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot respawn entity async");
|
|
+
|
|
+ this.getBukkitEntity(); // force bukkit entity to be created before TPing
|
|
+
|
|
+ if (alive != this.isAlive()) {
|
|
+ throw new IllegalStateException("isAlive expected = " + alive);
|
|
+ }
|
|
+
|
|
+ if (!this.hasNullCallback()) {
|
|
+ this.unRide();
|
|
+ }
|
|
+
|
|
+ if (this.isVehicle() || this.isPassenger()) {
|
|
+ throw new IllegalStateException("Dead player should not be a vehicle or passenger");
|
|
+ }
|
|
+
|
|
+ ServerLevel origin = this.getLevel();
|
|
+ ServerLevel respawnWorld = this.server.getLevel(this.getRespawnDimension());
|
|
+
|
|
+ // modified based off PlayerList#respawn
|
|
+
|
|
+ EntityTreeNode passengerTree = this.makePassengerTree();
|
|
+
|
|
+ this.isChangingDimension = true;
|
|
+ // must be manually removed from connections
|
|
+ this.getLevel().getCurrentWorldData().connections.remove(this.connection.connection);
|
|
+ origin.removePlayerImmediately(this, RemovalReason.CHANGED_DIMENSION);
|
|
+
|
|
+ BlockPos respawnPos = this.getRespawnPosition();
|
|
+ float respawnAngle = this.getRespawnAngle();
|
|
+ boolean isRespawnForced = this.isRespawnForced();
|
|
+
|
|
+ ca.spottedleaf.concurrentutil.completable.Completable<Location> spawnPosComplete =
|
|
+ new ca.spottedleaf.concurrentutil.completable.Completable<>();
|
|
+ boolean[] usedRespawnAnchor = new boolean[1];
|
|
+
|
|
+ // set up post spawn location logic
|
|
+ spawnPosComplete.addWaiter((spawnLoc, throwable) -> {
|
|
+ // update pos and velocity
|
|
+ ServerPlayer.this.setPosRaw(spawnLoc.getX(), spawnLoc.getY(), spawnLoc.getZ());
|
|
+ ServerPlayer.this.setYRot(spawnLoc.getYaw());
|
|
+ ServerPlayer.this.setYHeadRot(spawnLoc.getYaw());
|
|
+ ServerPlayer.this.setXRot(spawnLoc.getPitch());
|
|
+ ServerPlayer.this.setDeltaMovement(Vec3.ZERO);
|
|
+ // placeInAsync will update the world
|
|
+
|
|
+ this.placeInAsync(
|
|
+ origin,
|
|
+ // use the load chunk flag just in case the spawn loc isn't loaded, and to ensure the chunks
|
|
+ // stay loaded for a bit with the teleport ticket
|
|
+ ((CraftWorld)spawnLoc.getWorld()).getHandle(),
|
|
+ TELEPORT_FLAG_LOAD_CHUNK | TELEPORT_FLAGS_PLAYER_RESPAWN | TELEPORT_FLAGS_AVOID_SUFFOCATION,
|
|
+ passengerTree, // note: we expect this to just be the player, no passengers
|
|
+ (entity) -> {
|
|
+ // reset player if needed
|
|
+ // only after placing, so that we don't trip thread checks
|
|
+ if (!alive) {
|
|
+ ServerPlayer.this.reset();
|
|
+ }
|
|
+ // now the player is in the world, and can receive sound
|
|
+ if (usedRespawnAnchor[0]) {
|
|
+ ServerPlayer.this.connection.send(
|
|
+ new ClientboundSoundPacket(
|
|
+ net.minecraft.sounds.SoundEvents.RESPAWN_ANCHOR_DEPLETE, SoundSource.BLOCKS,
|
|
+ ServerPlayer.this.getX(), ServerPlayer.this.getY(), ServerPlayer.this.getZ(),
|
|
+ 1.0F, 1.0F, ServerPlayer.this.getLevel().getRandom().nextLong()
|
|
+ )
|
|
+ );
|
|
+ }
|
|
+ // now the respawn logic is complete
|
|
+
|
|
+ // last, call the function callback
|
|
+ if (respawnComplete != null) {
|
|
+ respawnComplete.accept(ServerPlayer.this);
|
|
+ }
|
|
+ }
|
|
+ );
|
|
+ });
|
|
+
|
|
+ // find and modify respawn block state
|
|
+ if (respawnWorld == null || respawnPos == null) {
|
|
+ // default to regular spawn
|
|
+ fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete);
|
|
+ } else {
|
|
+ // load chunk for block
|
|
+ // give at least 1 radius of loaded chunks so that we do not sync load anything
|
|
+ int radiusBlocks = 16;
|
|
+ respawnWorld.loadChunksAsync(respawnPos, radiusBlocks,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER,
|
|
+ (chunks) -> {
|
|
+ Vec3 spawnPos = findRespawnPositionAndUseSpawnBlock(
|
|
+ respawnWorld, respawnPos, respawnAngle, isRespawnForced, alive
|
|
+ ).orElse(null);
|
|
+ if (spawnPos == null) {
|
|
+ // no spawn
|
|
+ ServerPlayer.this.connection.send(
|
|
+ new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F)
|
|
+ );
|
|
+ ServerPlayer.this.setRespawnPosition(
|
|
+ null, null, 0f, false, false,
|
|
+ com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN
|
|
+ );
|
|
+ // default to regular spawn
|
|
+ fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ boolean isRespawnAnchor = respawnWorld.getBlockState(respawnPos).is(Blocks.RESPAWN_ANCHOR);
|
|
+ boolean isBed = respawnWorld.getBlockState(respawnPos).is(net.minecraft.tags.BlockTags.BEDS);
|
|
+ usedRespawnAnchor[0] = !alive && isRespawnAnchor;
|
|
+
|
|
+ // determine angle
|
|
+ float locAngle;
|
|
+ if (!isBed && !isRespawnAnchor) {
|
|
+ // something else
|
|
+ locAngle = respawnAngle;
|
|
+ } else {
|
|
+ // select angle in direction of the difference applied to respawn pos?
|
|
+ Vec3 vec3d1 = Vec3.atBottomCenterOf(respawnPos).subtract(spawnPos).normalize();
|
|
+
|
|
+ locAngle = (float) Mth.wrapDegrees(Mth.atan2(vec3d1.z, vec3d1.x) * 57.2957763671875D - 90.0D);
|
|
+ }
|
|
+ ServerPlayer.this.setRespawnPosition(
|
|
+ respawnWorld.dimension(), respawnPos, respawnAngle, isRespawnForced, false,
|
|
+ com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN
|
|
+ );
|
|
+
|
|
+ // finished now, pass the location on
|
|
+ spawnPosComplete.complete(
|
|
+ io.papermc.paper.util.MCUtil.toLocation(respawnWorld, spawnPos, locAngle, 0.0f)
|
|
+ );
|
|
+ return;
|
|
+ }
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) {
|
|
+ if (yaw != null) {
|
|
+ this.setYRot(yaw.floatValue());
|
|
+ this.setYHeadRot(yaw.floatValue());
|
|
+ }
|
|
+ if (pitch != null) {
|
|
+ this.setXRot(pitch.floatValue());
|
|
+ }
|
|
+ if (speedDirectionUpdate != null) {
|
|
+ this.setDeltaMovement(speedDirectionUpdate.normalize().scale(this.getDeltaMovement().length()));
|
|
+ }
|
|
+ this.connection.internalTeleport(pos.x, pos.y, pos.z, this.getYRot(), this.getXRot(), java.util.Collections.emptySet());
|
|
+ this.connection.resetPosition();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected ServerPlayer transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) {
|
|
+ // must be manually removed from connections
|
|
+ this.getLevel().getCurrentWorldData().connections.remove(this.connection.connection);
|
|
+ this.getLevel().removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION);
|
|
+
|
|
+ this.spawnIn(destination);
|
|
+ this.transform(pos, yaw, pitch, speedDirectionUpdate);
|
|
+
|
|
+ return this;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void preChangeDimension() {
|
|
+ super.preChangeDimension();
|
|
+ this.stopUsingItem();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) {
|
|
+ if (destination == originWorld && (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L) {
|
|
+ this.unsetRemoved();
|
|
+ destination.addDuringTeleport(this);
|
|
+
|
|
+ // must be manually added to connections
|
|
+ this.getLevel().getCurrentWorldData().connections.add(this.connection.connection);
|
|
+
|
|
+ if ((teleportFlags & TELEPORT_FLAGS_AVOID_SUFFOCATION) != 0L && treeNode.passengers == null && treeNode.parent == null) {
|
|
+ this.avoidSuffocation();
|
|
+ }
|
|
+
|
|
+ // required to set up the pending teleport stuff to the client, and to actually update
|
|
+ // the player's position clientside
|
|
+ this.connection.internalTeleport(
|
|
+ this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot(),
|
|
+ java.util.Collections.emptySet()
|
|
+ );
|
|
+ this.connection.resetPosition();
|
|
+
|
|
+ this.postChangeDimension();
|
|
+ } else {
|
|
+ // Modelled after PlayerList#respawn
|
|
+
|
|
+ // We avoid checking for disconnection here, which means we do not have to add/remove from
|
|
+ // the player list here. We can let this be properly handled by the connection handler
|
|
+
|
|
+ // pre-add logic
|
|
+ PlayerList playerlist = this.server.getPlayerList();
|
|
+ net.minecraft.world.level.storage.LevelData worlddata = destination.getLevelData();
|
|
+ this.connection.send(
|
|
+ new ClientboundRespawnPacket(
|
|
+ destination.dimensionTypeId(), destination.dimension(),
|
|
+ BiomeManager.obfuscateSeed(destination.getSeed()),
|
|
+ this.gameMode.getGameModeForPlayer(),
|
|
+ this.gameMode.getPreviousGameModeForPlayer(),
|
|
+ destination.isDebug(), destination.isFlat(),
|
|
+ // if we do not want to respawn, we aren't dead
|
|
+ (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L ? (byte)1 : (byte)0,
|
|
+ this.getLastDeathLocation()
|
|
+ )
|
|
+ );
|
|
+ // don't bother with the chunk cache radius and simulation distance packets, they are handled
|
|
+ // by the chunk loader
|
|
+ this.spawnIn(destination); // important that destination != null
|
|
+ // we can delay teleport until later, the player position is already set up at the target
|
|
+ this.setShiftKeyDown(false);
|
|
+
|
|
+ this.connection.send(new net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket(
|
|
+ destination.getSharedSpawnPos(), destination.getSharedSpawnAngle()
|
|
+ ));
|
|
+ this.connection.send(new ClientboundChangeDifficultyPacket(
|
|
+ worlddata.getDifficulty(), worlddata.isDifficultyLocked()
|
|
+ ));
|
|
+ this.connection.send(new ClientboundSetExperiencePacket(
|
|
+ this.experienceProgress, this.totalExperience, this.experienceLevel
|
|
+ ));
|
|
+
|
|
+ playerlist.sendLevelInfo(this, destination);
|
|
+ playerlist.sendPlayerPermissionLevel(this);
|
|
+
|
|
+ // regular world add logic
|
|
+ this.unsetRemoved();
|
|
+ destination.addDuringTeleport(this);
|
|
+
|
|
+ // must be manually added to connections
|
|
+ this.getLevel().getCurrentWorldData().connections.add(this.connection.connection);
|
|
+
|
|
+ if ((teleportFlags & TELEPORT_FLAGS_AVOID_SUFFOCATION) != 0L && treeNode.passengers == null && treeNode.parent == null) {
|
|
+ this.avoidSuffocation();
|
|
+ }
|
|
+
|
|
+ // required to set up the pending teleport stuff to the client, and to actually update
|
|
+ // the player's position clientside
|
|
+ this.connection.internalTeleport(
|
|
+ this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot(),
|
|
+ java.util.Collections.emptySet()
|
|
+ );
|
|
+ this.connection.resetPosition();
|
|
+
|
|
+ // delay callback until after post add logic
|
|
+
|
|
+ // post add logic
|
|
+
|
|
+ // "Added from changeDimension"
|
|
+ this.setHealth(this.getHealth());
|
|
+ playerlist.sendAllPlayerInfo(this);
|
|
+ this.onUpdateAbilities();
|
|
+ for (MobEffectInstance mobEffect : this.getActiveEffects()) {
|
|
+ this.connection.send(new ClientboundUpdateMobEffectPacket(this.getId(), mobEffect));
|
|
+ }
|
|
+
|
|
+ this.triggerDimensionChangeTriggers(originWorld);
|
|
+
|
|
+ // finished
|
|
+
|
|
+ this.postChangeDimension();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean endPortalLogicAsync() {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async");
|
|
+
|
|
+ if (this.getLevel().getTypeKey() == LevelStem.END) {
|
|
+ if (!this.canPortalAsync(false)) {
|
|
+ return false;
|
|
+ }
|
|
+ this.wonGame = true;
|
|
+ // TODO is there a better solution to this that DOESN'T skip the credits?
|
|
+ this.seenCredits = true;
|
|
+ this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.WIN_GAME, this.seenCredits ? 0.0F : 1.0F));
|
|
+ this.exitEndCredits();
|
|
+ return true;
|
|
+ } else {
|
|
+ return super.endPortalLogicAsync();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) {
|
|
+ super.prePortalLogic(origin, destination, type);
|
|
+ if (origin.getTypeKey() == LevelStem.OVERWORLD && destination.getTypeKey() == LevelStem.NETHER) {
|
|
+ this.enteredNetherPosition = this.position();
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Nullable
|
|
@Override
|
|
public Entity changeDimension(ServerLevel destination) {
|
|
@@ -2098,6 +2474,12 @@ public class ServerPlayer extends Player {
|
|
public void setCamera(@Nullable Entity entity) {
|
|
Entity entity1 = this.getCamera();
|
|
|
|
+ // Folia start - region threading
|
|
+ if (entity != null && !io.papermc.paper.util.TickThread.isTickThreadFor(entity)) {
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
this.camera = (Entity) (entity == null ? this : entity);
|
|
if (entity1 != this.camera) {
|
|
// Paper start - Add PlayerStartSpectatingEntityEvent and PlayerStopSpectatingEntity Event
|
|
@@ -2559,7 +2941,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 0a3eb5e929c605d9eb7369de8ade8b49951f5d37..6b7b44ad8cb7ece5cfbf69bc54ed589a34f448a2 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
|
|
@@ -123,11 +123,11 @@ public class ServerPlayerGameMode {
|
|
}
|
|
|
|
public void tick() {
|
|
- this.gameTicks = MinecraftServer.currentTick; // CraftBukkit;
|
|
+ ++this.gameTicks; // CraftBukkit; // Folia - region threading
|
|
BlockState iblockdata;
|
|
|
|
if (this.hasDelayedDestroy) {
|
|
- iblockdata = this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper
|
|
+ iblockdata = !io.papermc.paper.util.TickThread.isTickThreadFor(this.level, this.delayedDestroyPos) ? null : this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper // Folia - region threading - don't destroy blocks not owned
|
|
if (iblockdata == null || iblockdata.isAir()) { // Paper
|
|
this.hasDelayedDestroy = false;
|
|
} else {
|
|
@@ -140,7 +140,7 @@ public class ServerPlayerGameMode {
|
|
}
|
|
} else if (this.isDestroyingBlock) {
|
|
// Paper start - don't want to do same logic as above, return instead
|
|
- iblockdata = this.level.getBlockStateIfLoaded(this.destroyPos);
|
|
+ iblockdata = !io.papermc.paper.util.TickThread.isTickThreadFor(this.level, this.destroyPos) ? null : this.level.getBlockStateIfLoaded(this.destroyPos); // Folia - region threading - don't destroy blocks not owned
|
|
if (iblockdata == null) {
|
|
this.isDestroyingBlock = false;
|
|
return;
|
|
@@ -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<net.minecraft.world.entity.item.ItemEntity> itemsToDrop = level.captureDrops; // Paper - store current list
|
|
- level.captureDrops = null; // Paper - Remove this earlier so that we can actually drop stuff
|
|
+ java.util.List<net.minecraft.world.entity.item.ItemEntity> itemsToDrop = level.getCurrentWorldData().captureDrops; // Paper - store current list // Folia - region threading
|
|
+ level.getCurrentWorldData().captureDrops = null; // Paper - Remove this earlier so that we can actually drop stuff // Folia - region threading
|
|
if (event.isDropItems()) {
|
|
org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDropItemEvent(bblock, state, this.player, itemsToDrop); // Paper - use stored ref
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
|
|
index 660693c6dc0ef86f4013df980b6d0c11c03e46cd..b64fc5adda3a9ab793a074d08df822754bd8d186 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
|
|
@@ -98,10 +98,15 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
this.chunkMap.level.chunkTaskScheduler.lightExecutor.queueRunnable(() -> { // Paper - rewrite chunk system
|
|
this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> {
|
|
chunkLightCallback.accept(chunkPos);
|
|
- ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> {
|
|
+ Runnable run = () -> { // Folia - region threading
|
|
((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()).broadcast(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(chunkPos, ThreadedLevelLightEngine.this, null, null, true), false);
|
|
((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, io.papermc.paper.util.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos));
|
|
- });
|
|
+ }; // Folia - region threading
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(
|
|
+ (ServerLevel)this.theLightEngine.getWorld(), chunkPos.x, chunkPos.z, run
|
|
+ );
|
|
+ // Folia end - region threading
|
|
}, onComplete);
|
|
});
|
|
this.tryScheduleUpdate();
|
|
@@ -109,7 +114,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
return totalChunks;
|
|
}
|
|
|
|
- private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap();
|
|
+ //private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); // Folia - region threading
|
|
|
|
private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, final Supplier<CompletableFuture<Void>> runnable) {
|
|
final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld();
|
|
@@ -128,11 +133,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
return;
|
|
}
|
|
|
|
- if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) {
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(world, chunkX, chunkZ)) { // Folia - region threading
|
|
// ticket logic is not safe to run off-main, re-schedule
|
|
- world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> {
|
|
+ Runnable run = () -> { // Folia - region threading
|
|
this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable);
|
|
- });
|
|
+ }; // Folia - region threading
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ world, chunkX, chunkZ, run
|
|
+ );
|
|
+ // Folia end - region threading
|
|
return;
|
|
}
|
|
|
|
@@ -145,22 +155,28 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
return;
|
|
}
|
|
|
|
- final int references = this.chunksBeingWorkedOn.addTo(key, 1);
|
|
+ final int references = this.chunkMap.level.getCurrentWorldData().chunksBeingWorkedOn.addTo(key, 1); // Folia - region threading
|
|
if (references == 0) {
|
|
final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
|
|
world.getChunkSource().addRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos);
|
|
}
|
|
|
|
- updateFuture.thenAcceptAsync((final Void ignore) -> {
|
|
- final int newReferences = this.chunksBeingWorkedOn.get(key);
|
|
- if (newReferences == 1) {
|
|
- this.chunksBeingWorkedOn.remove(key);
|
|
- final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
|
|
- world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos);
|
|
- } else {
|
|
- this.chunksBeingWorkedOn.put(key, newReferences - 1);
|
|
- }
|
|
- }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> {
|
|
+ // Folia start - region threading
|
|
+ updateFuture.thenAccept((final Void ignore) -> {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ this.chunkMap.level, chunkX, chunkZ, () -> {
|
|
+ final int newReferences = this.chunkMap.level.getCurrentWorldData().chunksBeingWorkedOn.get(key);
|
|
+ if (newReferences == 1) {
|
|
+ this.chunkMap.level.getCurrentWorldData().chunksBeingWorkedOn.remove(key);
|
|
+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
|
|
+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos);
|
|
+ } else {
|
|
+ this.chunkMap.level.getCurrentWorldData().chunksBeingWorkedOn.put(key, newReferences - 1);
|
|
+ }
|
|
+ }
|
|
+ );
|
|
+ }).whenComplete((final Void ignore, final Throwable thr) -> {
|
|
+ // Folia end - region threading
|
|
if (thr != null) {
|
|
LOGGER.error("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr);
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
|
|
index 97d1ff2af23bac14e67bca5896843325aaa5bfc1..8bede935166cfe0defbc7bb1e959bd91f2cd1c6e 100644
|
|
--- a/src/main/java/net/minecraft/server/level/TicketType.java
|
|
+++ b/src/main/java/net/minecraft/server/level/TicketType.java
|
|
@@ -35,6 +35,15 @@ public class TicketType<T> {
|
|
public static final TicketType<Long> POI_LOAD = create("poi_load", Long::compareTo);
|
|
public static final TicketType<Unit> UNLOAD_COOLDOWN = create("unload_cooldown", (u1, u2) -> 0, 5 * 20);
|
|
// Paper end - rewrite chunk system
|
|
+ // Folia start - region threading
|
|
+ public static final TicketType<Unit> LOGIN = create("login", (u1, u2) -> 0, 20);
|
|
+ public static final TicketType<Unit> DELAYED = create("delay", (u1, u2) -> 0, 5);
|
|
+ public static final TicketType<Long> END_GATEWAY_EXIT_SEARCH = create("end_gateway_exit_search", Long::compareTo);
|
|
+ public static final TicketType<Long> NON_FULL_SYNC_LOAD = create("non_full_sync_load", Long::compareTo);
|
|
+ public static final TicketType<Long> NETHER_PORTAL_DOUBLE_CHECK = create("nether_portal_double_check", Long::compareTo);
|
|
+ public static final TicketType<Long> TELEPORT_HOLD_TICKET = create("teleport_hold_ticket", Long::compareTo);
|
|
+ public static final TicketType<Unit> REGION_SCHEDULER_API_HOLD = create("region_scheduler_api_hold", (a, b) -> 0);
|
|
+ // Folia end - region threading
|
|
|
|
public static <T> TicketType<T> create(String name, Comparator<T> 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<ChunkAccess> chunks, ChunkStatus status, int placementRadius) {
|
|
this.generatingStatus = status;
|
|
this.writeRadiusCutoff = placementRadius;
|
|
diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
|
|
index 44d99e89226adb6234b9405f25ac9dab9bd84297..072634e26d32ca0b3438a5d3a03be3670d0f846e 100644
|
|
--- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
|
|
+++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
|
|
@@ -154,10 +154,13 @@ public class ServerConnectionListener {
|
|
// Paper end
|
|
|
|
//ServerConnectionListener.this.connections.add(object);
|
|
- pending.add(object); // Paper
|
|
+ // Folia - connection fixes - move down
|
|
channelpipeline.addLast("packet_handler", (ChannelHandler) object);
|
|
((Connection) object).setListener(new ServerHandshakePacketListenerImpl(ServerConnectionListener.this.server, (Connection) object));
|
|
io.papermc.paper.network.ChannelInitializeListenerHolder.callListeners(channel); // Paper
|
|
+ // Folia start - regionised threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addConnection((Connection)object);
|
|
+ // Folia end - regionised threading
|
|
}
|
|
}).group((EventLoopGroup) lazyinitvar.get()).localAddress(address)).option(ChannelOption.AUTO_READ, false).bind().syncUninterruptibly()); // CraftBukkit // Paper
|
|
}
|
|
@@ -218,7 +221,7 @@ public class ServerConnectionListener {
|
|
// Spigot Start
|
|
this.addPending(); // Paper
|
|
// This prevents players from 'gaming' the server, and strategically relogging to increase their position in the tick order
|
|
- if ( org.spigotmc.SpigotConfig.playerShuffle > 0 && MinecraftServer.currentTick % org.spigotmc.SpigotConfig.playerShuffle == 0 )
|
|
+ if ( org.spigotmc.SpigotConfig.playerShuffle > 0 && 0 % org.spigotmc.SpigotConfig.playerShuffle == 0 ) // Folia - region threading
|
|
{
|
|
Collections.shuffle( this.connections );
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
|
|
index 2d8488b9aa088b6d5f0c7e557b8ad0b29bc4cd88..aab6ffceaabecf8efa7f5d83f30a537a3f62c7bd 100644
|
|
--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
|
|
+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
|
|
@@ -322,10 +322,10 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
|
|
private final org.bukkit.craftbukkit.CraftServer cserver;
|
|
public boolean processedDisconnect;
|
|
- private int lastTick = MinecraftServer.currentTick;
|
|
+ private long lastTick = Util.getMillis() / 50L; // Folia - region threading
|
|
private int allowedPlayerTicks = 1;
|
|
- private int lastDropTick = MinecraftServer.currentTick;
|
|
- private int lastBookTick = MinecraftServer.currentTick;
|
|
+ private long lastDropTick = Util.getMillis() / 50L; // Folia - region threading
|
|
+ private long lastBookTick = Util.getMillis() / 50L; // Folia - region threading
|
|
private int dropCount = 0;
|
|
|
|
// Get position of last block hit for BlockDamageLevel.STOPPED
|
|
@@ -342,8 +342,40 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
}
|
|
// CraftBukkit end
|
|
|
|
+ // Folia start - region threading
|
|
+ public net.minecraft.world.level.ChunkPos disconnectPos;
|
|
+ private static final java.util.concurrent.atomic.AtomicLong DISCONNECT_TICKET_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong();
|
|
+ public static final net.minecraft.server.level.TicketType<Long> DISCONNECT_TICKET = net.minecraft.server.level.TicketType.create("disconnect_ticket", Long::compareTo);
|
|
+ public final Long disconnectTicketId = Long.valueOf(DISCONNECT_TICKET_ID_GENERATOR.getAndIncrement());
|
|
+
|
|
+ private void checkKeepAlive() {
|
|
+ long currentTime = Util.getMillis();
|
|
+ long elapsedTime = currentTime - this.keepAliveTime;
|
|
+
|
|
+ if (this.keepAlivePending) {
|
|
+ if (!this.processedDisconnect && elapsedTime >= KEEPALIVE_LIMIT) { // check keepalive limit, don't fire if already disconnected
|
|
+ ServerGamePacketListenerImpl.LOGGER.warn("{} was kicked due to keepalive timeout!", this.player.getScoreboardName()); // more info
|
|
+ this.disconnect(Component.translatable("disconnect.timeout", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause
|
|
+ }
|
|
+ } else {
|
|
+ if (elapsedTime >= 15000L) { // 15 seconds
|
|
+ this.keepAlivePending = true;
|
|
+ this.keepAliveTime = currentTime;
|
|
+ this.keepAliveChallenge = currentTime;
|
|
+ this.send(new ClientboundKeepAlivePacket(this.keepAliveChallenge));
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Override
|
|
public void tick() {
|
|
+ // Folia start - region threading
|
|
+ this.checkKeepAlive();
|
|
+ if (this.player.wonGame) {
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
if (this.ackBlockChangesUpTo > -1) {
|
|
this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo));
|
|
this.ackBlockChangesUpTo = -1;
|
|
@@ -395,22 +427,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
this.server.getProfiler().push("keepAlive");
|
|
// Paper Start - give clients a longer time to respond to pings as per pre 1.12.2 timings
|
|
// This should effectively place the keepalive handling back to "as it was" before 1.12.2
|
|
- long currentTime = Util.getMillis();
|
|
- long elapsedTime = currentTime - this.keepAliveTime;
|
|
-
|
|
- if (this.keepAlivePending) {
|
|
- if (!this.processedDisconnect && elapsedTime >= KEEPALIVE_LIMIT) { // check keepalive limit, don't fire if already disconnected
|
|
- ServerGamePacketListenerImpl.LOGGER.warn("{} was kicked due to keepalive timeout!", this.player.getScoreboardName()); // more info
|
|
- this.disconnect(Component.translatable("disconnect.timeout", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause
|
|
- }
|
|
- } else {
|
|
- if (elapsedTime >= 15000L) { // 15 seconds
|
|
- this.keepAlivePending = true;
|
|
- this.keepAliveTime = currentTime;
|
|
- this.keepAliveChallenge = currentTime;
|
|
- this.send(new ClientboundKeepAlivePacket(this.keepAliveChallenge));
|
|
- }
|
|
- }
|
|
+ // Folia - region threading - move to own method above
|
|
// Paper end
|
|
|
|
this.server.getProfiler().pop();
|
|
@@ -443,6 +460,19 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
this.lastGoodX = this.player.getX();
|
|
this.lastGoodY = this.player.getY();
|
|
this.lastGoodZ = this.player.getZ();
|
|
+ // Folia start - support vehicle teleportations
|
|
+ this.lastVehicle = this.player.getRootVehicle();
|
|
+ if (this.lastVehicle != this.player && this.lastVehicle.getControllingPassenger() == this.player) {
|
|
+ this.vehicleFirstGoodX = this.lastVehicle.getX();
|
|
+ this.vehicleFirstGoodY = this.lastVehicle.getY();
|
|
+ this.vehicleFirstGoodZ = this.lastVehicle.getZ();
|
|
+ this.vehicleLastGoodX = this.lastVehicle.getX();
|
|
+ this.vehicleLastGoodY = this.lastVehicle.getY();
|
|
+ this.vehicleLastGoodZ = this.lastVehicle.getZ();
|
|
+ } else {
|
|
+ this.lastVehicle = null;
|
|
+ }
|
|
+ // Folia end - support vehicle teleportations
|
|
}
|
|
|
|
@Override
|
|
@@ -479,24 +509,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
if (this.processedDisconnect) {
|
|
return;
|
|
}
|
|
- if (!this.cserver.isPrimaryThread()) {
|
|
- Waitable waitable = new Waitable() {
|
|
- @Override
|
|
- protected Object evaluate() {
|
|
- ServerGamePacketListenerImpl.this.disconnect(reason, cause); // Paper - adventure, kick event cause
|
|
- return null;
|
|
- }
|
|
- };
|
|
-
|
|
- this.server.processQueue.add(waitable);
|
|
-
|
|
- try {
|
|
- waitable.get();
|
|
- } catch (InterruptedException e) {
|
|
- Thread.currentThread().interrupt();
|
|
- } catch (ExecutionException e) {
|
|
- throw new RuntimeException(e);
|
|
- }
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(this.player)) { // Folia - region threading
|
|
+ this.connection.disconnectSafely(PaperAdventure.asVanilla(reason), cause); // Folia - region threading - it HAS to be delayed/async to avoid deadlock if we try to wait for another region
|
|
return;
|
|
}
|
|
|
|
@@ -527,7 +541,6 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
|
|
Objects.requireNonNull(this.connection);
|
|
// CraftBukkit - Don't wait
|
|
- minecraftserver.scheduleOnMain(networkmanager::handleDisconnection); // Paper
|
|
}
|
|
|
|
private <T, R> CompletableFuture<R> filterTextPacket(T text, BiFunction<TextFilter, T, CompletableFuture<R>> filterer) {
|
|
@@ -610,9 +623,10 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
// Paper end - fix large move vectors killing the server
|
|
|
|
// CraftBukkit start - handle custom speeds and skipped ticks
|
|
- this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick;
|
|
+ int currTick = (int)(Util.getMillis() / 50); // Folia - region threading
|
|
+ this.allowedPlayerTicks += currTick - this.lastTick; // Folia - region threading
|
|
this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1);
|
|
- this.lastTick = (int) (System.currentTimeMillis() / 50);
|
|
+ this.lastTick = (int) currTick; // Folia - region threading
|
|
|
|
++this.receivedMovePacketCount;
|
|
int i = this.receivedMovePacketCount - this.knownMovePacketCount;
|
|
@@ -866,13 +880,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
// PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); // Paper - run this async
|
|
// CraftBukkit start
|
|
if (this.chatSpamTickCount.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamLimit && !this.server.getPlayerList().isOp(this.player.getGameProfile())) { // Paper start - split and make configurable
|
|
- server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause
|
|
+ this.disconnect(Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause // Folia - region threading
|
|
return;
|
|
}
|
|
// Paper start
|
|
String str = packet.getCommand(); int index = -1;
|
|
if (str.length() > 64 && ((index = str.indexOf(' ')) == -1 || index >= 64)) {
|
|
- server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause
|
|
+ this.disconnect(Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause // Folia - region threading
|
|
return;
|
|
}
|
|
// Paper end
|
|
@@ -897,7 +911,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
if (!event.isHandled()) {
|
|
if (!event.isCancelled()) {
|
|
|
|
- this.server.scheduleOnMain(() -> { // This needs to be on main
|
|
+ this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading
|
|
ParseResults<CommandSourceStack> parseresults = this.server.getCommands().getDispatcher().parse(stringreader, this.player.createCommandSourceStack());
|
|
|
|
this.server.getCommands().getDispatcher().getCompletionSuggestions(parseresults).thenAccept((suggestions) -> {
|
|
@@ -908,7 +922,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
this.connection.send(new ClientboundCommandSuggestionsPacket(packet.getId(), suggestEvent.getSuggestions()));
|
|
// Paper end - Brigadier API
|
|
});
|
|
- });
|
|
+ }, null, 1L); // Folia - region threading
|
|
}
|
|
} else if (!completions.isEmpty()) {
|
|
final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(command, stringreader.getTotalLength());
|
|
@@ -1217,7 +1231,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
int byteLength = testString.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
|
|
if (byteLength > 256 * 4) {
|
|
ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send a book with with a page too large!");
|
|
- server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause
|
|
+ this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Folia - region threading
|
|
return;
|
|
}
|
|
byteTotal += byteLength;
|
|
@@ -1240,17 +1254,17 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
|
|
if (byteTotal > byteAllowed) {
|
|
ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send too large of a book. Book Size: " + byteTotal + " - Allowed: "+ byteAllowed + " - Pages: " + pageList.size());
|
|
- server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause
|
|
+ this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Folia - region threading
|
|
return;
|
|
}
|
|
}
|
|
// Paper end
|
|
// CraftBukkit start
|
|
- if (this.lastBookTick + 20 > MinecraftServer.currentTick) {
|
|
- server.scheduleOnMain(() -> this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause // Paper - Also ensure this is called on main
|
|
+ if (this.lastBookTick + 20 > this.lastTick) {
|
|
+ this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Paper - Also ensure this is called on main // Folia - region threading
|
|
return;
|
|
}
|
|
- this.lastBookTick = MinecraftServer.currentTick;
|
|
+ this.lastBookTick = this.lastTick;
|
|
// CraftBukkit end
|
|
int i = packet.getSlot();
|
|
|
|
@@ -1437,9 +1451,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);
|
|
@@ -1819,9 +1834,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
if (!this.player.isSpectator()) {
|
|
// limit how quickly items can be dropped
|
|
// If the ticks aren't the same then the count starts from 0 and we update the lastDropTick.
|
|
- if (this.lastDropTick != MinecraftServer.currentTick) {
|
|
+ if (this.lastDropTick != io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) {
|
|
this.dropCount = 0;
|
|
- this.lastDropTick = MinecraftServer.currentTick;
|
|
+ this.lastDropTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick();
|
|
} else {
|
|
// Else we increment the drop count and check the amount.
|
|
this.dropCount++;
|
|
@@ -1849,7 +1864,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
case ABORT_DESTROY_BLOCK:
|
|
case STOP_DESTROY_BLOCK:
|
|
// Paper start - Don't allow digging in unloaded chunks
|
|
- if (this.player.level.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4) == null) {
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(this.player.getLevel(), blockposition.getX() >> 4, blockposition.getZ() >> 4, 8) || this.player.level.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4) == null) { // Folia - region threading - don't destroy blocks not owned
|
|
this.player.connection.ackBlockChangesUpTo(packet.getSequence());
|
|
return;
|
|
}
|
|
@@ -1933,7 +1948,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
BlockPos blockposition = movingobjectpositionblock.getBlockPos();
|
|
Vec3 vec3d1 = Vec3.atCenterOf(blockposition);
|
|
|
|
- if (this.player.getEyePosition().distanceToSqr(vec3d1) <= ServerGamePacketListenerImpl.MAX_INTERACTION_DISTANCE) {
|
|
+ if (io.papermc.paper.util.TickThread.isTickThreadFor(this.player.getLevel(), blockposition.getX() >> 4, blockposition.getZ() >> 4, 8) && this.player.getEyePosition().distanceToSqr(vec3d1) <= ServerGamePacketListenerImpl.MAX_INTERACTION_DISTANCE) { // Folia - do not allow players to interact with blocks outside the current region
|
|
Vec3 vec3d2 = vec3d.subtract(vec3d1);
|
|
double d0 = 1.0000001D;
|
|
|
|
@@ -2046,7 +2061,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
Entity entity = packet.getEntity(worldserver);
|
|
|
|
if (entity != null) {
|
|
- this.player.teleportTo(worldserver, entity.getX(), entity.getY(), entity.getZ(), entity.getYRot(), entity.getXRot(), org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit
|
|
+ io.papermc.paper.threadedregions.TeleportUtils.teleport(this.player, false, entity, null, null, Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE, null); // Folia - region threading
|
|
return;
|
|
}
|
|
}
|
|
@@ -2109,6 +2124,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
|
|
@@ -2193,9 +2210,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<LastSeenMessages> optional = this.tryHandleChat(packet.message(), packet.timeStamp(), packet.lastSeenMessages());
|
|
|
|
@@ -2229,17 +2246,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<LastSeenMessages> 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
|
|
}
|
|
|
|
}
|
|
@@ -2313,9 +2330,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
private Optional<LastSeenMessages> tryHandleChat(String message, Instant timestamp, LastSeenMessages.Update acknowledgment) {
|
|
if (!this.updateChatOrder(timestamp)) {
|
|
ServerGamePacketListenerImpl.LOGGER.warn("{} sent out-of-order chat: '{}': {} > {}", this.player.getName().getString(), message, this.lastChatTimeStamp.get().getEpochSecond(), timestamp.getEpochSecond()); // Paper
|
|
- this.server.scheduleOnMain(() -> { // Paper - push to main
|
|
+ // Folia - region threading
|
|
this.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat"), org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event ca
|
|
- }); // Paper - push to main
|
|
+ // Folia - region threading
|
|
return Optional.empty();
|
|
} else {
|
|
Optional<LastSeenMessages> optional = this.unpackAndApplyLastSeen(acknowledgment);
|
|
@@ -2390,7 +2407,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());
|
|
@@ -2468,6 +2485,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<Void> wait = new Waitable<>() {
|
|
@@ -2528,6 +2546,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
|
|
@@ -2769,7 +2788,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
|
|
this.player.resetLastActionTime();
|
|
this.player.setShiftKeyDown(packet.isUsingSecondaryAction());
|
|
- if (entity != null) {
|
|
+ if (io.papermc.paper.util.TickThread.isTickThreadFor(entity) && entity != null) { // Folia - region threading - do not allow interaction of entities outside the current region
|
|
if (!worldserver.getWorldBorder().isWithinBounds(entity.blockPosition())) {
|
|
return;
|
|
}
|
|
@@ -2909,6 +2928,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);
|
|
@@ -2917,6 +2942,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
|
|
@@ -3269,7 +3306,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;
|
|
}
|
|
}
|
|
@@ -3411,7 +3448,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<FilteredText> signText) {
|
|
@@ -3481,9 +3524,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
this.keepAlivePending = false;
|
|
} else if (!this.isSingleplayerOwner()) {
|
|
// Paper start - This needs to be handled on the main thread for plugins
|
|
- server.submit(() -> {
|
|
+ // Folia - region threading - do not schedule to main anymore, there is no main
|
|
this.disconnect(Component.translatable("disconnect.timeout"), org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause
|
|
- });
|
|
+ // Folia - region threading - do not schedule to main anymore, there is no main
|
|
// Paper end
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
|
|
index 2ff578e4a953ffcf5176815ba8e3f06f73499989..2e96377d628b3a07fb565020074d665f594f32e8 100644
|
|
--- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
|
|
+++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
|
|
@@ -53,7 +53,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
|
|
private final byte[] challenge;
|
|
final MinecraftServer server;
|
|
public final Connection connection;
|
|
- public ServerLoginPacketListenerImpl.State state;
|
|
+ public volatile ServerLoginPacketListenerImpl.State state; // Folia - region threading
|
|
private int tick;
|
|
public @Nullable
|
|
GameProfile gameProfile;
|
|
@@ -80,20 +80,14 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
|
|
}
|
|
// Paper end
|
|
if (this.state == ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT) {
|
|
- // Paper start - prevent logins to be processed even though disconnect was called
|
|
- if (connection.isConnected()) {
|
|
+ // Folia start - region threading - rewrite login process
|
|
+ String name = this.gameProfile.getName();
|
|
+ UUID uniqueId = UUIDUtil.getOrCreatePlayerUUID(this.gameProfile);
|
|
+ if (this.server.getPlayerList().pushPendingJoin(name, uniqueId, this.connection)) {
|
|
this.handleAcceptedLogin();
|
|
}
|
|
- // Paper end
|
|
- } else if (this.state == ServerLoginPacketListenerImpl.State.DELAY_ACCEPT) {
|
|
- ServerPlayer entityplayer = this.server.getPlayerList().getPlayer(this.gameProfile.getId());
|
|
-
|
|
- if (entityplayer == null) {
|
|
- this.state = ServerLoginPacketListenerImpl.State.READY_TO_ACCEPT;
|
|
- this.placeNewPlayer(this.delayedAcceptPlayer);
|
|
- this.delayedAcceptPlayer = null;
|
|
- }
|
|
- }
|
|
+ // Folia end - region threading - rewrite login process
|
|
+ } // Folia - region threading - remove delayed accept
|
|
|
|
if (this.tick++ == 600) {
|
|
this.disconnect(Component.translatable("multiplayer.disconnect.slow_login"));
|
|
@@ -163,7 +157,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
|
|
// this.disconnect(ichatbasecomponent);
|
|
// CraftBukkit end
|
|
} else {
|
|
- this.state = ServerLoginPacketListenerImpl.State.ACCEPTED;
|
|
+ this.state = ServerLoginPacketListenerImpl.State.HANDING_OFF; // Folia - region threading - rewrite login process
|
|
if (this.server.getCompressionThreshold() >= 0 && !this.connection.isMemoryConnection()) {
|
|
this.connection.send(new ClientboundLoginCompressionPacket(this.server.getCompressionThreshold()), PacketSendListener.thenRun(() -> {
|
|
this.connection.setupCompression(this.server.getCompressionThreshold(), true);
|
|
@@ -171,17 +165,55 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
|
|
}
|
|
|
|
this.connection.send(new ClientboundGameProfilePacket(this.gameProfile));
|
|
- ServerPlayer entityplayer = this.server.getPlayerList().getPlayer(this.gameProfile.getId());
|
|
+ // Folia - region threading - rewrite login process
|
|
|
|
try {
|
|
- ServerPlayer entityplayer1 = this.server.getPlayerList().getPlayerForLogin(this.gameProfile, s); // CraftBukkit - add player reference
|
|
-
|
|
- if (entityplayer != null) {
|
|
- this.state = ServerLoginPacketListenerImpl.State.DELAY_ACCEPT;
|
|
- this.delayedAcceptPlayer = entityplayer1;
|
|
- } else {
|
|
- this.placeNewPlayer(entityplayer1);
|
|
- }
|
|
+ // Folia start - region threading - rewrite login process
|
|
+ org.apache.commons.lang3.mutable.MutableObject<net.minecraft.nbt.CompoundTag> data = new org.apache.commons.lang3.mutable.MutableObject<>();
|
|
+ org.apache.commons.lang3.mutable.MutableObject<String> lastKnownName = new org.apache.commons.lang3.mutable.MutableObject<>();
|
|
+ ca.spottedleaf.concurrentutil.completable.Completable<org.bukkit.Location> toComplete = new ca.spottedleaf.concurrentutil.completable.Completable<>();
|
|
+ // note: need to call addWaiter before completion to ensure the callback is invoked synchronously
|
|
+ // the loadSpawnForNewPlayer function always completes the completable once the chunks were loaded,
|
|
+ // on the load callback for those chunks (so on the same region)
|
|
+ // this guarantees the chunk cannot unload under our feet
|
|
+ toComplete.addWaiter((org.bukkit.Location loc, Throwable t) -> {
|
|
+ int chunkX = net.minecraft.util.Mth.floor(loc.getX()) >> 4;
|
|
+ int chunkZ = net.minecraft.util.Mth.floor(loc.getZ()) >> 4;
|
|
+
|
|
+ net.minecraft.server.level.ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)loc.getWorld()).getHandle();
|
|
+ // we just need to hold the chunks at loaded until the next tick
|
|
+ // so we do not need to care about unique IDs for the ticket
|
|
+ world.getChunkSource().addTicketAtLevel(
|
|
+ net.minecraft.server.level.TicketType.LOGIN,
|
|
+ new net.minecraft.world.level.ChunkPos(chunkX, chunkZ),
|
|
+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL,
|
|
+ net.minecraft.util.Unit.INSTANCE
|
|
+ );
|
|
+
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ world, chunkX, chunkZ,
|
|
+ () -> {
|
|
+ // now at this point the connection is held by the region, so we have to check isConnected()
|
|
+ // this would have been handled in connection ticking, but we are in a state between
|
|
+ // being owned by the global tick thread and the region so we have to do it
|
|
+ if (t != null || !ServerLoginPacketListenerImpl.this.connection.isConnected()) {
|
|
+ ServerLoginPacketListenerImpl.this.connection.handleDisconnection();
|
|
+ return;
|
|
+ }
|
|
+ ServerLoginPacketListenerImpl.this.state = State.ACCEPTED;
|
|
+ ServerLoginPacketListenerImpl.this.server.getPlayerList().placeNewPlayer(
|
|
+ ServerLoginPacketListenerImpl.this.connection,
|
|
+ s,
|
|
+ data.getValue(),
|
|
+ lastKnownName.getValue(),
|
|
+ loc
|
|
+ );
|
|
+ },
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER
|
|
+ );
|
|
+ });
|
|
+ this.server.getPlayerList().loadSpawnForNewPlayer(this.connection, s, data, lastKnownName, toComplete);
|
|
+ // Folia end - region threading - rewrite login process
|
|
} catch (Exception exception) {
|
|
ServerLoginPacketListenerImpl.LOGGER.error("Couldn't place player in world", exception);
|
|
MutableComponent ichatmutablecomponent = Component.translatable("multiplayer.disconnect.invalid_player_data");
|
|
@@ -198,9 +230,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
|
|
|
|
}
|
|
|
|
- private void placeNewPlayer(ServerPlayer player) {
|
|
- this.server.getPlayerList().placeNewPlayer(this.connection, player);
|
|
- }
|
|
+ // Folia end - region threading - rewrite login process
|
|
|
|
@Override
|
|
public void onDisconnect(Component reason) {
|
|
@@ -397,7 +427,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
|
|
uniqueId = gameProfile.getId();
|
|
// Paper end
|
|
|
|
- if (PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) {
|
|
+ if (false && PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { // Folia - region threading
|
|
final PlayerPreLoginEvent event = new PlayerPreLoginEvent(playerName, address, uniqueId);
|
|
if (asyncEvent.getResult() != PlayerPreLoginEvent.Result.ALLOWED) {
|
|
event.disallow(asyncEvent.getResult(), asyncEvent.kickMessage()); // Paper - Adventure
|
|
@@ -480,7 +510,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
|
|
|
|
public static enum State {
|
|
|
|
- HELLO, KEY, AUTHENTICATING, NEGOTIATING, READY_TO_ACCEPT, DELAY_ACCEPT, ACCEPTED;
|
|
+ HELLO, KEY, AUTHENTICATING, NEGOTIATING, READY_TO_ACCEPT, DELAY_ACCEPT, HANDING_OFF, ACCEPTED; // Folia - region threading
|
|
|
|
private State() {}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
index 0dbe182fbae5ce5ba182176eb5d5e3f1897e77f2..e79d96f8c6dccc2289a93a66c25df885afdc1c3f 100644
|
|
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
@@ -139,7 +139,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<ServerPlayer> players = new java.util.concurrent.CopyOnWriteArrayList(); // CraftBukkit - ArrayList -> CopyOnWriteArrayList: Iterator safety
|
|
- private final Map<UUID, ServerPlayer> playersByUUID = Maps.newHashMap();
|
|
+ private final Map<UUID, ServerPlayer> playersByUUID = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading - change to CHM - Note: we do NOT expect concurrency PER KEY!
|
|
private final UserBanList bans;
|
|
private final IpBanList ipBans;
|
|
private final ServerOpList ops;
|
|
@@ -161,9 +161,56 @@ public abstract class PlayerList {
|
|
|
|
// CraftBukkit start
|
|
private CraftServer cserver;
|
|
- private final Map<String,ServerPlayer> playersByName = new java.util.HashMap<>();
|
|
+ private final Map<String,ServerPlayer> 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<String, Connection> connectionByName = new java.util.HashMap<>();
|
|
+ private final Map<UUID, Connection> 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<RegistryLayer> registryManager, PlayerDataStorage saveHandler, int maxPlayers) {
|
|
this.cserver = server.server = new CraftServer((DedicatedServer) server, this);
|
|
server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper
|
|
@@ -185,7 +232,7 @@ public abstract class PlayerList {
|
|
}
|
|
abstract public void loadAndSaveFiles(); // Paper - moved from DedicatedPlayerList constructor
|
|
|
|
- public void placeNewPlayer(Connection connection, ServerPlayer player) {
|
|
+ public void loadSpawnForNewPlayer(Connection connection, ServerPlayer player, org.apache.commons.lang3.mutable.MutableObject<CompoundTag> data, org.apache.commons.lang3.mutable.MutableObject<String> lastKnownName, ca.spottedleaf.concurrentutil.completable.Completable<Location> toComplete) {
|
|
player.isRealPlayer = true; // Paper
|
|
player.loginTime = System.currentTimeMillis(); // Paper
|
|
GameProfile gameprofile = player.getGameProfile();
|
|
@@ -235,8 +282,28 @@ public abstract class PlayerList {
|
|
worldserver1 = worldserver;
|
|
}
|
|
|
|
- if (nbttagcompound == null) player.fudgeSpawnLocation(worldserver1); // Paper - only move to spawn on first login, otherwise, stay where you are....
|
|
-
|
|
+ // Folia start - region threading - rewrite login process
|
|
+ if (nbttagcompound == null) ServerPlayer.fudgeSpawnLocation(worldserver1, player, toComplete); // Folia - only move to spawn on first login, otherwise, stay where you are....
|
|
+ if (nbttagcompound != null) {
|
|
+ worldserver1.loadChunksForMoveAsync(
|
|
+ player.getBoundingBox(),
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER,
|
|
+ (c) -> {
|
|
+ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(worldserver1, player.position()));
|
|
+ }
|
|
+ );
|
|
+ }
|
|
+ data.setValue(nbttagcompound);
|
|
+ lastKnownName.setValue(s);
|
|
+ return;
|
|
+ }
|
|
+ // nbttagcomound -> player data
|
|
+ // s -> last known name
|
|
+ public void placeNewPlayer(Connection connection, ServerPlayer player, CompoundTag nbttagcompound, String s, Location selectedSpawn) {
|
|
+ ServerLevel worldserver1 = ((CraftWorld)selectedSpawn.getWorld()).getHandle();
|
|
+ player.setPosRaw(selectedSpawn.getX(), selectedSpawn.getY(), selectedSpawn.getZ());
|
|
+ player.lastSave = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick();
|
|
+ // Folia end - region threading - rewrite login process
|
|
player.setLevel(worldserver1);
|
|
String s1 = "local";
|
|
|
|
@@ -247,7 +314,7 @@ public abstract class PlayerList {
|
|
// Spigot start - spawn location event
|
|
Player spawnPlayer = player.getBukkitEntity();
|
|
org.spigotmc.event.player.PlayerSpawnLocationEvent ev = new com.destroystokyo.paper.event.player.PlayerInitialSpawnEvent(spawnPlayer, spawnPlayer.getLocation()); // Paper use our duplicate event
|
|
- this.cserver.getPluginManager().callEvent(ev);
|
|
+ //this.cserver.getPluginManager().callEvent(ev); // Folia - region threading - TODO WTF TO DO WITH THIS EVENT?
|
|
|
|
Location loc = ev.getSpawnLocation();
|
|
worldserver1 = ((CraftWorld) loc.getWorld()).getHandle();
|
|
@@ -266,6 +333,7 @@ public abstract class PlayerList {
|
|
|
|
player.loadGameTypes(nbttagcompound);
|
|
ServerGamePacketListenerImpl playerconnection = new ServerGamePacketListenerImpl(this.server, connection, player);
|
|
+ worldserver1.getCurrentWorldData().connections.add(connection); // Folia - region threading - only AFTER updating listener to game
|
|
GameRules gamerules = worldserver1.getGameRules();
|
|
boolean flag = gamerules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN);
|
|
boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO);
|
|
@@ -283,7 +351,7 @@ public abstract class PlayerList {
|
|
this.sendPlayerPermissionLevel(player);
|
|
player.getStats().markAllDirty();
|
|
player.getRecipeBook().sendInitialRecipeBook(player);
|
|
- this.updateEntireScoreboard(worldserver1.getScoreboard(), player);
|
|
+ if (false) this.updateEntireScoreboard(worldserver1.getScoreboard(), player); // Folia - region threading
|
|
this.server.invalidateStatus();
|
|
MutableComponent ichatmutablecomponent;
|
|
|
|
@@ -325,7 +393,7 @@ public abstract class PlayerList {
|
|
this.cserver.getPluginManager().callEvent(playerJoinEvent);
|
|
|
|
if (!player.connection.isAcceptingMessages()) {
|
|
- return;
|
|
+ //return; // Folia - region threading - must still allow the player to connect, as we must add to chunk map before handling disconnect
|
|
}
|
|
|
|
final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage();
|
|
@@ -340,8 +408,7 @@ public abstract class PlayerList {
|
|
ClientboundPlayerInfoUpdatePacket packet = ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player));
|
|
|
|
final List<ServerPlayer> onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1); // Paper - use single player info update packet
|
|
- for (int i = 0; i < this.players.size(); ++i) {
|
|
- ServerPlayer entityplayer1 = (ServerPlayer) this.players.get(i);
|
|
+ for (ServerPlayer entityplayer1 : this.players) { // Folia - region threadingv
|
|
|
|
if (entityplayer1.getBukkitEntity().canSee(bukkitPlayer)) {
|
|
entityplayer1.connection.send(packet);
|
|
@@ -458,7 +525,7 @@ public abstract class PlayerList {
|
|
// Paper start - Add to collideRule team if needed
|
|
final Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard();
|
|
final PlayerTeam collideRuleTeam = scoreboard.getPlayerTeam(this.collideRuleTeamName);
|
|
- if (this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) {
|
|
+ if (false && this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { // Folia - region threading
|
|
scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam);
|
|
}
|
|
// Paper end
|
|
@@ -549,7 +616,7 @@ public abstract class PlayerList {
|
|
|
|
protected void save(ServerPlayer player) {
|
|
if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit
|
|
- player.lastSave = MinecraftServer.currentTick; // Paper
|
|
+ player.lastSave = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading
|
|
this.playerIo.save(player);
|
|
ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit
|
|
|
|
@@ -589,7 +656,7 @@ public abstract class PlayerList {
|
|
// CraftBukkit end
|
|
|
|
// Paper start - Remove from collideRule team if needed
|
|
- if (this.collideRuleTeamName != null) {
|
|
+ if (false && this.collideRuleTeamName != null) { // Folia - region threading
|
|
final Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard();
|
|
final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName);
|
|
if (entityplayer.getTeam() == team && team != null) {
|
|
@@ -629,6 +696,7 @@ public abstract class PlayerList {
|
|
|
|
entityplayer.unRide();
|
|
worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER);
|
|
+ entityplayer.retireScheduler(); // Folia - region threading
|
|
entityplayer.getAdvancements().stopListening();
|
|
this.players.remove(entityplayer);
|
|
this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
|
|
@@ -647,8 +715,7 @@ public abstract class PlayerList {
|
|
// CraftBukkit start
|
|
// this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(entityplayer.getUUID())));
|
|
ClientboundPlayerInfoRemovePacket packet = new ClientboundPlayerInfoRemovePacket(List.of(entityplayer.getUUID()));
|
|
- for (int i = 0; i < this.players.size(); i++) {
|
|
- ServerPlayer entityplayer2 = (ServerPlayer) this.players.get(i);
|
|
+ for (ServerPlayer entityplayer2 : this.players) { // Folia - region threading
|
|
|
|
if (entityplayer2.getBukkitEntity().canSee(entityplayer.getBukkitEntity())) {
|
|
entityplayer2.connection.send(packet);
|
|
@@ -673,19 +740,13 @@ public abstract class PlayerList {
|
|
|
|
ServerPlayer entityplayer;
|
|
|
|
- for (int i = 0; i < this.players.size(); ++i) {
|
|
- entityplayer = (ServerPlayer) this.players.get(i);
|
|
- if (entityplayer.getUUID().equals(uuid) || (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && entityplayer.getGameProfile().getName().equalsIgnoreCase(gameprofile.getName()))) { // Paper - validate usernames
|
|
- list.add(entityplayer);
|
|
- }
|
|
- }
|
|
+ // Folia - region threading - rewrite login process - moved to pushPendingJoin
|
|
|
|
Iterator iterator = list.iterator();
|
|
|
|
while (iterator.hasNext()) {
|
|
entityplayer = (ServerPlayer) iterator.next();
|
|
- this.save(entityplayer); // CraftBukkit - Force the player's inventory to be saved
|
|
- entityplayer.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause
|
|
+ // Folia - moved to pushPendingJoin
|
|
}
|
|
|
|
// Instead of kicking then returning, we need to store the kick reason
|
|
@@ -724,7 +785,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
|
|
}
|
|
}
|
|
@@ -976,10 +1037,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<ServerPlayer>() {
|
|
+ target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), java.util.Arrays.stream(players).filter(new Predicate<ServerPlayer>() { // Folia - region threading
|
|
@Override
|
|
public boolean test(ServerPlayer input) {
|
|
return target.getBukkitEntity().canSee(input.getBukkitEntity());
|
|
@@ -1005,18 +1066,17 @@ public abstract class PlayerList {
|
|
|
|
// CraftBukkit start - add a world/entity limited version
|
|
public void broadcastAll(Packet packet, net.minecraft.world.entity.player.Player entityhuman) {
|
|
- for (int i = 0; i < this.players.size(); ++i) {
|
|
- ServerPlayer entityplayer = this.players.get(i);
|
|
+ for (ServerPlayer entityplayer : this.players) { // Folia - region threading
|
|
if (entityhuman != null && !entityplayer.getBukkitEntity().canSee(entityhuman.getBukkitEntity())) {
|
|
continue;
|
|
}
|
|
- ((ServerPlayer) this.players.get(i)).connection.send(packet);
|
|
+ entityplayer.connection.send(packet); // Folia - region threading
|
|
}
|
|
}
|
|
|
|
public void broadcastAll(Packet packet, Level world) {
|
|
- for (int i = 0; i < world.players().size(); ++i) {
|
|
- ((ServerPlayer) world.players().get(i)).connection.send(packet);
|
|
+ for (net.minecraft.world.entity.player.Player player : world.players()) { // Folia - region threading
|
|
+ ((ServerPlayer) player).connection.send(packet); // Folia - region threading
|
|
}
|
|
|
|
}
|
|
@@ -1060,8 +1120,7 @@ public abstract class PlayerList {
|
|
if (scoreboardteambase == null) {
|
|
this.broadcastSystemMessage(message, false);
|
|
} else {
|
|
- for (int i = 0; i < this.players.size(); ++i) {
|
|
- ServerPlayer entityplayer = (ServerPlayer) this.players.get(i);
|
|
+ for (ServerPlayer entityplayer : this.players) { // Folia - region threading
|
|
|
|
if (entityplayer.getTeam() != scoreboardteambase) {
|
|
entityplayer.sendSystemMessage(message);
|
|
@@ -1072,10 +1131,12 @@ public abstract class PlayerList {
|
|
}
|
|
|
|
public String[] getPlayerNamesArray() {
|
|
- String[] astring = new String[this.players.size()];
|
|
+ List<ServerPlayer> players = new java.util.ArrayList<>(this.players); // Folia start - region threading
|
|
+ String[] astring = new String[players.size()];
|
|
|
|
- for (int i = 0; i < this.players.size(); ++i) {
|
|
- astring[i] = ((ServerPlayer) this.players.get(i)).getGameProfile().getName();
|
|
+ for (int i = 0; i < players.size(); ++i) {
|
|
+ astring[i] = ((ServerPlayer) players.get(i)).getGameProfile().getName();
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
return astring;
|
|
@@ -1094,7 +1155,9 @@ public abstract class PlayerList {
|
|
ServerPlayer entityplayer = this.getPlayer(profile.getId());
|
|
|
|
if (entityplayer != null) {
|
|
+ entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading
|
|
this.sendPlayerPermissionLevel(entityplayer);
|
|
+ }, null, 1L); // Folia - region threading
|
|
}
|
|
|
|
}
|
|
@@ -1104,7 +1167,10 @@ public abstract class PlayerList {
|
|
ServerPlayer entityplayer = this.getPlayer(profile.getId());
|
|
|
|
if (entityplayer != null) {
|
|
+ entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading
|
|
this.sendPlayerPermissionLevel(entityplayer);
|
|
+ }, null, 1L); // Folia - region threading
|
|
+
|
|
}
|
|
|
|
}
|
|
@@ -1165,8 +1231,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<Level> worldKey, Packet<?> packet) {
|
|
- for (int i = 0; i < this.players.size(); ++i) {
|
|
- ServerPlayer entityplayer = (ServerPlayer) this.players.get(i);
|
|
+ for (ServerPlayer entityplayer : this.players) { // Folia - region threading
|
|
|
|
// CraftBukkit start - Test if player receiving packet can see the source of the packet
|
|
if (player != null && !entityplayer.getBukkitEntity().canSee(player.getBukkitEntity())) {
|
|
@@ -1196,9 +1261,12 @@ public abstract class PlayerList {
|
|
io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main
|
|
MinecraftTimings.savePlayers.startTiming(); // Paper
|
|
int numSaved = 0;
|
|
- long now = MinecraftServer.currentTick;
|
|
- for (int i = 0; i < this.players.size(); ++i) {
|
|
- ServerPlayer entityplayer = this.players.get(i);
|
|
+ long now = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading
|
|
+ for (ServerPlayer entityplayer : this.players) { // Folia start - region threading
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(entityplayer)) {
|
|
+ continue;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
if (interval == -1 || now - entityplayer.lastSave >= interval) {
|
|
this.save(entityplayer);
|
|
if (interval != -1 && ++numSaved <= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { break; }
|
|
@@ -1318,6 +1386,20 @@ public abstract class PlayerList {
|
|
}
|
|
|
|
public void removeAll(boolean isRestarting) {
|
|
+ // Folia start - region threading
|
|
+ // just send disconnect packet, don't modify state
|
|
+ for (ServerPlayer player : this.players) {
|
|
+ final Component shutdownMessage = PaperAdventure.asVanilla(this.server.server.shutdownMessage()); // Paper - Adventure
|
|
+ // CraftBukkit end
|
|
+
|
|
+ player.connection.send(new net.minecraft.network.protocol.game.ClientboundDisconnectPacket(shutdownMessage), net.minecraft.network.PacketSendListener.thenRun(() -> {
|
|
+ player.connection.connection.disconnect(shutdownMessage);
|
|
+ }));
|
|
+ }
|
|
+ if (true) {
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
// Paper end
|
|
// CraftBukkit start - disconnect safely
|
|
for (ServerPlayer player : this.players) {
|
|
@@ -1327,7 +1409,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<K, V extends StoredUserEntry<K>> {
|
|
}
|
|
|
|
public void save() throws IOException {
|
|
+ synchronized (this) { // Folia - region threading
|
|
this.removeExpired(); // Paper - remove expired values before saving
|
|
JsonArray jsonarray = new JsonArray();
|
|
Stream<JsonObject> stream = this.map.values().stream().map((jsonlistentry) -> { // CraftBukkit - decompile error
|
|
@@ -178,10 +179,12 @@ public abstract class StoredUserList<K, V extends StoredUserEntry<K>> {
|
|
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<K, V extends StoredUserEntry<K>> {
|
|
}
|
|
|
|
}
|
|
+ } // 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<T> extends AbstractSet<T> {
|
|
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<T> extends AbstractSet<T> {
|
|
}
|
|
}
|
|
// Paper end - rewrite chunk system
|
|
+ // Folia start - region threading
|
|
+ public int binarySearch(final T search) {
|
|
+ return this.findIndex(search);
|
|
+ }
|
|
+
|
|
+ public int insertAndGetIdx(final T value) {
|
|
+ final int idx = this.findIndex(value);
|
|
+ if (idx >= 0) {
|
|
+ // exists already
|
|
+ return idx;
|
|
+ }
|
|
+
|
|
+ this.addInternal(value, getInsertionPosition(idx));
|
|
+ return idx;
|
|
+ }
|
|
+
|
|
+ public T removeFirst() {
|
|
+ final T ret = this.contents[0];
|
|
+
|
|
+ this.removeInternal(0);
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ public T removeLast() {
|
|
+ final int index = --this.size;
|
|
+
|
|
+ final T ret = this.contents[index];
|
|
+
|
|
+ this.contents[index] = null;
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
|
|
@Override
|
|
public boolean remove(Object object) {
|
|
diff --git a/src/main/java/net/minecraft/util/SpawnUtil.java b/src/main/java/net/minecraft/util/SpawnUtil.java
|
|
index 83ef8cb27db685cceb5c2b7c9674e17b93ba081c..2d87c16420a97b9142d4ea76ceb6013deed22a1f 100644
|
|
--- a/src/main/java/net/minecraft/util/SpawnUtil.java
|
|
+++ b/src/main/java/net/minecraft/util/SpawnUtil.java
|
|
@@ -59,7 +59,7 @@ public class SpawnUtil {
|
|
return Optional.of(t0);
|
|
}
|
|
|
|
- t0.discard();
|
|
+ //t0.discard(); // Folia - region threading
|
|
}
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/damagesource/CombatTracker.java b/src/main/java/net/minecraft/world/damagesource/CombatTracker.java
|
|
index a454aa44acddd668c96a17da44ccccb1bbac4546..0e6112a7dbeb17b088e3585b0d284ec36e48ca80 100644
|
|
--- a/src/main/java/net/minecraft/world/damagesource/CombatTracker.java
|
|
+++ b/src/main/java/net/minecraft/world/damagesource/CombatTracker.java
|
|
@@ -40,7 +40,7 @@ public class CombatTracker {
|
|
public void prepareForDamage() {
|
|
this.resetPreparedStatus();
|
|
Optional<BlockPos> optional = this.mob.getLastClimbablePos();
|
|
- if (optional.isPresent()) {
|
|
+ if (optional.isPresent() && io.papermc.paper.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.mob.level, optional.get())) { // Folia - region threading - may sync load the block, which would crash
|
|
BlockState blockState = this.mob.level.getBlockState(optional.get());
|
|
if (!blockState.is(Blocks.LADDER) && !blockState.is(BlockTags.TRAPDOORS)) {
|
|
if (blockState.is(Blocks.VINE)) {
|
|
@@ -92,6 +92,7 @@ public class CombatTracker {
|
|
Component component = combatEntry2.getAttackerName();
|
|
DamageSource damageSource = combatEntry2.getSource();
|
|
Entity entity = damageSource.getEntity();
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(entity)) entity = null; // Folia - region threading - not safe to access other entity data
|
|
DeathMessageType deathMessageType = damageSource.type().deathMessageType();
|
|
Component component4;
|
|
if (combatEntry != null && deathMessageType == DeathMessageType.FALL_VARIANTS) {
|
|
@@ -158,6 +159,7 @@ public class CombatTracker {
|
|
|
|
for(CombatEntry combatEntry : this.entries) {
|
|
Entity var8 = combatEntry.getSource().getEntity();
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(var8)) { continue; } // Folia - region threading - skip entities we do not own
|
|
if (var8 instanceof Player player2) {
|
|
if (player == null || combatEntry.getDamage() > g) {
|
|
g = combatEntry.getDamage();
|
|
diff --git a/src/main/java/net/minecraft/world/damagesource/DamageSource.java b/src/main/java/net/minecraft/world/damagesource/DamageSource.java
|
|
index 93a1e990b0a6caae4143c2f9d09bfb368fa1d6db..ad3166481dd37f4b5380f8bf28653bb4fd5c4933 100644
|
|
--- a/src/main/java/net/minecraft/world/damagesource/DamageSource.java
|
|
+++ b/src/main/java/net/minecraft/world/damagesource/DamageSource.java
|
|
@@ -112,7 +112,7 @@ public class DamageSource {
|
|
Entity entity = this.causingEntity;
|
|
ItemStack itemstack;
|
|
|
|
- if (entity instanceof LivingEntity) {
|
|
+ if (entity instanceof LivingEntity livingEntity && io.papermc.paper.util.TickThread.isTickThreadFor(livingEntity)) { // Folia - region threading
|
|
LivingEntity entityliving2 = (LivingEntity) entity;
|
|
|
|
itemstack = entityliving2.getMainHandItem();
|
|
diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
|
|
index 9a1e8589e6b371869b2199650172d61ae186c907..29df951426d344885c1bded236f03c059c7878a3 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/Entity.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
|
|
@@ -165,7 +165,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
|
|
// Paper start
|
|
public static RandomSource SHARED_RANDOM = new RandomRandomSource();
|
|
- private static final class RandomRandomSource extends java.util.Random implements net.minecraft.world.level.levelgen.BitRandomSource {
|
|
+ public static final class RandomRandomSource extends java.util.Random implements net.minecraft.world.level.levelgen.BitRandomSource { // Folia - region threading
|
|
private boolean locked = false;
|
|
|
|
@Override
|
|
@@ -239,17 +239,29 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
|
|
public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData; // Paper
|
|
public boolean collisionLoadChunks = false; // Paper
|
|
- private CraftEntity bukkitEntity;
|
|
+ private volatile CraftEntity bukkitEntity; // Folia - region threading
|
|
|
|
public @org.jetbrains.annotations.Nullable net.minecraft.server.level.ChunkMap.TrackedEntity tracker; // Paper
|
|
public @Nullable Throwable addedToWorldStack; // Paper - entity debug
|
|
public CraftEntity getBukkitEntity() {
|
|
if (this.bukkitEntity == null) {
|
|
- this.bukkitEntity = CraftEntity.getEntity(this.level.getCraftServer(), this);
|
|
+ // Folia start - region threading
|
|
+ synchronized (this) {
|
|
+ if (this.bukkitEntity == null) {
|
|
+ return this.bukkitEntity = CraftEntity.getEntity(this.level.getCraftServer(), this);
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
}
|
|
return this.bukkitEntity;
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ public CraftEntity getBukkitEntityRaw() {
|
|
+ return this.bukkitEntity;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Override
|
|
public CommandSender getBukkitSender(CommandSourceStack wrapper) {
|
|
return this.getBukkitEntity();
|
|
@@ -489,28 +501,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
this.isLegacyTrackingEntity = isLegacyTrackingEntity;
|
|
}
|
|
|
|
- public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> 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<Entity> passengers = this.getIndirectPassengers();
|
|
- net.minecraft.server.level.ChunkMap chunkMap = ((ServerLevel)this.level).getChunkSource().chunkMap;
|
|
- org.spigotmc.TrackingRange.TrackingRangeType type = this.trackingRangeType;
|
|
- int range = chunkMap.getEntityTrackerRange(type.ordinal());
|
|
-
|
|
- for (Entity passenger : passengers) {
|
|
- org.spigotmc.TrackingRange.TrackingRangeType passengerType = passenger.trackingRangeType;
|
|
- int passengerRange = chunkMap.getEntityTrackerRange(passengerType.ordinal());
|
|
- if (passengerRange > range) {
|
|
- type = passengerType;
|
|
- range = passengerRange;
|
|
- }
|
|
- }
|
|
-
|
|
- return chunkMap.playerEntityTrackerTrackMaps[type.ordinal()].getObjectsInRange(MCUtil.getCoordinateKey(this));
|
|
- }
|
|
+ // Folia - region threading
|
|
// Paper end - optimise entity tracking
|
|
// Paper start - make end portalling safe
|
|
public BlockPos portalBlock;
|
|
@@ -542,6 +533,25 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
this.teleportTo(worldserver, null);
|
|
}
|
|
// Paper end - make end portalling safe
|
|
+ // Folia start
|
|
+ private static final java.util.concurrent.ConcurrentHashMap<Class<? extends Entity>, 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<? extends Entity> c) -> {
|
|
+ return CLASS_ID_GENERATOR.getAndIncrement();
|
|
+ });
|
|
+ private static final java.util.concurrent.atomic.AtomicLong REFERENCE_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong();
|
|
+ public final long referenceId = REFERENCE_ID_GENERATOR.getAndIncrement();
|
|
+ // Folia end
|
|
+ // Folia start - region ticking
|
|
+ public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) {
|
|
+ if (this.activatedTick != Integer.MIN_VALUE) {
|
|
+ this.activatedTick += fromTickOffset;
|
|
+ }
|
|
+ if (this.activatedImmunityTick != Integer.MIN_VALUE) {
|
|
+ this.activatedImmunityTick += fromTickOffset;
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region ticking
|
|
|
|
public Entity(EntityType<?> type, Level world) {
|
|
this.id = Entity.ENTITY_COUNTER.incrementAndGet();
|
|
@@ -657,6 +667,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
}
|
|
|
|
public final void discard() {
|
|
+ // Folia start - region threading
|
|
+ if (this.isRemoved()) {
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
this.remove(Entity.RemovalReason.DISCARDED);
|
|
}
|
|
|
|
@@ -781,6 +796,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
|
|
// CraftBukkit start
|
|
public void postTick() {
|
|
+ // Folia start - region threading
|
|
+ // moved to doPortalLogic
|
|
+ if (true) {
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
// No clean way to break out of ticking once the entity has been copied to a new world, so instead we move the portalling later in the tick cycle
|
|
if (!(this instanceof ServerPlayer) && this.isAlive()) { // Paper - don't attempt to teleport dead entities
|
|
this.handleNetherPortal();
|
|
@@ -803,7 +824,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
this.walkDistO = this.walkDist;
|
|
this.xRotO = this.getXRot();
|
|
this.yRotO = this.getYRot();
|
|
- if (this instanceof ServerPlayer) this.handleNetherPortal(); // CraftBukkit - // Moved up to postTick
|
|
+ //if (this instanceof ServerPlayer) this.handleNetherPortal(); // CraftBukkit - // Moved up to postTick // Folia - region threading - ONLY allow in postTick()
|
|
if (this.canSpawnSprintParticle()) {
|
|
this.spawnSprintParticle();
|
|
}
|
|
@@ -904,11 +925,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
// This will be called every single tick the entity is in lava, so don't throw an event
|
|
this.setSecondsOnFire(15, false);
|
|
}
|
|
- CraftEventFactory.blockDamage = (this.lastLavaContact) == null ? null : org.bukkit.craftbukkit.block.CraftBlock.at(level, lastLavaContact);
|
|
+ CraftEventFactory.blockDamageRT.set((this.lastLavaContact) == null ? null : org.bukkit.craftbukkit.block.CraftBlock.at(level, lastLavaContact)); // Folia - region threading
|
|
if (this.hurt(this.damageSources().lava(), 4.0F)) {
|
|
this.playSound(SoundEvents.GENERIC_BURN, 0.4F, 2.0F + this.random.nextFloat() * 0.4F);
|
|
}
|
|
- CraftEventFactory.blockDamage = null;
|
|
+ CraftEventFactory.blockDamageRT.set(null); // Folia - region threading
|
|
// CraftBukkit end - we also don't throw an event unless the object in lava is living, to save on some event calls
|
|
|
|
}
|
|
@@ -1016,8 +1037,8 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
} else {
|
|
this.wasOnFire = this.isOnFire();
|
|
if (movementType == MoverType.PISTON) {
|
|
- this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20); // Paper
|
|
- this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20); // Paper
|
|
+ this.activatedTick = Math.max(this.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper
|
|
+ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper
|
|
movement = this.limitPistonMovement(movement);
|
|
if (movement.equals(Vec3.ZERO)) {
|
|
return;
|
|
@@ -3090,6 +3111,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
|
|
@Nullable
|
|
public Team getTeam() {
|
|
+ // Folia start - region threading
|
|
+ if (true) {
|
|
+ return null;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
if (!this.level.paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof Player)) { return null; } // Paper
|
|
return this.level.getScoreboard().getPlayersTeam(this.getScoreboardName());
|
|
}
|
|
@@ -3205,9 +3231,9 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
if (this.fireImmune()) {
|
|
return;
|
|
}
|
|
- CraftEventFactory.entityDamage = lightning;
|
|
+ CraftEventFactory.entityDamageRT.set(lightning); // Folia - region threading
|
|
if (!this.hurt(this.damageSources().lightningBolt(), 5.0F)) {
|
|
- CraftEventFactory.entityDamage = null;
|
|
+ CraftEventFactory.entityDamageRT.set(null); // Folia - region threading
|
|
return;
|
|
}
|
|
// CraftBukkit end
|
|
@@ -3380,6 +3406,751 @@ 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<EntityTreeNode> getFullTree() {
|
|
+ List<EntityTreeNode> 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<EntityTreeNode> queue = new java.util.ArrayDeque<>();
|
|
+ queue.add(this);
|
|
+
|
|
+ EntityTreeNode curr;
|
|
+ while ((curr = queue.pollFirst()) != null) {
|
|
+ EntityTreeNode[] passengers = curr.passengers;
|
|
+ if (passengers == null) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ List<Entity> 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<EntityTreeNode> 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<EntityTreeNode> queue = new java.util.ArrayDeque<>();
|
|
+ queue.add(root);
|
|
+ EntityTreeNode curr;
|
|
+ while ((curr = queue.pollFirst()) != null) {
|
|
+ Entity vehicle = curr.root;
|
|
+ List<Entity> 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<EntityTreeNode> queue = new java.util.ArrayDeque<>();
|
|
+ queue.add(root);
|
|
+ EntityTreeNode curr;
|
|
+ while ((curr = queue.pollFirst()) != null) {
|
|
+ Entity vehicle = curr.root;
|
|
+ List<Entity> 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<Entity> teleportComplete) {
|
|
+ Vec3 pos = this.position();
|
|
+ ChunkPos posChunk = new ChunkPos(
|
|
+ io.papermc.paper.util.CoordinateUtils.getChunkX(pos),
|
|
+ io.papermc.paper.util.CoordinateUtils.getChunkZ(pos)
|
|
+ );
|
|
+
|
|
+ // ensure the region is always ticking in case of a shutdown
|
|
+ // otherwise, the shutdown will not be able to complete the shutdown as it requires a ticking region
|
|
+ Long teleportHoldId = Long.valueOf(TELEPORT_HOLD_TICKET_GEN.getAndIncrement());
|
|
+ originWorld.chunkSource.addTicketAtLevel(
|
|
+ TicketType.TELEPORT_HOLD_TICKET, posChunk,
|
|
+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL,
|
|
+ teleportHoldId
|
|
+ );
|
|
+ final ServerLevel.PendingTeleport pendingTeleport = new ServerLevel.PendingTeleport(passengerTree, pos);
|
|
+ destination.pushPendingTeleport(pendingTeleport);
|
|
+
|
|
+ Runnable scheduleEntityJoin = () -> {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ destination,
|
|
+ io.papermc.paper.util.CoordinateUtils.getChunkX(pos), io.papermc.paper.util.CoordinateUtils.getChunkZ(pos),
|
|
+ () -> {
|
|
+ if (!destination.removePendingTeleport(pendingTeleport)) {
|
|
+ // shutdown logic placed the entity already, and we are shutting down - do nothing to ensure
|
|
+ // we do not produce any errors here
|
|
+ return;
|
|
+ }
|
|
+ originWorld.chunkSource.removeTicketAtLevel(
|
|
+ TicketType.TELEPORT_HOLD_TICKET, posChunk,
|
|
+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL,
|
|
+ teleportHoldId
|
|
+ );
|
|
+ List<EntityTreeNode> 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) {
|
|
+ this.removeAfterChangingDimensions(); // remove before so that any CBEntity#getHandle call affects this entity before copying
|
|
+
|
|
+ Entity copy = this.getType().create(destination);
|
|
+ copy.restoreFrom(this);
|
|
+ copy.transform(pos, yaw, pitch, speedDirectionUpdate);
|
|
+
|
|
+ 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<Entity> teleportComplete) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot teleport entity async");
|
|
+
|
|
+ if (!ServerLevel.isInSpawnableBounds(new BlockPos(io.papermc.paper.util.CoordinateUtils.getBlockX(pos), io.papermc.paper.util.CoordinateUtils.getBlockY(pos), io.papermc.paper.util.CoordinateUtils.getBlockZ(pos)))) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if ((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<EntityTreeNode> fullPassengerTree = passengerTree.getFullTree();
|
|
+ ServerLevel originWorld = (ServerLevel)this.level;
|
|
+
|
|
+ for (EntityTreeNode node : fullPassengerTree) {
|
|
+ node.root.preChangeDimension();
|
|
+ }
|
|
+
|
|
+ for (EntityTreeNode node : fullPassengerTree) {
|
|
+ node.root = node.root.transformForAsyncTeleport(destination, pos, yaw, pitch, speedDirectionUpdate);
|
|
+ }
|
|
+
|
|
+ passengerTree.root.placeInAsync(originWorld, destination, teleportFlags, passengerTree, teleportComplete);
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ public void preChangeDimension() {
|
|
+
|
|
+ }
|
|
+
|
|
+ public void postChangeDimension() {
|
|
+
|
|
+ }
|
|
+
|
|
+ protected static enum PortalType {
|
|
+ NETHER, END;
|
|
+ }
|
|
+
|
|
+ public boolean doPortalLogic() {
|
|
+ if (this.tryNetherPortal()) {
|
|
+ return true;
|
|
+ }
|
|
+ if (this.tryEndPortal()) {
|
|
+ return true;
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ protected boolean tryEndPortal() {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async");
|
|
+ BlockPos pos = this.portalBlock;
|
|
+ ServerLevel world = this.portalWorld;
|
|
+ this.portalBlock = null;
|
|
+ this.portalWorld = null;
|
|
+
|
|
+ if (pos == null || world == null || world != this.level) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (this.isPassenger() || this.isVehicle() || !this.canChangeDimensions() || this.isRemoved() || !this.valid || !this.isAlive()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ return this.endPortalLogicAsync();
|
|
+ }
|
|
+
|
|
+ protected boolean tryNetherPortal() {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async");
|
|
+
|
|
+ int portalWaitTime = this.getPortalWaitTime();
|
|
+
|
|
+ if (this.isInsidePortal) {
|
|
+ // if we are in a nether portal still, this flag will be set next tick.
|
|
+ this.isInsidePortal = false;
|
|
+ if (this.portalTime++ >= portalWaitTime) {
|
|
+ this.portalTime = portalWaitTime;
|
|
+ if (this.netherPortalLogicAsync()) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ } else {
|
|
+ // rapidly decrease portal time
|
|
+ this.portalTime = Math.max(0, this.portalTime - 4);
|
|
+ }
|
|
+
|
|
+ this.processPortalCooldown();
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ public boolean endPortalLogicAsync() {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async");
|
|
+
|
|
+ ServerLevel destination = this.getServer().getLevel(this.getLevel().getTypeKey() == LevelStem.END ? Level.OVERWORLD : Level.END);
|
|
+ if (destination == null) {
|
|
+ // wat
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ return this.portalToAsync(destination, false, PortalType.END, null);
|
|
+ }
|
|
+
|
|
+ public boolean netherPortalLogicAsync() {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async");
|
|
+
|
|
+ ServerLevel destination = this.getServer().getLevel(this.getLevel().getTypeKey() == LevelStem.NETHER ? Level.OVERWORLD : Level.NETHER);
|
|
+ if (destination == null) {
|
|
+ // wat
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ return this.portalToAsync(destination, false, PortalType.NETHER, null);
|
|
+ }
|
|
+
|
|
+ private static final java.util.concurrent.atomic.AtomicLong CREATE_PORTAL_DOUBLE_CHECK = new java.util.concurrent.atomic.AtomicLong();
|
|
+ private static final java.util.concurrent.atomic.AtomicLong TELEPORT_HOLD_TICKET_GEN = new java.util.concurrent.atomic.AtomicLong();
|
|
+
|
|
+ // To simplify portal logic, in region threading both players
|
|
+ // and non-player entities will create portals. By guaranteeing
|
|
+ // that the teleportation can take place, we can simply
|
|
+ // remove the entity, find/create the portal, and place async.
|
|
+ // If we have to worry about whether the entity may not teleport,
|
|
+ // we need to first search, then report back, ...
|
|
+ protected void findOrCreatePortalAsync(ServerLevel origin, ServerLevel destination, PortalType type,
|
|
+ ca.spottedleaf.concurrentutil.completable.Completable<PortalInfo> portalInfoCompletable) {
|
|
+ switch (type) {
|
|
+ // end portal logic is quite simple, the spawn in the end is fixed and when returning to the overworld
|
|
+ // we just select the spawn position
|
|
+ case END: {
|
|
+ if (destination.getTypeKey() == LevelStem.END) {
|
|
+ BlockPos targetPos = ServerLevel.END_SPAWN_POINT;
|
|
+ // need to load chunks so we can create the platform
|
|
+ destination.loadChunksAsync(
|
|
+ targetPos, 16, // load 16 blocks to be safe from block physics
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH,
|
|
+ (chunks) -> {
|
|
+ ServerLevel.makeObsidianPlatform(destination, null, targetPos);
|
|
+
|
|
+ // the portal obsidian is placed at targetPos.y - 2, so if we want to place the entity
|
|
+ // on the obsidian, we need to spawn at targetPos.y - 1
|
|
+ portalInfoCompletable.complete(
|
|
+ new PortalInfo(Vec3.atBottomCenterOf(targetPos.below()), Vec3.ZERO, 90.0f, 0.0f, destination, null)
|
|
+ );
|
|
+ }
|
|
+ );
|
|
+ } else {
|
|
+ BlockPos spawnPos = destination.getSharedSpawnPos();
|
|
+ // need to load chunk for heightmap
|
|
+ destination.loadChunksAsync(
|
|
+ spawnPos, 0,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH,
|
|
+ (chunks) -> {
|
|
+ BlockPos adjustedSpawn = destination.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, spawnPos);
|
|
+
|
|
+ // done
|
|
+ portalInfoCompletable.complete(
|
|
+ new PortalInfo(Vec3.atBottomCenterOf(adjustedSpawn), Vec3.ZERO, 90.0f, 0.0f, destination, null)
|
|
+ );
|
|
+ }
|
|
+ );
|
|
+ }
|
|
+
|
|
+ break;
|
|
+ }
|
|
+ // for the nether logic, we need to first load the chunks in radius to empty (so that POI is created)
|
|
+ // then we can search for an existing portal using the POI routines
|
|
+ // if we don't find a portal, then we bring the chunks in the create radius to full and
|
|
+ // create it
|
|
+ case NETHER: {
|
|
+ // hoisted from the create fallback, so that we can avoid the sync load later if we need it
|
|
+ BlockState originalPortalBlock = this.portalEntrancePos == null ? null : origin.getBlockStateIfLoaded(this.portalEntrancePos);
|
|
+ Direction.Axis originalPortalDirection = originalPortalBlock == null ? Direction.Axis.X :
|
|
+ originalPortalBlock.getOptionalValue(net.minecraft.world.level.block.NetherPortalBlock.AXIS).orElse(Direction.Axis.X);
|
|
+ BlockUtil.FoundRectangle originalPortalRectangle =
|
|
+ originalPortalBlock == null || !originalPortalBlock.hasProperty(BlockStateProperties.HORIZONTAL_AXIS)
|
|
+ ? null
|
|
+ : BlockUtil.getLargestRectangleAround(
|
|
+ this.portalEntrancePos, originalPortalDirection, 21, Direction.Axis.Y, 21,
|
|
+ (blockpos) -> {
|
|
+ return origin.getBlockStateFromEmptyChunkIfLoaded(blockpos) == originalPortalBlock;
|
|
+ }
|
|
+ );
|
|
+
|
|
+ boolean destinationIsNether = destination.getTypeKey() == LevelStem.NETHER;
|
|
+
|
|
+ int portalSearchRadius = origin.paperConfig().environment.portalSearchVanillaDimensionScaling && destinationIsNether ?
|
|
+ (int)(destination.paperConfig().environment.portalSearchRadius / destination.dimensionType().coordinateScale()) :
|
|
+ destination.paperConfig().environment.portalSearchRadius;
|
|
+ int portalCreateRadius = destination.paperConfig().environment.portalCreateRadius;
|
|
+
|
|
+ WorldBorder destinationBorder = destination.getWorldBorder();
|
|
+ double dimensionScale = DimensionType.getTeleportationScale(origin.dimensionType(), destination.dimensionType());
|
|
+ BlockPos targetPos = destination.getWorldBorder().clampToBounds(this.getX() * dimensionScale, this.getY(), this.getZ() * dimensionScale);
|
|
+
|
|
+ ca.spottedleaf.concurrentutil.completable.Completable<BlockUtil.FoundRectangle> portalFound
|
|
+ = new ca.spottedleaf.concurrentutil.completable.Completable<>();
|
|
+
|
|
+ // post portal find/create logic
|
|
+ portalFound.addWaiter(
|
|
+ (BlockUtil.FoundRectangle portal, Throwable thr) -> {
|
|
+ // no portal could be created
|
|
+ if (portal == null) {
|
|
+ portalInfoCompletable.complete(
|
|
+ new PortalInfo(Vec3.atCenterOf(targetPos), Vec3.ZERO, 90.0f, 0.0f, destination, null)
|
|
+ );
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ Vec3 relativePos = originalPortalRectangle == null ?
|
|
+ new Vec3(0.5, 0.0, 0.0) :
|
|
+ Entity.this.getRelativePortalPosition(originalPortalDirection, originalPortalRectangle);
|
|
+
|
|
+ portalInfoCompletable.complete(
|
|
+ PortalShape.createPortalInfo(
|
|
+ destination, portal, originalPortalDirection, relativePos,
|
|
+ Entity.this, Entity.this.getDeltaMovement(), Entity.this.getYRot(), Entity.this.getXRot(), null
|
|
+ )
|
|
+ );
|
|
+ }
|
|
+ );
|
|
+
|
|
+ // kick off search for existing portal or creation
|
|
+ destination.loadChunksAsync(
|
|
+ // add 32 so that the final search for a portal frame doesn't load any chunks
|
|
+ targetPos, portalSearchRadius + 32,
|
|
+ net.minecraft.world.level.chunk.ChunkStatus.EMPTY,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH,
|
|
+ (chunks) -> {
|
|
+ BlockUtil.FoundRectangle portal =
|
|
+ destination.getPortalForcer().findPortalAround(targetPos, destinationBorder, portalSearchRadius).orElse(null);
|
|
+ if (portal != null) {
|
|
+ portalFound.complete(portal);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // add tickets so that we can re-search for a portal once the chunks are loaded
|
|
+ Long ticketId = Long.valueOf(CREATE_PORTAL_DOUBLE_CHECK.getAndIncrement());
|
|
+ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunks) {
|
|
+ destination.chunkSource.addTicketAtLevel(
|
|
+ TicketType.NETHER_PORTAL_DOUBLE_CHECK, chunk.getPos(),
|
|
+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL,
|
|
+ ticketId
|
|
+ );
|
|
+ }
|
|
+
|
|
+ // no portal found - create one
|
|
+ destination.loadChunksAsync(
|
|
+ targetPos, portalCreateRadius + 32,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH,
|
|
+ (chunks2) -> {
|
|
+ // don't need the tickets anymore
|
|
+ // note: we expect removeTicketsAtLevel to add an unknown ticket for us automatically
|
|
+ // if the ticket level were to decrease
|
|
+ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunks) {
|
|
+ destination.chunkSource.removeTicketAtLevel(
|
|
+ TicketType.NETHER_PORTAL_DOUBLE_CHECK, chunk.getPos(),
|
|
+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL,
|
|
+ ticketId
|
|
+ );
|
|
+ }
|
|
+
|
|
+ // when two entities portal at the same time, it is possible that both entities reach this
|
|
+ // part of the code - and create a double portal
|
|
+ // to fix this, we just issue another search to try and see if another entity created
|
|
+ // a portal nearby
|
|
+ BlockUtil.FoundRectangle existingTryAgain =
|
|
+ destination.getPortalForcer().findPortalAround(targetPos, destinationBorder, portalSearchRadius).orElse(null);
|
|
+ if (existingTryAgain != null) {
|
|
+ portalFound.complete(existingTryAgain);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // we do not have the correct entity reference here
|
|
+ BlockUtil.FoundRectangle createdPortal =
|
|
+ destination.getPortalForcer().createPortal(targetPos, originalPortalDirection, null, portalCreateRadius).orElse(null);
|
|
+ // if it wasn't created, passing null is expected here
|
|
+ portalFound.complete(createdPortal);
|
|
+ }
|
|
+ );
|
|
+ }
|
|
+ );
|
|
+ break;
|
|
+ }
|
|
+ default: {
|
|
+ throw new IllegalStateException("Unknown portal type " + type);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public boolean canPortalAsync(boolean considerPassengers) {
|
|
+ return this.canPortalAsync(considerPassengers, false);
|
|
+ }
|
|
+
|
|
+ protected boolean canPortalAsync(boolean considerPassengers, boolean skipPassengerCheck) {
|
|
+ if (considerPassengers) {
|
|
+ if (!skipPassengerCheck && this.isPassenger()) {
|
|
+ return false;
|
|
+ }
|
|
+ } else {
|
|
+ if (this.isVehicle() || (!skipPassengerCheck && this.isPassenger())) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ this.getBukkitEntity(); // force bukkit entity to be created before TPing
|
|
+ if (!this.canTeleportAsync() || !this.canChangeDimensions() || this.isOnPortalCooldown()) {
|
|
+ return false;
|
|
+ }
|
|
+ if (considerPassengers) {
|
|
+ for (Entity entity : this.passengers) {
|
|
+ if (!entity.canPortalAsync(true, true)) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) {
|
|
+
|
|
+ }
|
|
+
|
|
+ protected boolean portalToAsync(ServerLevel destination, boolean takePassengers,
|
|
+ PortalType type, java.util.function.Consumer<Entity> teleportComplete) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot portal entity async");
|
|
+ if (!this.canPortalAsync(takePassengers)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ Vec3 initialPosition = this.position();
|
|
+ ChunkPos initialPositionChunk = new ChunkPos(
|
|
+ io.papermc.paper.util.CoordinateUtils.getChunkX(initialPosition),
|
|
+ io.papermc.paper.util.CoordinateUtils.getChunkZ(initialPosition)
|
|
+ );
|
|
+
|
|
+ // first, remove entity/passengers from world
|
|
+ EntityTreeNode passengerTree = this.detachPassengers();
|
|
+ List<EntityTreeNode> fullPassengerTree = passengerTree.getFullTree();
|
|
+ ServerLevel originWorld = (ServerLevel)this.level;
|
|
+
|
|
+ for (EntityTreeNode node : fullPassengerTree) {
|
|
+ node.root.preChangeDimension();
|
|
+ node.root.prePortalLogic(originWorld, destination, type);
|
|
+ }
|
|
+
|
|
+ for (EntityTreeNode node : fullPassengerTree) {
|
|
+ // we will update pos/rot/speed later
|
|
+ node.root = node.root.transformForAsyncTeleport(destination, null, null, null, null);
|
|
+ // set portal cooldown
|
|
+ node.root.setPortalCooldown();
|
|
+ }
|
|
+
|
|
+ // ensure the region is always ticking in case of a shutdown
|
|
+ // otherwise, the shutdown will not be able to complete the shutdown as it requires a ticking region
|
|
+ Long teleportHoldId = Long.valueOf(TELEPORT_HOLD_TICKET_GEN.getAndIncrement());
|
|
+ originWorld.chunkSource.addTicketAtLevel(
|
|
+ TicketType.TELEPORT_HOLD_TICKET, initialPositionChunk,
|
|
+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL,
|
|
+ teleportHoldId
|
|
+ );
|
|
+
|
|
+ ServerLevel.PendingTeleport beforeFindDestination = new ServerLevel.PendingTeleport(passengerTree, initialPosition);
|
|
+ originWorld.pushPendingTeleport(beforeFindDestination);
|
|
+
|
|
+ ca.spottedleaf.concurrentutil.completable.Completable<PortalInfo> portalInfoCompletable
|
|
+ = new ca.spottedleaf.concurrentutil.completable.Completable<>();
|
|
+
|
|
+ portalInfoCompletable.addWaiter((PortalInfo info, Throwable throwable) -> {
|
|
+ if (!originWorld.removePendingTeleport(beforeFindDestination)) {
|
|
+ // the shutdown thread has placed us back into the origin world at the original position
|
|
+ // we just have to abandon this teleport to prevent duplication
|
|
+ return;
|
|
+ }
|
|
+ originWorld.chunkSource.removeTicketAtLevel(
|
|
+ TicketType.TELEPORT_HOLD_TICKET, initialPositionChunk,
|
|
+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL,
|
|
+ teleportHoldId
|
|
+ );
|
|
+ // adjust passenger tree to final pos
|
|
+ for (EntityTreeNode node : fullPassengerTree) {
|
|
+ node.root.transform(info.pos, Float.valueOf(info.yRot), Float.valueOf(info.xRot), info.speed);
|
|
+ }
|
|
+
|
|
+ // place
|
|
+ passengerTree.root.placeInAsync(
|
|
+ originWorld, destination, Entity.TELEPORT_FLAG_LOAD_CHUNK | (takePassengers ? Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS : 0L),
|
|
+ passengerTree, teleportComplete
|
|
+ );
|
|
+ });
|
|
+
|
|
+
|
|
+ passengerTree.root.findOrCreatePortalAsync(originWorld, destination, type, portalInfoCompletable);
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Nullable
|
|
public Entity changeDimension(ServerLevel destination) {
|
|
// CraftBukkit start
|
|
@@ -3904,17 +4675,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
|
|
|
|
@@ -4405,7 +5172,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
|
|
@@ -4426,7 +5194,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
|
|
@@ -4513,6 +5281,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
return this.removalReason != null;
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ public final boolean hasNullCallback() {
|
|
+ return this.levelCallback == EntityInLevelCallback.NULL;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Nullable
|
|
public Entity.RemovalReason getRemovalReason() {
|
|
return this.removalReason;
|
|
@@ -4537,7 +5311,23 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
|
|
if (reason != RemovalReason.UNLOADED_TO_CHUNK) this.getPassengers().forEach(Entity::stopRiding); // Paper - chunk system - don't adjust passenger state when unloading, it's just not safe (and messes with our logic in entity chunk unload)
|
|
this.levelCallback.onRemove(reason);
|
|
+ // Folia start - region threading
|
|
+ if (!(this instanceof ServerPlayer) && reason != RemovalReason.CHANGED_DIMENSION) {
|
|
+ // Players need to be special cased, because they are regularly removed from the world
|
|
+ this.retireScheduler();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+ }
|
|
+
|
|
+ // Folia start - region threading
|
|
+ /**
|
|
+ * Invoked only when the entity is truly removed from the server, never to be added to any world.
|
|
+ */
|
|
+ public final void retireScheduler() {
|
|
+ // we need to force create the bukkit entity so that the scheduler can be retired...
|
|
+ this.getBukkitEntity().taskScheduler.retire();
|
|
}
|
|
+ // Folia end - region threading
|
|
|
|
public void unsetRemoved() {
|
|
this.removalReason = null;
|
|
diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
|
|
index dcfb71b5a53df789e366fea2080921d677549a2e..485e5c3f1348f9e79f48f6ec3fad34fa58dcfe73 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
|
|
@@ -464,7 +464,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
|
|
|
|
if (this.isDeadOrDying() && this.level.shouldTickDeath(this)) {
|
|
this.tickDeath();
|
|
- }
|
|
+ } else { this.broadcastedDeath = false; } // Folia - region threading
|
|
|
|
if (this.lastHurtByPlayerTime > 0) {
|
|
--this.lastHurtByPlayerTime;
|
|
@@ -610,11 +610,14 @@ public abstract class LivingEntity extends Entity implements Attackable {
|
|
return true;
|
|
}
|
|
|
|
+ public boolean broadcastedDeath = false; // Folia - region threading
|
|
protected void tickDeath() {
|
|
++this.deathTime;
|
|
- if (this.deathTime >= 20 && !this.level.isClientSide() && !this.isRemoved()) {
|
|
+ if (this.deathTime >= 20 && !this.level.isClientSide() && !this.isRemoved() && !this.broadcastedDeath) { // Folia - region threading
|
|
+ this.broadcastedDeath = true; // Folia - region threading
|
|
this.level.broadcastEntityEvent(this, (byte) 60);
|
|
- this.remove(Entity.RemovalReason.KILLED);
|
|
+ if (!(this instanceof ServerPlayer)) this.remove(Entity.RemovalReason.KILLED); // Folia - region threading - don't remove, we want the tick scheduler to be running
|
|
+ if ((this instanceof ServerPlayer)) this.unRide(); // Folia - region threading - unmount player when dead
|
|
}
|
|
|
|
}
|
|
@@ -835,9 +838,9 @@ public abstract class LivingEntity extends Entity implements Attackable {
|
|
}
|
|
|
|
this.hurtTime = nbt.getShort("HurtTime");
|
|
- this.deathTime = nbt.getShort("DeathTime");
|
|
+ this.deathTime = nbt.getShort("DeathTime"); this.broadcastedDeath = false; // Folia - region threading
|
|
this.lastHurtByMobTimestamp = nbt.getInt("HurtByTimestamp");
|
|
- if (nbt.contains("Team", 8)) {
|
|
+ if (false && nbt.contains("Team", 8)) { // Folia start - region threading
|
|
String s = nbt.getString("Team");
|
|
PlayerTeam scoreboardteam = this.level.getScoreboard().getPlayerTeam(s);
|
|
if (!level.paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof net.minecraft.world.entity.player.Player)) { scoreboardteam = null; } // Paper
|
|
@@ -2266,7 +2269,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
|
|
|
|
@Nullable
|
|
public LivingEntity getKillCredit() {
|
|
- return (LivingEntity) (this.combatTracker.getKiller() != null ? this.combatTracker.getKiller() : (this.lastHurtByPlayer != null ? this.lastHurtByPlayer : (this.lastHurtByMob != null ? this.lastHurtByMob : null)));
|
|
+ return (LivingEntity) (this.combatTracker.getKiller() != null ? this.combatTracker.getKiller() : (this.lastHurtByPlayer != null && io.papermc.paper.util.TickThread.isTickThreadFor(this.lastHurtByPlayer) ? this.lastHurtByPlayer : (this.lastHurtByMob != null && io.papermc.paper.util.TickThread.isTickThreadFor(this.lastHurtByMob) ? this.lastHurtByMob : null))); // Folia - region threading
|
|
}
|
|
|
|
public final float getMaxHealth() {
|
|
@@ -3394,7 +3397,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
|
|
this.pushEntities();
|
|
this.level.getProfiler().pop();
|
|
// Paper start
|
|
- if (((ServerLevel) this.level).hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) {
|
|
+ if (((ServerLevel) this.level).getCurrentWorldData().hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) {
|
|
if (this.xo != getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) {
|
|
Location from = new Location(this.level.getWorld(), this.xo, this.yo, this.zo, this.yRotO, this.xRotO);
|
|
Location to = new Location (this.level.getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot());
|
|
@@ -4054,7 +4057,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
|
|
BlockPos blockposition = BlockPos.containing(d0, d1, d2);
|
|
Level world = this.level;
|
|
|
|
- if (world.hasChunkAt(blockposition)) {
|
|
+ if (io.papermc.paper.util.TickThread.isTickThreadFor((ServerLevel)world, blockposition) && world.hasChunkAt(blockposition)) { // Folia - region threading
|
|
boolean flag2 = false;
|
|
|
|
while (!flag2 && blockposition.getY() > world.getMinBuildHeight()) {
|
|
diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java
|
|
index 02cb6b8c1d59855ff4a8aad3024fe12007eca0ee..88e2d818264450e63e0f5693fdc7940d072e50a6 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/Mob.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/Mob.java
|
|
@@ -135,6 +135,14 @@ public abstract class Mob extends LivingEntity implements Targeting {
|
|
|
|
public boolean aware = true; // CraftBukkit
|
|
|
|
+ // Folia start - region threading
|
|
+ @Override
|
|
+ public void preChangeDimension() {
|
|
+ super.preChangeDimension();
|
|
+ this.dropLeash(true, true);
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
protected Mob(EntityType<? extends Mob> type, Level world) {
|
|
super(type, world);
|
|
this.handItems = NonNullList.withSize(2, ItemStack.EMPTY);
|
|
@@ -283,9 +291,21 @@ public abstract class Mob extends LivingEntity implements Targeting {
|
|
@Nullable
|
|
@Override
|
|
public LivingEntity getTarget() {
|
|
+ // Folia start - region threading
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(this.target)) {
|
|
+ this.target = null;
|
|
+ return null;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
return this.target;
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ public LivingEntity getTargetRaw() {
|
|
+ return this.target;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
public org.bukkit.craftbukkit.entity.CraftMob getBukkitMob() { return (org.bukkit.craftbukkit.entity.CraftMob) super.getBukkitEntity(); } // Paper
|
|
public void setTarget(@Nullable LivingEntity target) {
|
|
// CraftBukkit start - fire event
|
|
@@ -293,7 +313,7 @@ public abstract class Mob extends LivingEntity implements Targeting {
|
|
}
|
|
|
|
public boolean setTarget(LivingEntity entityliving, EntityTargetEvent.TargetReason reason, boolean fireEvent) {
|
|
- if (this.getTarget() == entityliving) return false;
|
|
+ if (this.getTargetRaw() == entityliving) return false; // Folia - region threading
|
|
if (fireEvent) {
|
|
if (reason == EntityTargetEvent.TargetReason.UNKNOWN && this.getTarget() != null && entityliving == null) {
|
|
reason = this.getTarget().isAlive() ? EntityTargetEvent.TargetReason.FORGOT_TARGET : EntityTargetEvent.TargetReason.TARGET_DIED;
|
|
@@ -855,12 +875,7 @@ public abstract class Mob extends LivingEntity implements Targeting {
|
|
if (this.level.getDifficulty() == Difficulty.PEACEFUL && this.shouldDespawnInPeaceful()) {
|
|
this.discard();
|
|
} else if (!this.isPersistenceRequired() && !this.requiresCustomPersistence()) {
|
|
- // Paper start - optimise checkDespawn
|
|
- Player entityhuman = this.level.findNearbyPlayer(this, level.paperConfig().entities.spawning.despawnRanges.get(this.getType().getCategory()).hard() + 1, EntitySelector.PLAYER_AFFECTS_SPAWNING); // Paper
|
|
- if (entityhuman == null) {
|
|
- entityhuman = ((ServerLevel)this.level).playersAffectingSpawning.isEmpty() ? null : ((ServerLevel)this.level).playersAffectingSpawning.get(0);
|
|
- }
|
|
- // Paper end - optimise checkDespawn
|
|
+ Player entityhuman = this.level.getNearestPlayer(this, -1.0D); // Folia - region threading
|
|
|
|
if (entityhuman != null) {
|
|
double d0 = entityhuman.distanceToSqr((Entity) this);
|
|
diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java b/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java
|
|
index 8ec07578c1e41997a2e5ef158885ad3f4c2a31b6..6dcacfca6eb4a8a6425f1aaeb57733d29989032a 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java
|
|
@@ -17,6 +17,12 @@ public class PoiCompetitorScan {
|
|
return context.group(context.present(MemoryModuleType.JOB_SITE), context.present(MemoryModuleType.NEAREST_LIVING_ENTITIES)).apply(context, (jobSite, mobs) -> {
|
|
return (world, entity, time) -> {
|
|
GlobalPos globalPos = context.get(jobSite);
|
|
+ // Folia start - region threading
|
|
+ if (globalPos.dimension() != world.dimension() || !io.papermc.paper.util.TickThread.isTickThreadFor(world, globalPos.pos())) {
|
|
+ entity.getBrain().eraseMemory(MemoryModuleType.JOB_SITE);
|
|
+ return true;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
world.getPoiManager().getType(globalPos.pos()).ifPresent((poiType) -> {
|
|
context.<List<LivingEntity>>get(mobs).stream().filter((mob) -> {
|
|
return mob instanceof Villager && mob != entity;
|
|
diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java
|
|
index 4fe177c950c769f5c6e76b522019b5d5b78259a5..624a4cc8a03880b74fb44bf16e0e35e4398eb1f9 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java
|
|
@@ -70,7 +70,7 @@ public class FollowOwnerGoal extends Goal {
|
|
|
|
@Override
|
|
public boolean canContinueToUse() {
|
|
- return this.navigation.isDone() ? false : (this.unableToMove() ? false : this.tamable.distanceToSqr((Entity) this.owner) > (double) (this.stopDistance * this.stopDistance));
|
|
+ return this.navigation.isDone() ? false : (this.unableToMove() ? false : (this.owner.level == this.level && this.tamable.distanceToSqr((Entity) this.owner) > (double) (this.stopDistance * this.stopDistance))); // Folia - region threading - check level
|
|
}
|
|
|
|
private boolean unableToMove() {
|
|
@@ -96,7 +96,7 @@ public class FollowOwnerGoal extends Goal {
|
|
if (this.tamable.distanceToSqr(this.owner) <= 16 * 16) this.tamable.getLookControl().setLookAt(this.owner, 10.0F, (float) this.tamable.getMaxHeadXRot()); // Paper
|
|
if (--this.timeToRecalcPath <= 0) {
|
|
this.timeToRecalcPath = this.adjustedTickDelay(10);
|
|
- if (this.tamable.distanceToSqr((Entity) this.owner) >= 144.0D) {
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(this.owner) || this.tamable.distanceToSqr((Entity) this.owner) >= 144.0D) { // Folia - region threading - required in case the player suddenly moves into another dimension
|
|
this.teleportToOwner();
|
|
} else {
|
|
this.navigation.moveTo((Entity) this.owner, this.speedModifier);
|
|
@@ -107,6 +107,11 @@ public class FollowOwnerGoal extends Goal {
|
|
|
|
private void teleportToOwner() {
|
|
BlockPos blockposition = this.owner.blockPosition();
|
|
+ // Folia start - region threading
|
|
+ if (this.owner.isRemoved() || this.owner.level != level) {
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
|
|
for (int i = 0; i < 10; ++i) {
|
|
int j = this.randomIntInclusive(-3, 3);
|
|
@@ -137,7 +142,21 @@ public class FollowOwnerGoal extends Goal {
|
|
}
|
|
to = event.getTo();
|
|
|
|
- this.tamable.moveTo(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch());
|
|
+ // Folia start - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick
|
|
+ // also, use teleportAsync so that crossing region boundaries will not blow up
|
|
+ Location finalTo = to;
|
|
+ this.tamable.getBukkitEntity().taskScheduler.schedule((TamableAnimal nmsEntity) -> {
|
|
+ if (nmsEntity.level == FollowOwnerGoal.this.level) {
|
|
+ nmsEntity.teleportAsync(
|
|
+ (net.minecraft.server.level.ServerLevel)nmsEntity.level,
|
|
+ new net.minecraft.world.phys.Vec3(finalTo.getX(), finalTo.getY(), finalTo.getZ()),
|
|
+ Float.valueOf(finalTo.getYaw()), Float.valueOf(finalTo.getPitch()),
|
|
+ net.minecraft.world.phys.Vec3.ZERO, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN, Entity.TELEPORT_FLAG_LOAD_CHUNK,
|
|
+ null
|
|
+ );
|
|
+ }
|
|
+ }, null, 1L);
|
|
+ // Folia start - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick
|
|
// CraftBukkit end
|
|
this.navigation.stop();
|
|
return true;
|
|
diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java
|
|
index 7ffe5bef3778d5971ea4ceadf3102725fd0d08cd..bd6910c279a3a319744d3c7c889a889b892a8f76 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java
|
|
@@ -80,11 +80,11 @@ public abstract class PathNavigation {
|
|
}
|
|
|
|
public void recomputePath() {
|
|
- if (this.level.getGameTime() - this.timeLastRecompute > 20L) {
|
|
+ if (this.tick - this.timeLastRecompute > 20L) { // Folia - region threading
|
|
if (this.targetPos != null) {
|
|
this.path = null;
|
|
this.path = this.createPath(this.targetPos, this.reachRange);
|
|
- this.timeLastRecompute = this.level.getGameTime();
|
|
+ this.timeLastRecompute = this.tick; // Folia - region threading
|
|
this.hasDelayedRecomputation = false;
|
|
}
|
|
} else {
|
|
@@ -199,7 +199,7 @@ public abstract class PathNavigation {
|
|
|
|
public boolean moveTo(Entity entity, double speed) {
|
|
// Paper start - Pathfinding optimizations
|
|
- if (this.pathfindFailures > 10 && this.path == null && net.minecraft.server.MinecraftServer.currentTick < this.lastFailure + 40) {
|
|
+ if (this.pathfindFailures > 10 && this.path == null && this.tick < this.lastFailure + 40) { // Folia - region threading
|
|
return false;
|
|
}
|
|
// Paper end
|
|
@@ -211,7 +211,7 @@ public abstract class PathNavigation {
|
|
return true;
|
|
} else {
|
|
this.pathfindFailures++;
|
|
- this.lastFailure = net.minecraft.server.MinecraftServer.currentTick;
|
|
+ this.lastFailure = this.tick; // Folia - region threading
|
|
return false;
|
|
}
|
|
// Paper end
|
|
diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java
|
|
index e3242cf9a6ad51a23c5781142198dec30c8f376d..f32f5982ceb368b240062b9b8ac0141be59e2f1e 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java
|
|
@@ -37,7 +37,7 @@ public class TemptingSensor extends Sensor<PathfinderMob> {
|
|
|
|
protected void doTick(ServerLevel world, PathfinderMob entity) {
|
|
Brain<?> behaviorcontroller = entity.getBrain();
|
|
- Stream<net.minecraft.server.level.ServerPlayer> stream = world.players().stream().filter(EntitySelector.NO_SPECTATORS).filter((entityplayer) -> { // CraftBukkit - decompile error
|
|
+ Stream<net.minecraft.server.level.ServerPlayer> stream = world.getLocalPlayers().stream().filter(EntitySelector.NO_SPECTATORS).filter((entityplayer) -> { // CraftBukkit - decompile error // Folia - region threading
|
|
return TemptingSensor.TEMPT_TARGETING.test(entity, entityplayer);
|
|
}).filter((entityplayer) -> {
|
|
return entity.closerThan(entityplayer, 10.0D);
|
|
diff --git a/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java b/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java
|
|
index fed09b886f4fa0006d160e5f2abb00dfee45434d..69075026c25ed1ce0f3c769ea0e4a8f3c4841f63 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java
|
|
@@ -22,62 +22,66 @@ import org.slf4j.Logger;
|
|
public class VillageSiege implements CustomSpawner {
|
|
|
|
private static final Logger LOGGER = LogUtils.getLogger();
|
|
- private boolean hasSetupSiege;
|
|
- private VillageSiege.State siegeState;
|
|
- private int zombiesToSpawn;
|
|
- private int nextSpawnTime;
|
|
- private int spawnX;
|
|
- private int spawnY;
|
|
- private int spawnZ;
|
|
+ // Folia - region threading
|
|
|
|
public VillageSiege() {
|
|
- this.siegeState = VillageSiege.State.SIEGE_DONE;
|
|
+ // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) {
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
+ // Folia start - region threading
|
|
+ // check if the spawn pos is no longer owned by this region
|
|
+ if (worldData.villageSiegeState.siegeState != State.SIEGE_DONE
|
|
+ && !io.papermc.paper.util.TickThread.isTickThreadFor(world, worldData.villageSiegeState.spawnX >> 4, worldData.villageSiegeState.spawnZ >> 4, 8)) {
|
|
+ // can't spawn here, just re-set
|
|
+ worldData.villageSiegeState = new io.papermc.paper.threadedregions.RegionizedWorldData.VillageSiegeState();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
if (!world.isDay() && spawnMonsters) {
|
|
float f = world.getTimeOfDay(0.0F);
|
|
|
|
if ((double) f == 0.5D) {
|
|
- this.siegeState = world.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE;
|
|
+ worldData.villageSiegeState.siegeState = world.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; // Folia - region threading
|
|
}
|
|
|
|
- if (this.siegeState == VillageSiege.State.SIEGE_DONE) {
|
|
+ if (worldData.villageSiegeState.siegeState == VillageSiege.State.SIEGE_DONE) { // Folia - region threading
|
|
return 0;
|
|
} else {
|
|
- if (!this.hasSetupSiege) {
|
|
+ if (!worldData.villageSiegeState.hasSetupSiege) { // Folia - region threading
|
|
if (!this.tryToSetupSiege(world)) {
|
|
return 0;
|
|
}
|
|
|
|
- this.hasSetupSiege = true;
|
|
+ worldData.villageSiegeState.hasSetupSiege = true; // Folia - region threading
|
|
}
|
|
|
|
- if (this.nextSpawnTime > 0) {
|
|
- --this.nextSpawnTime;
|
|
+ if (worldData.villageSiegeState.nextSpawnTime > 0) { // Folia - region threading
|
|
+ --worldData.villageSiegeState.nextSpawnTime; // Folia - region threading
|
|
return 0;
|
|
} else {
|
|
- this.nextSpawnTime = 2;
|
|
- if (this.zombiesToSpawn > 0) {
|
|
+ worldData.villageSiegeState.nextSpawnTime = 2; // Folia - region threading
|
|
+ if (worldData.villageSiegeState.zombiesToSpawn > 0) { // Folia - region threading
|
|
this.trySpawn(world);
|
|
- --this.zombiesToSpawn;
|
|
+ --worldData.villageSiegeState.zombiesToSpawn; // Folia - region threading
|
|
} else {
|
|
- this.siegeState = VillageSiege.State.SIEGE_DONE;
|
|
+ worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Folia - region threading
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
}
|
|
} else {
|
|
- this.siegeState = VillageSiege.State.SIEGE_DONE;
|
|
- this.hasSetupSiege = false;
|
|
+ worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Folia - region threading
|
|
+ worldData.villageSiegeState.hasSetupSiege = false; // Folia - region threading
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private boolean tryToSetupSiege(ServerLevel world) {
|
|
- Iterator iterator = world.players().iterator();
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
+ Iterator iterator = world.getLocalPlayers().iterator(); // Folia - region threading
|
|
|
|
while (iterator.hasNext()) {
|
|
Player entityhuman = (Player) iterator.next();
|
|
@@ -89,12 +93,12 @@ public class VillageSiege implements CustomSpawner {
|
|
for (int i = 0; i < 10; ++i) {
|
|
float f = world.random.nextFloat() * 6.2831855F;
|
|
|
|
- this.spawnX = blockposition.getX() + Mth.floor(Mth.cos(f) * 32.0F);
|
|
- this.spawnY = blockposition.getY();
|
|
- this.spawnZ = blockposition.getZ() + Mth.floor(Mth.sin(f) * 32.0F);
|
|
- if (this.findRandomSpawnPos(world, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)) != null) {
|
|
- this.nextSpawnTime = 0;
|
|
- this.zombiesToSpawn = 20;
|
|
+ worldData.villageSiegeState.spawnX = blockposition.getX() + Mth.floor(Mth.cos(f) * 32.0F); // Folia - region threading
|
|
+ worldData.villageSiegeState.spawnY = blockposition.getY(); // Folia - region threading
|
|
+ worldData.villageSiegeState.spawnZ = blockposition.getZ() + Mth.floor(Mth.sin(f) * 32.0F); // Folia - region threading
|
|
+ if (this.findRandomSpawnPos(world, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)) != null) { // Folia - region threading
|
|
+ worldData.villageSiegeState.nextSpawnTime = 0; // Folia - region threading
|
|
+ worldData.villageSiegeState.zombiesToSpawn = 20; // Folia - region threading
|
|
break;
|
|
}
|
|
}
|
|
@@ -108,7 +112,8 @@ public class VillageSiege implements CustomSpawner {
|
|
}
|
|
|
|
private void trySpawn(ServerLevel world) {
|
|
- Vec3 vec3d = this.findRandomSpawnPos(world, new BlockPos(this.spawnX, this.spawnY, this.spawnZ));
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
+ Vec3 vec3d = this.findRandomSpawnPos(world, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)); // Folia - region threading
|
|
|
|
if (vec3d != null) {
|
|
Zombie entityzombie;
|
|
@@ -143,7 +148,7 @@ public class VillageSiege implements CustomSpawner {
|
|
return null;
|
|
}
|
|
|
|
- private static enum State {
|
|
+ public static enum State { // Folia - region threading
|
|
|
|
SIEGE_CAN_ACTIVATE, SIEGE_TONIGHT, SIEGE_DONE;
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
|
|
index 9be85eb0abec02bc0e0eded71c34ab1c565c63e7..9c56304476b4fc841b5d7694232617586ebd8e84 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
|
|
@@ -48,11 +48,13 @@ public class PoiManager extends SectionStorage<PoiSection> {
|
|
}
|
|
|
|
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<PoiSection> {
|
|
}
|
|
|
|
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<PoiSection> {
|
|
|
|
@Override
|
|
public void tick(BooleanSupplier shouldKeepTicking) {
|
|
+ synchronized (this.villageDistanceTracker) { // Folia - region threading
|
|
this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system
|
|
+ } // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
diff --git a/src/main/java/net/minecraft/world/entity/animal/Cat.java b/src/main/java/net/minecraft/world/entity/animal/Cat.java
|
|
index 72b30a5cdeb8a43702d9ab5f198311929761fad1..3951a1f740839f286a190f3fda66132fc4e7ead5 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/animal/Cat.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/animal/Cat.java
|
|
@@ -363,7 +363,7 @@ public class Cat extends TamableAnimal implements VariantHolder<CatVariant> {
|
|
});
|
|
ServerLevel worldserver = world.getLevel();
|
|
|
|
- if (worldserver.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK, world).isValid()) { // Paper - fix deadlock
|
|
+ if (world.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK).isValid()) { // Paper - fix deadlock // Folia - region threading - properly fix this
|
|
this.setVariant((CatVariant) BuiltInRegistries.CAT_VARIANT.getOrThrow(CatVariant.ALL_BLACK));
|
|
this.setPersistenceRequired();
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java
|
|
index 81dab77f525ae667614f940c4ff5ec308a9579a2..5fdb9ab7c6c0ae6bb948a44c5578c4304e8d6c3f 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java
|
|
@@ -337,9 +337,9 @@ public class Turtle extends Animal {
|
|
|
|
@Override
|
|
public void thunderHit(ServerLevel world, LightningBolt lightning) {
|
|
- org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = lightning; // CraftBukkit
|
|
+ org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(lightning); // CraftBukkit // Folia - region threading
|
|
this.hurt(this.damageSources().lightningBolt(), Float.MAX_VALUE);
|
|
- org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = null; // CraftBukkit
|
|
+ org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Folia - region threading
|
|
}
|
|
|
|
private static class TurtleMoveControl extends MoveControl {
|
|
diff --git a/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java b/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java
|
|
index 30aec9dff249ae629b22318e52902361a9fa4099..b452b566693558fa714eca2db08ca775e73d48a0 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java
|
|
@@ -294,8 +294,10 @@ public class ItemFrame extends HangingEntity {
|
|
MapItemSavedData worldmap = MapItem.getSavedData(i, this.level);
|
|
|
|
if (worldmap != null) {
|
|
+ synchronized (worldmap) { // Folia - make map data thread-safe
|
|
worldmap.removedFromFrame(this.pos, this.getId());
|
|
worldmap.setDirty(true);
|
|
+ } // Folia - make map data thread-safe
|
|
}
|
|
|
|
});
|
|
diff --git a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java
|
|
index ce2c3c146ef64400e00084bd2245d2b87a67fbc2..c8d125955754e27da54d95fb5b1cea39ca54b618 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java
|
|
@@ -293,9 +293,9 @@ public class FallingBlockEntity extends Entity {
|
|
float f2 = (float) Math.min(Mth.floor((float) i * this.fallDamagePerDistance), this.fallDamageMax);
|
|
|
|
this.level.getEntities((Entity) this, this.getBoundingBox(), predicate).forEach((entity) -> {
|
|
- CraftEventFactory.entityDamage = this; // CraftBukkit
|
|
+ CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // Folia - region threading
|
|
entity.hurt(damagesource1, f2);
|
|
- CraftEventFactory.entityDamage = null; // CraftBukkit
|
|
+ CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Folia - region threading
|
|
});
|
|
boolean flag = this.blockState.is(BlockTags.ANVIL);
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java
|
|
index d47b3ac633e7936d30abfda6fc46c2c7412d76fe..4245973677f1e2a3a75dce74c3e010330d0a9a41 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java
|
|
@@ -51,7 +51,7 @@ public class ItemEntity extends Entity implements TraceableEntity {
|
|
@Nullable
|
|
public UUID target;
|
|
public final float bobOffs;
|
|
- private int lastTick = MinecraftServer.currentTick - 1; // CraftBukkit
|
|
+ //private int lastTick = MinecraftServer.currentTick - 1; // CraftBukkit // Folia - region threading
|
|
public boolean canMobPickup = true; // Paper
|
|
private int despawnRate = -1; // Paper
|
|
public net.kyori.adventure.util.TriState frictionState = net.kyori.adventure.util.TriState.NOT_SET; // Paper
|
|
@@ -125,13 +125,11 @@ public class ItemEntity extends Entity implements TraceableEntity {
|
|
this.discard();
|
|
} else {
|
|
super.tick();
|
|
- // CraftBukkit start - Use wall time for pickup and despawn timers
|
|
- int elapsedTicks = MinecraftServer.currentTick - this.lastTick;
|
|
- if (this.pickupDelay != 32767) this.pickupDelay -= elapsedTicks;
|
|
- this.pickupDelay = Math.max(0, this.pickupDelay); // Paper - don't go below 0
|
|
- if (this.age != -32768) this.age += elapsedTicks;
|
|
- this.lastTick = MinecraftServer.currentTick;
|
|
- // CraftBukkit end
|
|
+ // Folia start - region threading - restore original timers
|
|
+ if (this.pickupDelay > 0 && this.pickupDelay != 32767) {
|
|
+ --this.pickupDelay;
|
|
+ }
|
|
+ // Folia end - region threading - restore original timers
|
|
|
|
this.xo = this.getX();
|
|
this.yo = this.getY();
|
|
@@ -185,11 +183,11 @@ public class ItemEntity extends Entity implements TraceableEntity {
|
|
this.mergeWithNeighbours();
|
|
}
|
|
|
|
- /* CraftBukkit start - moved up
|
|
+ // Folia - region threading - restore original timers
|
|
if (this.age != -32768) {
|
|
++this.age;
|
|
}
|
|
- // CraftBukkit end */
|
|
+ // Folia - region threading - restore original timers
|
|
|
|
this.hasImpulse |= this.updateInWaterStateAndDoFluidPushing();
|
|
if (!this.level.isClientSide) {
|
|
@@ -216,13 +214,14 @@ public class ItemEntity extends Entity implements TraceableEntity {
|
|
// Spigot start - copied from above
|
|
@Override
|
|
public void inactiveTick() {
|
|
- // CraftBukkit start - Use wall time for pickup and despawn timers
|
|
- int elapsedTicks = MinecraftServer.currentTick - this.lastTick;
|
|
- if (this.pickupDelay != 32767) this.pickupDelay -= elapsedTicks;
|
|
- this.pickupDelay = Math.max(0, this.pickupDelay); // Paper - don't go below 0
|
|
- if (this.age != -32768) this.age += elapsedTicks;
|
|
- this.lastTick = MinecraftServer.currentTick;
|
|
- // CraftBukkit end
|
|
+ // Folia start - region threading - restore original timers
|
|
+ if (this.pickupDelay > 0 && this.pickupDelay != 32767) {
|
|
+ --this.pickupDelay;
|
|
+ }
|
|
+ if (this.age != -32768) {
|
|
+ ++this.age;
|
|
+ }
|
|
+ // Folia end - region threading - restore original timers
|
|
|
|
if (!this.level.isClientSide && this.age >= this.despawnRate) { // Spigot // Paper
|
|
// CraftBukkit start - fire ItemDespawnEvent
|
|
@@ -520,14 +519,20 @@ public class ItemEntity extends Entity implements TraceableEntity {
|
|
return false;
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ @Override
|
|
+ public void postChangeDimension() {
|
|
+ super.postChangeDimension();
|
|
+ this.mergeWithNeighbours();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Nullable
|
|
@Override
|
|
public Entity changeDimension(ServerLevel destination) {
|
|
Entity entity = super.changeDimension(destination);
|
|
|
|
- if (!this.level.isClientSide && entity instanceof ItemEntity) {
|
|
- ((ItemEntity) entity).mergeWithNeighbours();
|
|
- }
|
|
+ if (entity != null) entity.postChangeDimension(); // Folia - region threading - move to post change
|
|
|
|
return entity;
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java
|
|
index dfdf7e7fc1070975ec18fd215c724f4fc84d3705..52150e27294fcd51ae23ea3ff06673fb1ca3d113 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java
|
|
@@ -60,7 +60,7 @@ public class PrimedTnt extends Entity implements TraceableEntity {
|
|
|
|
@Override
|
|
public void tick() {
|
|
- if (level.spigotConfig.maxTntTicksPerTick > 0 && ++level.spigotConfig.currentPrimedTnt > level.spigotConfig.maxTntTicksPerTick) { return; } // Spigot
|
|
+ if (level.spigotConfig.maxTntTicksPerTick > 0 && ++level.getCurrentWorldData().currentPrimedTnt > level.spigotConfig.maxTntTicksPerTick) { return; } // Spigot // Folia - region threading
|
|
if (!this.isNoGravity()) {
|
|
this.setDeltaMovement(this.getDeltaMovement().add(0.0D, -0.04D, 0.0D));
|
|
}
|
|
@@ -102,7 +102,7 @@ public class PrimedTnt extends Entity implements TraceableEntity {
|
|
*/
|
|
// Send position and velocity updates to nearby players on every tick while the TNT is in water.
|
|
// This does pretty well at keeping their clients in sync with the server.
|
|
- net.minecraft.server.level.ChunkMap.TrackedEntity ete = ((net.minecraft.server.level.ServerLevel)this.level).getChunkSource().chunkMap.entityMap.get(this.getId());
|
|
+ net.minecraft.server.level.ChunkMap.TrackedEntity ete = this.tracker; // Folia - region threading
|
|
if (ete != null) {
|
|
net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket velocityPacket = new net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket(this);
|
|
net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket positionPacket = new net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket(this);
|
|
diff --git a/src/main/java/net/minecraft/world/entity/monster/Zombie.java b/src/main/java/net/minecraft/world/entity/monster/Zombie.java
|
|
index 9976205537cfe228735687f1e9c52c74ac025690..f286abfff186b657db99f28d3592465ccee4498a 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/monster/Zombie.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/monster/Zombie.java
|
|
@@ -94,7 +94,7 @@ public class Zombie extends Monster {
|
|
private boolean canBreakDoors;
|
|
private int inWaterTime;
|
|
public int conversionTime;
|
|
- private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field
|
|
+ // private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field // Folia - region threading - restore original timers
|
|
private boolean shouldBurnInDay = true; // Paper
|
|
|
|
public Zombie(EntityType<? extends Zombie> 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<? extends ZombieVillager> type, Level world) {
|
|
super(type, world);
|
|
@@ -145,10 +145,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder {
|
|
public void tick() {
|
|
if (!this.level.isClientSide && this.isAlive() && this.isConverting()) {
|
|
int i = this.getConversionProgress();
|
|
- // CraftBukkit start - Use wall time instead of ticks for villager conversion
|
|
- int elapsedTicks = MinecraftServer.currentTick - this.lastTick;
|
|
- i *= elapsedTicks;
|
|
- // CraftBukkit end
|
|
+ // Folia - region threading - restore original timers
|
|
|
|
this.villagerConversionTime -= i;
|
|
if (this.villagerConversionTime <= 0) {
|
|
@@ -157,7 +154,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder {
|
|
}
|
|
|
|
super.tick();
|
|
- this.lastTick = MinecraftServer.currentTick; // CraftBukkit
|
|
+ // Folia - region threading - restore original timers
|
|
}
|
|
|
|
@Override
|
|
diff --git a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java
|
|
index ca96b893e22de3ae7c11d5cded51edf70bdcb6f2..6000e891620850aac303630bf6676085d41f65b5 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java
|
|
@@ -213,10 +213,18 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa
|
|
this.readInventoryFromTag(nbt);
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ @Override
|
|
+ public void preChangeDimension() {
|
|
+ super.preChangeDimension();
|
|
+ this.stopTrading();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Nullable
|
|
@Override
|
|
public Entity changeDimension(ServerLevel destination) {
|
|
- this.stopTrading();
|
|
+ this.preChangeDimension(); // Folia - region threading - move into preChangeDimension
|
|
return super.changeDimension(destination);
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java
|
|
index 5f407535298a31a34cfe114dd863fd6a9b977707..cb0f75fb32836efa50f0a86dfae7907b0c88780f 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java
|
|
@@ -21,17 +21,18 @@ import net.minecraft.world.phys.AABB;
|
|
|
|
public class CatSpawner implements CustomSpawner {
|
|
private static final int TICK_DELAY = 1200;
|
|
- private int nextTick;
|
|
+ //private int nextTick; // Folia - region threading
|
|
|
|
@Override
|
|
public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) {
|
|
if (spawnAnimals && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) {
|
|
- --this.nextTick;
|
|
- if (this.nextTick > 0) {
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
+ --worldData.catSpawnerNextTick; // Folia - region threading
|
|
+ if (worldData.catSpawnerNextTick > 0) { // Folia - region threading
|
|
return 0;
|
|
} else {
|
|
- this.nextTick = 1200;
|
|
- Player player = world.getRandomPlayer();
|
|
+ worldData.catSpawnerNextTick = 1200; // Folia - region threading
|
|
+ Player player = world.getRandomLocalPlayer(); // Folia - region threading
|
|
if (player == null) {
|
|
return 0;
|
|
} else {
|
|
diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java
|
|
index 6023b9eb3001e1a98ab8b970d853c4e7c7603f4d..0d16b7ea1e2c7d430b09721d50ea199aaa58bc89 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/npc/Villager.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java
|
|
@@ -201,7 +201,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
|
|
brain.setCoreActivities(ImmutableSet.of(Activity.CORE));
|
|
brain.setDefaultActivity(Activity.IDLE);
|
|
brain.setActiveActivityIfPossible(Activity.IDLE);
|
|
- brain.updateActivityFromSchedule(this.level.getDayTime(), this.level.getGameTime());
|
|
+ brain.updateActivityFromSchedule(this.level.getLevelData().getDayTime(), this.level.getLevelData().getGameTime()); // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
@@ -722,6 +722,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
|
|
ServerLevel worldserver = minecraftserver.getLevel(globalpos.dimension());
|
|
|
|
if (worldserver != null) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( // Folia - region threading
|
|
+ worldserver, globalpos.pos().getX() >> 4, globalpos.pos().getZ() >> 4, () -> { // Folia - region threading
|
|
PoiManager villageplace = worldserver.getPoiManager();
|
|
Optional<Holder<PoiType>> optional = villageplace.getType(globalpos.pos());
|
|
BiPredicate<Villager, Holder<PoiType>> bipredicate = (BiPredicate) Villager.POI_MEMORIES.get(pos);
|
|
@@ -730,6 +732,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
|
|
villageplace.release(globalpos.pos());
|
|
DebugPackets.sendPoiTicketCountPacket(worldserver, globalpos.pos());
|
|
}
|
|
+ }); // Folia - region threading
|
|
|
|
}
|
|
});
|
|
diff --git a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java
|
|
index ae9f9112ce9bec82e7571f679017f1723d9eb982..509718d30800f94ea7d3233aba8566547e5766af 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,37 +54,37 @@ public class WanderingTraderSpawner implements CustomSpawner {
|
|
|
|
@Override
|
|
public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) {
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
// Paper start
|
|
- if (this.tickDelay == Integer.MIN_VALUE) {
|
|
- this.tickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
|
|
- this.spawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength;
|
|
- this.spawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin;
|
|
+ if (worldData.wanderingTraderTickDelay == Integer.MIN_VALUE) { // Folia - region threading
|
|
+ worldData.wanderingTraderTickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading
|
|
+ worldData.wanderingTraderSpawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading
|
|
+ worldData.wanderingTraderSpawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading
|
|
}
|
|
if (!world.getGameRules().getBoolean(GameRules.RULE_DO_TRADER_SPAWNING)) {
|
|
return 0;
|
|
- } else if (this.tickDelay - 1 > 0) {
|
|
- this.tickDelay = this.tickDelay - 1;
|
|
+ } else if (worldData.wanderingTraderTickDelay - 1 > 0) { // Folia - region threading
|
|
+ worldData.wanderingTraderTickDelay = worldData.wanderingTraderTickDelay - 1; // Folia - region threading
|
|
return 0;
|
|
} else {
|
|
- this.tickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
|
|
- this.spawnDelay = this.spawnDelay - world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
|
|
+ worldData.wanderingTraderTickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading
|
|
+ worldData.wanderingTraderSpawnDelay = worldData.wanderingTraderSpawnDelay - world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading
|
|
//this.serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways
|
|
- if (this.spawnDelay > 0) {
|
|
+ if (worldData.wanderingTraderSpawnDelay > 0) { // Folia - region threading
|
|
return 0;
|
|
} else {
|
|
- this.spawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength;
|
|
+ worldData.wanderingTraderSpawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading
|
|
if (!world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) {
|
|
return 0;
|
|
} else {
|
|
- int i = this.spawnChance;
|
|
+ int i = worldData.wanderingTraderSpawnChance; // Folia - region threading
|
|
|
|
- this.serverLevelData.setWanderingTraderSpawnChance(this.spawnChance);
|
|
- 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 {
|
|
@@ -98,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;
|
|
@@ -128,7 +126,7 @@ public class WanderingTraderSpawner implements CustomSpawner {
|
|
this.tryToSpawnLlamaFor(world, entityvillagertrader, 4);
|
|
}
|
|
|
|
- this.serverLevelData.setWanderingTraderId(entityvillagertrader.getUUID());
|
|
+ //this.serverLevelData.setWanderingTraderId(entityvillagertrader.getUUID()); // Folia - region threading - doesn't appear to be used anywhere, so avoid the race condition here...
|
|
// entityvillagertrader.setDespawnDelay(48000); // CraftBukkit - moved to EntityVillagerTrader constructor. This lets the value be modified by plugins on CreatureSpawnEvent
|
|
entityvillagertrader.setWanderTarget(blockposition1);
|
|
entityvillagertrader.restrictTo(blockposition1, 16);
|
|
diff --git a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java
|
|
index 5d6d26cfe8f0ab68a3145214b3fc126ca7a71a66..c8be8b54859e39967ede5c53ce940da39a68563e 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java
|
|
@@ -149,6 +149,11 @@ public abstract class AbstractArrow extends Projectile {
|
|
@Override
|
|
public void tick() {
|
|
super.tick();
|
|
+ // Folia start - region threading - make sure entities do not move into regions they do not own
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.getLevel(), this.position(), this.getDeltaMovement(), 1)) {
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading - make sure entities do not move into regions they do not own
|
|
boolean flag = this.isNoPhysics();
|
|
Vec3 vec3d = this.getDeltaMovement();
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java b/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java
|
|
index 2096e8a0bdbcfc865f175f3a01ab688542481531..8e072c1ae653258722fce7b50cd27076988d9666 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java
|
|
@@ -77,6 +77,11 @@ public abstract class AbstractHurtingProjectile extends Projectile {
|
|
this.discard();
|
|
} else {
|
|
super.tick();
|
|
+ // Folia start - region threading - make sure entities do not move into regions they do not own
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.getLevel(), this.position(), this.getDeltaMovement(), 1)) {
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading - make sure entities do not move into regions they do not own
|
|
if (this.shouldBurn()) {
|
|
this.setSecondsOnFire(1);
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java b/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java
|
|
index d8a2b5e0e0ff640bcc827f1b1310bd19b9154cc2..2a4542a9bcb990e276796fb042d9f780cc7b63dd 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java
|
|
@@ -129,9 +129,9 @@ public class EvokerFangs extends Entity implements TraceableEntity {
|
|
|
|
if (target.isAlive() && !target.isInvulnerable() && target != entityliving1) {
|
|
if (entityliving1 == null) {
|
|
- org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = this; // CraftBukkit
|
|
+ org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // Folia - region threading
|
|
target.hurt(this.damageSources().magic(), 6.0F);
|
|
- org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = null; // CraftBukkit
|
|
+ org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Folia - region threading
|
|
} else {
|
|
if (entityliving1.isAlliedTo((Entity) target)) {
|
|
return;
|
|
diff --git a/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java b/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
|
|
index fca27f98989bf106060ba08196255fe32f850df5..98c9eda4056093d8d230ac56ac407c39556ba2cb 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
|
|
@@ -129,6 +129,11 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier {
|
|
|
|
});
|
|
}
|
|
+ // Folia start - region threading
|
|
+ if (this.attachedToEntity != null && !io.papermc.paper.util.TickThread.isTickThreadFor(this.attachedToEntity)) {
|
|
+ this.attachedToEntity = null;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
|
|
if (this.attachedToEntity != null) {
|
|
if (this.attachedToEntity.isFallFlying()) {
|
|
@@ -240,9 +245,9 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier {
|
|
|
|
if (f > 0.0F) {
|
|
if (this.attachedToEntity != null) {
|
|
- CraftEventFactory.entityDamage = this; // CraftBukkit
|
|
+ CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // Folia - region threading
|
|
this.attachedToEntity.hurt(this.damageSources().fireworks(this, this.getOwner()), 5.0F + (float) (nbttaglist.size() * 2));
|
|
- CraftEventFactory.entityDamage = null; // CraftBukkit
|
|
+ CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Folia - region threading
|
|
}
|
|
|
|
double d0 = 5.0D;
|
|
@@ -269,9 +274,9 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier {
|
|
if (flag) {
|
|
float f1 = f * (float) Math.sqrt((5.0D - (double) this.distanceTo(entityliving)) / 5.0D);
|
|
|
|
- CraftEventFactory.entityDamage = this; // CraftBukkit
|
|
+ CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // Folia - region threading
|
|
entityliving.hurt(this.damageSources().fireworks(this, this.getOwner()), f1);
|
|
- CraftEventFactory.entityDamage = null; // CraftBukkit
|
|
+ CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // Folia - region threading
|
|
}
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
|
|
index d77410588a1c10d8ac902f21a8bd7e35f74fecd2..675a01ec82b31140594beace0f0b651be096f90e 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
|
|
@@ -98,7 +98,7 @@ public class FishingHook extends Projectile {
|
|
|
|
public FishingHook(net.minecraft.world.entity.player.Player thrower, Level world, int luckOfTheSeaLevel, int lureLevel) {
|
|
this(EntityType.FISHING_BOBBER, world, luckOfTheSeaLevel, lureLevel);
|
|
- this.setOwner(thrower);
|
|
+ //this.setOwner(thrower); // Folia - region threading - move this down after position so that thread-checks do not fail
|
|
float f = thrower.getXRot();
|
|
float f1 = thrower.getYRot();
|
|
float f2 = Mth.cos(-f1 * 0.017453292F - 3.1415927F);
|
|
@@ -110,6 +110,7 @@ public class FishingHook extends Projectile {
|
|
double d2 = thrower.getZ() - (double) f2 * 0.3D;
|
|
|
|
this.moveTo(d0, d1, d2, f1, f);
|
|
+ this.setOwner(thrower); // Folia - region threading - move this down after position so that thread-checks do not fail
|
|
Vec3 vec3d = new Vec3((double) (-f3), (double) Mth.clamp(-(f5 / f4), -5.0F, 5.0F), (double) (-f2));
|
|
double d3 = vec3d.length();
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java
|
|
index c4f4a26e016eea744f587461af80461074d48303..42f6481eb92a3aa91010c235f08ea6581e36573e 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java
|
|
@@ -30,6 +30,11 @@ public class LlamaSpit extends Projectile {
|
|
public void tick() {
|
|
super.tick();
|
|
Vec3 vec3d = this.getDeltaMovement();
|
|
+ // Folia start - region threading - make sure entities do not move into regions they do not own
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.getLevel(), this.position(), this.getDeltaMovement(), 1)) {
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading - make sure entities do not move into regions they do not own
|
|
HitResult movingobjectposition = ProjectileUtil.getHitResult(this, this::canHitEntity);
|
|
|
|
this.preOnHit(movingobjectposition); // CraftBukkit - projectile hit event
|
|
diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
|
|
index 8b2a3a8482018b7db7de81bc295862f783e17ce5..ad8cff8bc7f12a48e2b5c9bbc30b33676d61853b 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
|
|
@@ -53,9 +53,20 @@ public abstract class Projectile extends Entity implements TraceableEntity {
|
|
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ // In general, this is an entire mess. At the time of writing, there are fifty usages of getOwner.
|
|
+ // Usage of this function is to avoid concurrency issues, even if it sacrifices behavior.
|
|
@Nullable
|
|
@Override
|
|
public Entity getOwner() {
|
|
+ Entity ret = this.getOwnerRaw();
|
|
+ return io.papermc.paper.util.TickThread.isTickThreadFor(ret) ? ret : null;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
+ @Nullable
|
|
+ public Entity getOwnerRaw() { // Folia - region threading
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot update owner state asynchronously"); // Folia - region threading
|
|
if (this.cachedOwner != null && !this.cachedOwner.isRemoved()) {
|
|
return this.cachedOwner;
|
|
} else if (this.ownerUUID != null && this.level instanceof ServerLevel) {
|
|
@@ -277,6 +288,6 @@ public abstract class Projectile extends Entity implements TraceableEntity {
|
|
public boolean mayInteract(Level world, BlockPos pos) {
|
|
Entity entity = this.getOwner();
|
|
|
|
- return entity instanceof Player ? entity.mayInteract(world, pos) : entity == null || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING);
|
|
+ return entity instanceof Player && io.papermc.paper.util.TickThread.isTickThreadFor(entity) ? entity.mayInteract(world, pos) : entity == null || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Folia - region threading
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java
|
|
index b9e4955fecabbad8d6762f3d933ea1402e932d9b..e026f2517a0714bba0f51aa2bf087c1babb55990 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java
|
|
@@ -23,7 +23,7 @@ public class SmallFireball extends Fireball {
|
|
public SmallFireball(Level world, LivingEntity owner, double velocityX, double velocityY, double velocityZ) {
|
|
super(EntityType.SMALL_FIREBALL, owner, velocityX, velocityY, velocityZ, world);
|
|
// CraftBukkit start
|
|
- if (this.getOwner() != null && this.getOwner() instanceof Mob) {
|
|
+ if (owner != null && owner instanceof Mob) { // Folia - region threading
|
|
isIncendiary = this.level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING);
|
|
}
|
|
// CraftBukkit end
|
|
diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java b/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java
|
|
index 88181c59e604ba3b132b9e695cef5eaf5b836029..0146b150b6cb70a7272e8d9781e100a012f93d9b 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java
|
|
@@ -44,6 +44,11 @@ public abstract class ThrowableProjectile extends Projectile {
|
|
@Override
|
|
public void tick() {
|
|
super.tick();
|
|
+ // Folia start - region threading - make sure entities do not move into regions they do not own
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.getLevel(), this.position(), this.getDeltaMovement(), 1)) {
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading - make sure entities do not move into regions they do not own
|
|
HitResult movingobjectposition = ProjectileUtil.getHitResult(this, this::canHitEntity);
|
|
boolean flag = false;
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java
|
|
index 39ab9a283d856ba8d578d1378285758e32a24cf0..2b35495df87614c3578727598ebf1b4469829154 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java
|
|
@@ -43,6 +43,62 @@ public class ThrownEnderpearl extends ThrowableItemProjectile {
|
|
entityHitResult.getEntity().hurt(this.damageSources().thrown(this, this.getOwner()), 0.0F);
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ private static void attemptTeleport(Entity source, ServerLevel checkWorld, net.minecraft.world.phys.Vec3 to) {
|
|
+ // ignore retired callback, in those cases we do not want to teleport
|
|
+ source.getBukkitEntity().taskScheduler.schedule(
|
|
+ (Entity entity) -> {
|
|
+ // source is now an invalid reference, do not use it, use the entity parameter
|
|
+
|
|
+ if (entity.getLevel() != checkWorld) {
|
|
+ // cannot teleport cross-world
|
|
+ return;
|
|
+ }
|
|
+ if (entity.isVehicle()) {
|
|
+ // cannot teleport vehicles
|
|
+ return;
|
|
+ }
|
|
+ // dismount from any vehicles, so we can teleport and to prevent desync
|
|
+ if (entity.isPassenger()) {
|
|
+ entity.stopRiding();
|
|
+ }
|
|
+
|
|
+ // reset fall damage so that if the entity was falling they do not instantly die
|
|
+ entity.resetFallDistance();
|
|
+
|
|
+ entity.teleportAsync(
|
|
+ checkWorld, to, null, null, null,
|
|
+ PlayerTeleportEvent.TeleportCause.ENDER_PEARL,
|
|
+ // chunk could have been unloaded
|
|
+ Entity.TELEPORT_FLAG_LOAD_CHUNK,
|
|
+ (Entity teleported) -> {
|
|
+ // entity is now an invalid reference, do not use it, instead use teleported
|
|
+ if (teleported instanceof ServerPlayer player) {
|
|
+ // connection teleport is already done
|
|
+ ServerLevel world = player.getLevel();
|
|
+
|
|
+ // endermite spawn chance
|
|
+ if (world.random.nextFloat() < 0.05F && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) {
|
|
+ Endermite entityendermite = (Endermite) EntityType.ENDERMITE.create(world);
|
|
+
|
|
+ if (entityendermite != null) {
|
|
+ entityendermite.moveTo(entity.getX(), entity.getY(), entity.getZ(), entity.getYRot(), entity.getXRot());
|
|
+ world.addFreshEntity(entityendermite, CreatureSpawnEvent.SpawnReason.ENDER_PEARL);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // damage player
|
|
+ player.hurt(player.damageSources().fall(), 5.0F);
|
|
+ }
|
|
+ }
|
|
+ );
|
|
+ },
|
|
+ null,
|
|
+ 1L
|
|
+ );
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Override
|
|
protected void onHit(HitResult hitResult) {
|
|
super.onHit(hitResult);
|
|
@@ -52,6 +108,20 @@ public class ThrownEnderpearl extends ThrowableItemProjectile {
|
|
}
|
|
|
|
if (!this.level.isClientSide && !this.isRemoved()) {
|
|
+ // Folia start - region threading
|
|
+ if (true) {
|
|
+ // we can't fire events, because we do not actually know where the other entity is located
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(this)) {
|
|
+ throw new IllegalStateException("Must be on tick thread for ticking entity: " + this);
|
|
+ }
|
|
+ Entity entity = this.getOwnerRaw();
|
|
+ if (entity != null) {
|
|
+ attemptTeleport(entity, (ServerLevel)this.getLevel(), this.position());
|
|
+ }
|
|
+ this.discard();
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
Entity entity = this.getOwner();
|
|
|
|
if (entity instanceof ServerPlayer) {
|
|
@@ -83,9 +153,9 @@ public class ThrownEnderpearl extends ThrowableItemProjectile {
|
|
|
|
entityplayer.connection.teleport(teleEvent.getTo());
|
|
entity.resetFallDistance();
|
|
- CraftEventFactory.entityDamage = this;
|
|
+ CraftEventFactory.entityDamageRT.set(this); // Folia - region threading
|
|
entity.hurt(this.damageSources().fall(), 5.0F);
|
|
- CraftEventFactory.entityDamage = null;
|
|
+ CraftEventFactory.entityDamageRT.set(null); // Folia - region threading
|
|
}
|
|
// CraftBukkit end
|
|
}
|
|
@@ -111,6 +181,14 @@ public class ThrownEnderpearl extends ThrowableItemProjectile {
|
|
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ @Override
|
|
+ public void preChangeDimension() {
|
|
+ super.preChangeDimension();
|
|
+ // Don't change the owner here, since the tick logic will consider it anyways.
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Nullable
|
|
@Override
|
|
public Entity changeDimension(ServerLevel destination) {
|
|
diff --git a/src/main/java/net/minecraft/world/entity/raid/Raid.java b/src/main/java/net/minecraft/world/entity/raid/Raid.java
|
|
index 879c3bb661e24b9682b654def57c2800f4f8ca92..359f1690497eac00899eb26c17308e0a6fe943ad 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/raid/Raid.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/raid/Raid.java
|
|
@@ -108,6 +108,13 @@ public class Raid {
|
|
private int celebrationTicks;
|
|
private Optional<BlockPos> 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<ServerPlayer> collection = this.raidEvent.getPlayers();
|
|
long i = this.random.nextLong();
|
|
- Iterator iterator = this.level.players().iterator();
|
|
+ Iterator iterator = this.level.getLocalPlayers().iterator(); // Folia - region threading
|
|
|
|
while (iterator.hasNext()) {
|
|
ServerPlayer entityplayer = (ServerPlayer) iterator.next();
|
|
diff --git a/src/main/java/net/minecraft/world/entity/raid/Raider.java b/src/main/java/net/minecraft/world/entity/raid/Raider.java
|
|
index 99e9d46d42ddd0b451c6aeb847f1b295ebe5c697..9bc6b5a0b476e019e719709e68314d8e84d55a59 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/raid/Raider.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/raid/Raider.java
|
|
@@ -91,7 +91,7 @@ public abstract class Raider extends PatrollingMonster {
|
|
|
|
if (this.canJoinRaid()) {
|
|
if (raid == null) {
|
|
- if (this.level.getGameTime() % 20L == 0L) {
|
|
+ if (this.level.getRedstoneGameTime() % 20L == 0L) { // Folia - region threading
|
|
Raid raid1 = ((ServerLevel) this.level).getRaidAt(this.blockPosition());
|
|
|
|
if (raid1 != null && Raids.canJoinRaid(this, raid1)) {
|
|
diff --git a/src/main/java/net/minecraft/world/entity/raid/Raids.java b/src/main/java/net/minecraft/world/entity/raid/Raids.java
|
|
index fabce3bc592b1b172b227395a07febdbb66ec3c9..ebdbadc1dba41f47ba4795ea43020b80102f5ccb 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/raid/Raids.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/raid/Raids.java
|
|
@@ -28,14 +28,14 @@ import net.minecraft.world.phys.Vec3;
|
|
public class Raids extends SavedData {
|
|
|
|
private static final String RAID_FILE_ID = "raids";
|
|
- public final Map<Integer, Raid> raidMap = Maps.newHashMap();
|
|
+ public final Map<Integer, Raid> 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 1b8f22805af87dc08e0dea9fd93a5f93c0b05107..00bc99948ddd67bb85c3797f869064540a8b1213 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java
|
|
@@ -128,7 +128,7 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper
|
|
|
|
// Paper start
|
|
public void immunize() {
|
|
- this.activatedImmunityTick = Math.max(this.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 20);
|
|
+ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20);
|
|
}
|
|
// Paper end
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/item/ArmorItem.java b/src/main/java/net/minecraft/world/item/ArmorItem.java
|
|
index d7a0cbde8f8c99276307502674c71463fbe7e89c..2a501b3fa8d69f627b279fd035fd2cb11d590f06 100644
|
|
--- a/src/main/java/net/minecraft/world/item/ArmorItem.java
|
|
+++ b/src/main/java/net/minecraft/world/item/ArmorItem.java
|
|
@@ -68,7 +68,7 @@ public class ArmorItem extends Item implements Equipable {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
|
|
|
|
BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityliving.getBukkitEntity());
|
|
- if (!DispenserBlock.eventFired) {
|
|
+ if (!DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
world.getCraftServer().getPluginManager().callEvent(event);
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java
|
|
index a6253272205337b3b855679b3057c2519a807a4c..c867737f839598b996b58942017ff6f5b2aaa3f0 100644
|
|
--- a/src/main/java/net/minecraft/world/item/ItemStack.java
|
|
+++ b/src/main/java/net/minecraft/world/item/ItemStack.java
|
|
@@ -334,6 +334,7 @@ public final class ItemStack {
|
|
}
|
|
|
|
public InteractionResult useOn(UseOnContext itemactioncontext, InteractionHand enumhand) { // CraftBukkit - add hand
|
|
+
|
|
net.minecraft.world.entity.player.Player entityhuman = itemactioncontext.getPlayer();
|
|
BlockPos blockposition = itemactioncontext.getClickedPos();
|
|
BlockInWorld shapedetectorblock = new BlockInWorld(itemactioncontext.getLevel(), blockposition, false);
|
|
@@ -345,12 +346,13 @@ public final class ItemStack {
|
|
CompoundTag oldData = this.getTagClone();
|
|
int oldCount = this.getCount();
|
|
ServerLevel world = (ServerLevel) itemactioncontext.getLevel();
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
|
|
if (!(this.getItem() instanceof BucketItem/* || this.getItem() instanceof SolidBucketItem*/)) { // if not bucket // Paper - capture block states for snow buckets
|
|
- world.captureBlockStates = true;
|
|
+ worldData.captureBlockStates = true; // Folia - region threading
|
|
// special case bonemeal
|
|
if (this.getItem() == Items.BONE_MEAL) {
|
|
- world.captureTreeGeneration = true;
|
|
+ worldData.captureTreeGeneration = true; // Folia - region threading
|
|
}
|
|
}
|
|
Item item = this.getItem();
|
|
@@ -359,14 +361,14 @@ public final class ItemStack {
|
|
int newCount = this.getCount();
|
|
this.setCount(oldCount);
|
|
this.setTagClone(oldData);
|
|
- world.captureBlockStates = false;
|
|
- if (enuminteractionresult.consumesAction() && world.captureTreeGeneration && world.capturedBlockStates.size() > 0) {
|
|
- world.captureTreeGeneration = false;
|
|
+ worldData.captureBlockStates = false; // Folia - region threading
|
|
+ if (enuminteractionresult.consumesAction() && worldData.captureTreeGeneration && worldData.capturedBlockStates.size() > 0) { // Folia - region threading
|
|
+ world.getCurrentWorldData().captureTreeGeneration = false; // Folia - region threading
|
|
Location location = new Location(world.getWorld(), blockposition.getX(), blockposition.getY(), blockposition.getZ());
|
|
- TreeType treeType = SaplingBlock.treeType;
|
|
- SaplingBlock.treeType = null;
|
|
- List<CraftBlockState> blocks = new java.util.ArrayList<>(world.capturedBlockStates.values());
|
|
- world.capturedBlockStates.clear();
|
|
+ TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading
|
|
+ SaplingBlock.treeTypeRT.set(null); // Folia - region threading
|
|
+ List<CraftBlockState> 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;
|
|
@@ -393,12 +395,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<BlockState> blocks = new java.util.ArrayList<>(world.capturedBlockStates.values());
|
|
- world.capturedBlockStates.clear();
|
|
+ List<BlockState> 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
|
|
@@ -409,13 +411,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();
|
|
@@ -430,7 +432,7 @@ public final class ItemStack {
|
|
this.setCount(newCount);
|
|
}
|
|
|
|
- for (Map.Entry<BlockPos, BlockEntity> e : world.capturedTileEntities.entrySet()) {
|
|
+ for (Map.Entry<BlockPos, BlockEntity> e : worldData.capturedTileEntities.entrySet()) { // Folia - region threading
|
|
world.setBlockEntity(e.getValue());
|
|
}
|
|
|
|
@@ -504,8 +506,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<MaterialColor> 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<? extends Player> getLocalPlayers() {
|
|
+ return java.util.Collections.emptyList();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
List<? extends Player> players();
|
|
|
|
default List<Entity> 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<org.bukkit.entity.HumanEntity> findNearbyBukkitPlayers(double x, double y, double z, double radius, @Nullable Predicate<Entity> predicate) {
|
|
com.google.common.collect.ImmutableList.Builder<org.bukkit.entity.HumanEntity> 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<Player> getNearbyPlayers(TargetingConditions targetPredicate, LivingEntity entity, AABB box) {
|
|
List<Player> list = Lists.newArrayList();
|
|
|
|
- for(Player player : this.players()) {
|
|
+ for(Player player : this.getLocalPlayers()) { // Folia - region threading
|
|
if (box.contains(player.getX(), player.getY(), player.getZ()) && targetPredicate.test(entity, player)) {
|
|
list.add(player);
|
|
}
|
|
@@ -234,8 +240,7 @@ public interface EntityGetter {
|
|
|
|
@Nullable
|
|
default Player getPlayerByUUID(UUID uuid) {
|
|
- for(int i = 0; i < this.players().size(); ++i) {
|
|
- Player player = this.players().get(i);
|
|
+ for(Player player : this.getLocalPlayers()) { // Folia - region threading
|
|
if (uuid.equals(player.getUUID())) {
|
|
return player;
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java
|
|
index 59837144c2c0460aca6e8c349eb3d6528111d1dc..7f32d5d5b709e8bb0395ccbeada2322c6e89cea3 100644
|
|
--- a/src/main/java/net/minecraft/world/level/Explosion.java
|
|
+++ b/src/main/java/net/minecraft/world/level/Explosion.java
|
|
@@ -246,7 +246,7 @@ public class Explosion {
|
|
continue;
|
|
}
|
|
|
|
- CraftEventFactory.entityDamage = this.source;
|
|
+ CraftEventFactory.entityDamageRT.set(this.source); // Folia - region threading
|
|
entity.lastDamageCancelled = false;
|
|
|
|
if (entity instanceof EnderDragon) {
|
|
@@ -262,7 +262,7 @@ public class Explosion {
|
|
entity.hurt(this.getDamageSource(), (float) ((int) ((d13 * d13 + d13) / 2.0D * 7.0D * (double) f2 + 1.0D)));
|
|
}
|
|
|
|
- CraftEventFactory.entityDamage = null;
|
|
+ CraftEventFactory.entityDamageRT.set(null); // Folia - region threading
|
|
if (entity.lastDamageCancelled) { // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Skip entity if damage event was cancelled
|
|
continue;
|
|
}
|
|
@@ -515,17 +515,10 @@ public class Explosion {
|
|
}
|
|
// Paper start - Optimize explosions
|
|
private float getBlockDensity(Vec3 vec3d, Entity entity) {
|
|
- if (!this.level.paperConfig().environment.optimizeExplosions) {
|
|
+ if (true || !this.level.paperConfig().environment.optimizeExplosions) { // Folia - region threading
|
|
return getSeenPercent(vec3d, entity);
|
|
}
|
|
- CacheKey key = new CacheKey(this, entity.getBoundingBox());
|
|
- Float blockDensity = this.level.explosionDensityCache.get(key);
|
|
- if (blockDensity == null) {
|
|
- blockDensity = getSeenPercent(vec3d, entity);
|
|
- this.level.explosionDensityCache.put(key, blockDensity);
|
|
- }
|
|
-
|
|
- return blockDensity;
|
|
+ return 0.0f; // Folia - region threading
|
|
}
|
|
|
|
static class CacheKey {
|
|
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
|
|
index 944da18bcc993ab0488a34cbbe9df134c355301a..c274f80de7184db0a89722ee2dd6d1a5af9b3c0a 100644
|
|
--- a/src/main/java/net/minecraft/world/level/Level.java
|
|
+++ b/src/main/java/net/minecraft/world/level/Level.java
|
|
@@ -118,10 +118,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
public static final int TICKS_PER_DAY = 24000;
|
|
public static final int MAX_ENTITY_SPAWN_Y = 20000000;
|
|
public static final int MIN_ENTITY_SPAWN_Y = -20000000;
|
|
- protected final List<TickingBlockEntity> blockEntityTickers = Lists.newArrayList(); public final int getTotalTileEntityTickers() { return this.blockEntityTickers.size(); } // Paper
|
|
- protected final NeighborUpdater neighborUpdater;
|
|
- private final List<TickingBlockEntity> pendingBlockEntityTickers = Lists.newArrayList();
|
|
- private boolean tickingBlockEntities;
|
|
+ //protected final List<TickingBlockEntity> 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<TickingBlockEntity> pendingBlockEntityTickers = Lists.newArrayList(); // Folia - region threading
|
|
+ //private boolean tickingBlockEntities; // Folia - region threading
|
|
public final Thread thread;
|
|
private final boolean isDebug;
|
|
private int skyDarken;
|
|
@@ -131,7 +131,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
public float rainLevel;
|
|
protected float oThunderLevel;
|
|
public float thunderLevel;
|
|
- public final RandomSource random = RandomSource.create();
|
|
+ public final RandomSource random = new Entity.RandomRandomSource(); // Folia - region threading
|
|
/** @deprecated */
|
|
@Deprecated
|
|
private final RandomSource threadSafeRandom = RandomSource.createThreadSafe();
|
|
@@ -145,7 +145,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
private final ResourceKey<Level> dimension;
|
|
private final RegistryAccess registryAccess;
|
|
private final DamageSources damageSources;
|
|
- private long subTickCount;
|
|
+ private final java.util.concurrent.atomic.AtomicLong subTickCount = new java.util.concurrent.atomic.AtomicLong(); //private long subTickCount; // Folia - region threading
|
|
|
|
// CraftBukkit start Added the following
|
|
private final CraftWorld world;
|
|
@@ -154,20 +154,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
public org.bukkit.generator.ChunkGenerator generator;
|
|
public static final boolean DEBUG_ENTITIES = Boolean.getBoolean("debug.entities"); // Paper
|
|
|
|
- public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710
|
|
- public boolean captureBlockStates = false;
|
|
- public boolean captureTreeGeneration = false;
|
|
- public Map<BlockPos, org.bukkit.craftbukkit.block.CraftBlockState> capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper
|
|
- public Map<BlockPos, BlockEntity> capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper
|
|
- public List<ItemEntity> captureDrops;
|
|
+ // Folia - region threading - moved to regionised data
|
|
public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<SpawnCategory> ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>();
|
|
- // Paper start
|
|
- public int wakeupInactiveRemainingAnimals;
|
|
- public int wakeupInactiveRemainingFlying;
|
|
- public int wakeupInactiveRemainingMonsters;
|
|
- public int wakeupInactiveRemainingVillagers;
|
|
- // Paper end
|
|
- public boolean populating;
|
|
+ // Folia - region threading - moved to regionised data
|
|
+ // Folia - region threading
|
|
public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot
|
|
// Paper start
|
|
private final io.papermc.paper.configuration.WorldConfiguration paperConfig;
|
|
@@ -181,9 +171,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
public static BlockPos lastPhysicsProblem; // Spigot
|
|
private org.spigotmc.TickLimiter entityLimiter;
|
|
private org.spigotmc.TickLimiter tileLimiter;
|
|
- private int tileTickPosition;
|
|
- public final Map<Explosion.CacheKey, Float> explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions
|
|
- public java.util.ArrayDeque<net.minecraft.world.level.block.RedstoneTorchBlock.Toggle> redstoneUpdateInfos; // Paper - Move from Map in BlockRedstoneTorch to here
|
|
+ //private int tileTickPosition; // Folia - region threading
|
|
+ //public final Map<Explosion.CacheKey, Float> explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions // Folia - region threading
|
|
+ //public java.util.ArrayDeque<net.minecraft.world.level.block.RedstoneTorchBlock.Toggle> redstoneUpdateInfos; // Paper - Move from Map in BlockRedstoneTorch to here // Folia - region threading
|
|
|
|
// Paper start - fix and optimise world upgrading
|
|
// copied from below
|
|
@@ -227,7 +217,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
List<net.minecraft.server.level.ServerPlayer> ret = new java.util.ArrayList<>();
|
|
double maxRangeSquared = maxRange * maxRange;
|
|
|
|
- for (net.minecraft.server.level.ServerPlayer player : (List<net.minecraft.server.level.ServerPlayer>)this.players()) {
|
|
+ for (net.minecraft.server.level.ServerPlayer player : (List<net.minecraft.server.level.ServerPlayer>)this.getLocalPlayers()) { // Folia - region threading
|
|
if ((maxRange < 0.0 || player.distanceToSqr(sourceX, sourceY, sourceZ) < maxRangeSquared)) {
|
|
if (predicate == null || predicate.test(player)) {
|
|
ret.add(player);
|
|
@@ -243,7 +233,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
net.minecraft.server.level.ServerPlayer closest = null;
|
|
double closestRangeSquared = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange;
|
|
|
|
- for (net.minecraft.server.level.ServerPlayer player : (List<net.minecraft.server.level.ServerPlayer>)this.players()) {
|
|
+ for (net.minecraft.server.level.ServerPlayer player : (List<net.minecraft.server.level.ServerPlayer>)this.getLocalPlayers()) { // Folia - region threading
|
|
double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ);
|
|
if (distanceSquared < closestRangeSquared && (predicate == null || predicate.test(player))) {
|
|
closest = player;
|
|
@@ -274,6 +264,33 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
|
|
public abstract ResourceKey<LevelStem> getTypeKey();
|
|
|
|
+ // Folia start - region ticking
|
|
+ public final io.papermc.paper.threadedregions.RegionizedData<io.papermc.paper.threadedregions.RegionizedWorldData> worldRegionData
|
|
+ = new io.papermc.paper.threadedregions.RegionizedData<>(
|
|
+ (ServerLevel)this, () -> new io.papermc.paper.threadedregions.RegionizedWorldData((ServerLevel)Level.this),
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData.REGION_CALLBACK
|
|
+ );
|
|
+ public volatile io.papermc.paper.threadedregions.RegionizedServer.WorldLevelData tickData;
|
|
+ public final java.util.concurrent.ConcurrentHashMap.KeySetView<ChunkHolder, Boolean> needsChangeBroadcasting = java.util.concurrent.ConcurrentHashMap.newKeySet();
|
|
+
|
|
+ public io.papermc.paper.threadedregions.RegionizedWorldData getCurrentWorldData() {
|
|
+ final io.papermc.paper.threadedregions.RegionizedWorldData ret = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData();
|
|
+ if (ret == null) {
|
|
+ return ret;
|
|
+ }
|
|
+ Level world = ret.world;
|
|
+ if (world != this) {
|
|
+ throw new IllegalStateException("World mismatch: expected " + this.getWorld().getName() + " but got " + world.getWorld().getName());
|
|
+ }
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public List<net.minecraft.server.level.ServerPlayer> getLocalPlayers() {
|
|
+ return this.getCurrentWorldData().getLocalPlayers();
|
|
+ }
|
|
+ // Folia end - region ticking
|
|
+
|
|
protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, Supplier<ProfilerFiller> 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<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - Async-Anti-Xray - Pass executor
|
|
this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot
|
|
this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper
|
|
@@ -317,7 +334,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
this.thread = Thread.currentThread();
|
|
this.biomeManager = new BiomeManager(this, i);
|
|
this.isDebug = flag1;
|
|
- this.neighborUpdater = new CollectingNeighborUpdater(this, j);
|
|
+ this.neighbourUpdateMax = j; // Folia - region threading
|
|
this.registryAccess = iregistrycustom;
|
|
this.damageSources = new DamageSources(iregistrycustom);
|
|
// CraftBukkit start
|
|
@@ -458,8 +475,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
@Nullable
|
|
public final BlockState getBlockStateIfLoaded(BlockPos pos) {
|
|
// CraftBukkit start - tree generation
|
|
- if (this.captureTreeGeneration) {
|
|
- CraftBlockState previous = this.capturedBlockStates.get(pos);
|
|
+ if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading
|
|
+ CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Folia - region threading
|
|
if (previous != null) {
|
|
return previous.getHandle();
|
|
}
|
|
@@ -521,16 +538,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
|
|
@Override
|
|
public boolean setBlock(BlockPos pos, BlockState state, int flags, int maxUpdateDepth) {
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); // Folia - region threading
|
|
// CraftBukkit start - tree generation
|
|
- if (this.captureTreeGeneration) {
|
|
+ if (worldData.captureTreeGeneration) { // Folia - region threading
|
|
// Paper start
|
|
BlockState type = getBlockState(pos);
|
|
if (!type.isDestroyable()) return false;
|
|
// Paper end
|
|
- CraftBlockState blockstate = this.capturedBlockStates.get(pos);
|
|
+ CraftBlockState blockstate = worldData.capturedBlockStates.get(pos); // Folia - region threading
|
|
if (blockstate == null) {
|
|
blockstate = CapturedBlockState.getTreeBlockState(this, pos, flags);
|
|
- this.capturedBlockStates.put(pos.immutable(), blockstate);
|
|
+ worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading
|
|
}
|
|
blockstate.setFlag(flags); // Paper - update the flag also
|
|
blockstate.setData(state);
|
|
@@ -547,10 +565,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
|
|
// CraftBukkit start - capture blockstates
|
|
boolean captured = false;
|
|
- if (this.captureBlockStates && !this.capturedBlockStates.containsKey(pos)) {
|
|
+ if (worldData.captureBlockStates && !worldData.capturedBlockStates.containsKey(pos)) { // Folia - region threading
|
|
CraftBlockState blockstate = (CraftBlockState) world.getBlockAt(pos.getX(), pos.getY(), pos.getZ()).getState(); // Paper - use CB getState to get a suitable snapshot
|
|
blockstate.setFlag(flags); // Paper - set flag
|
|
- this.capturedBlockStates.put(pos.immutable(), blockstate);
|
|
+ worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading
|
|
captured = true;
|
|
}
|
|
// CraftBukkit end
|
|
@@ -560,8 +578,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
|
|
if (iblockdata1 == null) {
|
|
// CraftBukkit start - remove blockstate if failed (or the same)
|
|
- if (this.captureBlockStates && captured) {
|
|
- this.capturedBlockStates.remove(pos);
|
|
+ if (worldData.captureBlockStates && captured) { // Folia - region threading
|
|
+ worldData.capturedBlockStates.remove(pos); // Folia - region threading
|
|
}
|
|
// CraftBukkit end
|
|
return false;
|
|
@@ -604,7 +622,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
*/
|
|
|
|
// CraftBukkit start
|
|
- if (!this.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates
|
|
+ if (!worldData.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates // Folia - region threading
|
|
// Modularize client and physic updates
|
|
// Spigot start
|
|
try {
|
|
@@ -653,7 +671,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
// CraftBukkit start
|
|
iblockdata1.updateIndirectNeighbourShapes(this, blockposition, k, j - 1); // Don't call an event for the old block to limit event spam
|
|
CraftWorld world = ((ServerLevel) this).getWorld();
|
|
- if (world != null && ((ServerLevel)this).hasPhysicsEvent) { // Paper
|
|
+ if (world != null && ((ServerLevel)this).getCurrentWorldData().hasPhysicsEvent) { // Paper // Folia - region threading
|
|
BlockPhysicsEvent event = new BlockPhysicsEvent(world.getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()), CraftBlockData.fromData(iblockdata));
|
|
this.getCraftServer().getPluginManager().callEvent(event);
|
|
|
|
@@ -667,7 +685,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
}
|
|
|
|
// CraftBukkit start - SPIGOT-5710
|
|
- if (!this.preventPoiUpdated) {
|
|
+ if (!this.getCurrentWorldData().preventPoiUpdated) { // Folia - region threading
|
|
this.onBlockStateChange(blockposition, iblockdata1, iblockdata2);
|
|
}
|
|
// CraftBukkit end
|
|
@@ -746,7 +764,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
|
|
@Override
|
|
public void neighborShapeChanged(Direction direction, BlockState neighborState, BlockPos pos, BlockPos neighborPos, int flags, int maxUpdateDepth) {
|
|
- this.neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, maxUpdateDepth);
|
|
+ this.getCurrentWorldData().neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, maxUpdateDepth); // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
@@ -771,11 +789,34 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
return this.getChunkSource().getLightEngine();
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ @Nullable
|
|
+ public BlockState getBlockStateFromEmptyChunkIfLoaded(BlockPos pos) {
|
|
+ net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource();
|
|
+ ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4);
|
|
+ if (chunk != null) {
|
|
+ return chunk.getBlockState(pos);
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ @Nullable
|
|
+ public BlockState getBlockStateFromEmptyChunk(BlockPos pos) {
|
|
+ net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource();
|
|
+ ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4);
|
|
+ if (chunk != null) {
|
|
+ return chunk.getBlockState(pos);
|
|
+ }
|
|
+ chunk = chunkProvider.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.EMPTY, true);
|
|
+ return chunk.getBlockState(pos);
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Override
|
|
public BlockState getBlockState(BlockPos pos) {
|
|
// CraftBukkit start - tree generation
|
|
- if (this.captureTreeGeneration) {
|
|
- CraftBlockState previous = this.capturedBlockStates.get(pos); // Paper
|
|
+ if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading
|
|
+ CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Paper // Folia - region threading
|
|
if (previous != null) {
|
|
return previous.getHandle();
|
|
}
|
|
@@ -866,7 +907,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
}
|
|
|
|
public void addBlockEntityTicker(TickingBlockEntity ticker) {
|
|
- (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker);
|
|
+ ((ServerLevel)this).getCurrentWorldData().addBlockEntityTicker(ticker); // Folia - regionised ticking
|
|
}
|
|
|
|
protected void tickBlockEntities() {
|
|
@@ -874,11 +915,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
|
|
gameprofilerfiller.push("blockEntities");
|
|
timings.tileEntityPending.startTiming(); // Spigot
|
|
- this.tickingBlockEntities = true;
|
|
- if (!this.pendingBlockEntityTickers.isEmpty()) {
|
|
- this.blockEntityTickers.addAll(this.pendingBlockEntityTickers);
|
|
- this.pendingBlockEntityTickers.clear();
|
|
- }
|
|
+ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = ((ServerLevel)this).getCurrentWorldData(); // Folia - regionised ticking
|
|
+ regionizedWorldData.seTtickingBlockEntities(true); // Folia - regionised ticking
|
|
+ regionizedWorldData.pushPendingTickingBlockEntities(); // Folia - regionised ticking
|
|
+ List<TickingBlockEntity> blockEntityTickers = regionizedWorldData.getBlockEntityTickers(); // Folia - regionised ticking
|
|
timings.tileEntityPending.stopTiming(); // Spigot
|
|
|
|
timings.tileEntityTick.startTiming(); // Spigot
|
|
@@ -887,9 +927,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
int tilesThisCycle = 0;
|
|
var toRemove = new it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet<TickingBlockEntity>(net.minecraft.Util.identityStrategy()); // Paper - use removeAll
|
|
toRemove.add(null);
|
|
- for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters
|
|
- this.tileTickPosition = (this.tileTickPosition < this.blockEntityTickers.size()) ? this.tileTickPosition : 0;
|
|
- TickingBlockEntity tickingblockentity = (TickingBlockEntity) this.blockEntityTickers.get(tileTickPosition);
|
|
+ for (int i = 0; i < blockEntityTickers.size(); i++) { // Paper - Disable tick limiters // Folia - regionised ticking
|
|
+ TickingBlockEntity tickingblockentity = (TickingBlockEntity) blockEntityTickers.get(i); // Folia - regionised ticking
|
|
// Spigot start
|
|
if (tickingblockentity == null) {
|
|
this.getCraftServer().getLogger().severe("Spigot has detected a null entity and has removed it, preventing a crash");
|
|
@@ -906,19 +945,19 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
} else if (this.shouldTickBlocksAt(tickingblockentity.getPos())) {
|
|
tickingblockentity.tick();
|
|
// Paper start - execute chunk tasks during tick
|
|
- if ((this.tileTickPosition & 7) == 0) {
|
|
+ if ((i & 7) == 0) { // Folia - regionised ticking
|
|
MinecraftServer.getServer().executeMidTickTasks();
|
|
}
|
|
// Paper end - execute chunk tasks during tick
|
|
}
|
|
}
|
|
- this.blockEntityTickers.removeAll(toRemove);
|
|
+ blockEntityTickers.removeAll(toRemove); // Folia - regionised ticking
|
|
|
|
timings.tileEntityTick.stopTiming(); // Spigot
|
|
- this.tickingBlockEntities = false;
|
|
- co.aikar.timings.TimingHistory.tileEntityTicks += this.blockEntityTickers.size(); // Paper
|
|
+ regionizedWorldData.seTtickingBlockEntities(false); // Folia - regionised ticking
|
|
+ //co.aikar.timings.TimingHistory.tileEntityTicks += this.blockEntityTickers.size(); // Paper // Folia - region threading
|
|
gameprofilerfiller.pop();
|
|
- spigotConfig.currentPrimedTnt = 0; // Spigot
|
|
+ regionizedWorldData.currentPrimedTnt = 0; // Spigot // Folia - region threading
|
|
}
|
|
|
|
public <T extends Entity> void guardEntityTick(Consumer<T> tickConsumer, T entity) {
|
|
@@ -1014,9 +1053,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
|
|
@Nullable
|
|
public BlockEntity getBlockEntity(BlockPos blockposition, boolean validate) {
|
|
+ // Folia start - region threading
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThread()) {
|
|
+ return null;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
// Paper start - Optimize capturedTileEntities lookup
|
|
net.minecraft.world.level.block.entity.BlockEntity blockEntity;
|
|
- if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(blockposition)) != null) {
|
|
+ if (!this.getCurrentWorldData().capturedTileEntities.isEmpty() && (blockEntity = this.getCurrentWorldData().capturedTileEntities.get(blockposition)) != null) { // Folia - region threading
|
|
return blockEntity;
|
|
}
|
|
// Paper end
|
|
@@ -1029,8 +1073,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
|
|
@@ -1234,13 +1278,30 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
|
|
public void disconnect() {}
|
|
|
|
+ @Override // Folia - region threading
|
|
public long getGameTime() {
|
|
- return this.levelData.getGameTime();
|
|
+ // Dumb world gen thread calls this for some reason. So, check for null.
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData();
|
|
+ return worldData == null ? this.getLevelData().getGameTime() : worldData.getTickData().nonRedstoneGameTime();
|
|
}
|
|
|
|
public long getDayTime() {
|
|
- return this.levelData.getDayTime();
|
|
+ // Dumb world gen thread calls this for some reason. So, check for null.
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData();
|
|
+ return worldData == null ? this.getLevelData().getDayTime() : worldData.getTickData().dayTime();
|
|
+ }
|
|
+
|
|
+ // Folia start - region threading
|
|
+ @Override
|
|
+ public long dayTime() {
|
|
+ return this.getDayTime();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public long getRedstoneGameTime() {
|
|
+ return this.getCurrentWorldData().getRedstoneGameTime();
|
|
}
|
|
+ // Folia end - region threading
|
|
|
|
public boolean mayInteract(Player player, BlockPos pos) {
|
|
return true;
|
|
@@ -1442,8 +1503,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
}
|
|
public final BlockPos.MutableBlockPos getRandomBlockPosition(int x, int y, int z, int l, BlockPos.MutableBlockPos out) {
|
|
// Paper end
|
|
- this.randValue = this.randValue * 3 + 1013904223;
|
|
- int i1 = this.randValue >> 2;
|
|
+ int i1 = this.random.nextInt() >> 2; // Folia - region threading
|
|
|
|
out.set(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); // Paper - change to setValues call
|
|
return out; // Paper
|
|
@@ -1474,7 +1534,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
|
|
@Override
|
|
public long nextSubTickCount() {
|
|
- return (long) (this.subTickCount++);
|
|
+ return this.subTickCount.getAndIncrement(); // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
diff --git a/src/main/java/net/minecraft/world/level/LevelAccessor.java b/src/main/java/net/minecraft/world/level/LevelAccessor.java
|
|
index 73d1adc5ddf0363966eac0c77c8dfbbb20a2b6a3..375a2b57bcb29458443c1a4e2be3c0e5f4e6019c 100644
|
|
--- a/src/main/java/net/minecraft/world/level/LevelAccessor.java
|
|
+++ b/src/main/java/net/minecraft/world/level/LevelAccessor.java
|
|
@@ -35,12 +35,22 @@ public interface LevelAccessor extends CommonLevelAccessor, LevelTimeAccess {
|
|
|
|
LevelTickAccess<Block> getBlockTicks();
|
|
|
|
+ // Folia start - region threading
|
|
+ default long getGameTime() {
|
|
+ return this.getLevelData().getGameTime();
|
|
+ }
|
|
+
|
|
+ default long getRedstoneGameTime() {
|
|
+ return this.getLevelData().getGameTime();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
default <T> ScheduledTick<T> 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 <T> ScheduledTick<T> createTick(BlockPos pos, T type, int delay) { // CraftBukkit - decompile error
|
|
- return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + (long) delay, this.nextSubTickCount());
|
|
+ return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + (long) delay, this.nextSubTickCount()); // Folia - region threading
|
|
}
|
|
|
|
default void scheduleTick(BlockPos pos, Block block, int delay, TickPriority priority) {
|
|
diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java
|
|
index 7fe1b8856bf916796fa6d2a984f0a07a2331e23b..07802d0a25e49519c3c9b33c217e05002cf05e31 100644
|
|
--- a/src/main/java/net/minecraft/world/level/LevelReader.java
|
|
+++ b/src/main/java/net/minecraft/world/level/LevelReader.java
|
|
@@ -135,6 +135,15 @@ public interface LevelReader extends BlockAndTintGetter, CollisionGetter, BiomeM
|
|
return this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true);
|
|
}
|
|
|
|
+ // Folia start - region threaeding
|
|
+ default ChunkAccess syncLoadNonFull(int chunkX, int chunkZ, ChunkStatus status) {
|
|
+ if (status == null || status.isOrAfter(ChunkStatus.FULL)) {
|
|
+ throw new IllegalArgumentException("Status: " + status.getName());
|
|
+ }
|
|
+ return this.getChunk(chunkX, chunkZ, status, true);
|
|
+ }
|
|
+ // Folia end - region threaeding
|
|
+
|
|
default ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status) {
|
|
return this.getChunk(chunkX, chunkZ, status, true);
|
|
}
|
|
@@ -204,6 +213,25 @@ public interface LevelReader extends BlockAndTintGetter, CollisionGetter, BiomeM
|
|
return maxY >= this.getMinBuildHeight() && minY < this.getMaxBuildHeight() ? this.hasChunksAt(minX, minZ, maxX, maxZ) : false;
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ default boolean hasAndOwnsChunksAt(int minX, int minZ, int maxX, int maxZ) {
|
|
+ int i = SectionPos.blockToSectionCoord(minX);
|
|
+ int j = SectionPos.blockToSectionCoord(maxX);
|
|
+ int k = SectionPos.blockToSectionCoord(minZ);
|
|
+ int l = SectionPos.blockToSectionCoord(maxZ);
|
|
+
|
|
+ for(int m = i; m <= j; ++m) {
|
|
+ for(int n = k; n <= l; ++n) {
|
|
+ if (!this.hasChunk(m, n) || (this instanceof net.minecraft.server.level.ServerLevel world && !io.papermc.paper.util.TickThread.isTickThreadFor(world, m, n))) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
/** @deprecated */
|
|
@Deprecated
|
|
default boolean hasChunksAt(int minX, int minZ, int maxX, int maxZ) {
|
|
diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
|
|
index 15d266fc97eb73338f4f6fb2cfe25d6861e79810..52a7d5ef4a25bee618d9c9a784df95a2341fe015 100644
|
|
--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java
|
|
+++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
|
|
@@ -115,11 +115,7 @@ public final class NaturalSpawner {
|
|
}
|
|
|
|
object2intopenhashmap.addTo(enumcreaturetype, 1);
|
|
- // Paper start
|
|
- if (countMobs) {
|
|
- chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity);
|
|
- }
|
|
- // Paper end
|
|
+ // Folia - rewrite chunk system - revert per player mob caps
|
|
});
|
|
}
|
|
}
|
|
@@ -146,7 +142,7 @@ public final class NaturalSpawner {
|
|
int limit = enumcreaturetype.getMaxInstancesPerChunk();
|
|
SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(enumcreaturetype);
|
|
if (CraftSpawnCategory.isValidForLimits(spawnCategory)) {
|
|
- spawnThisTick = world.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worlddata.getGameTime() % world.ticksPerSpawnCategory.getLong(spawnCategory) == 0;
|
|
+ spawnThisTick = world.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && world.getRedstoneGameTime() % world.ticksPerSpawnCategory.getLong(spawnCategory) == 0; // Folia - region threading
|
|
limit = world.getWorld().getSpawnLimit(spawnCategory);
|
|
}
|
|
|
|
@@ -154,37 +150,13 @@ public final class NaturalSpawner {
|
|
continue;
|
|
}
|
|
|
|
- // Paper start - only allow spawns upto the limit per chunk and update count afterwards
|
|
- int currEntityCount = info.mobCategoryCounts.getInt(enumcreaturetype);
|
|
- int k1 = limit * info.getSpawnableChunkCount() / NaturalSpawner.MAGIC_NUMBER;
|
|
- int difference = k1 - currEntityCount;
|
|
-
|
|
- if (world.paperConfig().entities.spawning.perPlayerMobSpawns) {
|
|
- int minDiff = Integer.MAX_VALUE;
|
|
- final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> inRange = world.getChunkSource().chunkMap.playerMobDistanceMap.getObjectsInRange(chunk.getPos());
|
|
- if (inRange != null) {
|
|
- final Object[] backingSet = inRange.getBackingSet();
|
|
- for (int k = 0; k < backingSet.length; k++) {
|
|
- if (!(backingSet[k] instanceof final net.minecraft.server.level.ServerPlayer player)) {
|
|
- continue;
|
|
- }
|
|
- minDiff = Math.min(limit - world.getChunkSource().chunkMap.getMobCountNear(player, enumcreaturetype), minDiff);
|
|
- }
|
|
- }
|
|
- difference = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff;
|
|
- }
|
|
- if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rareSpawn || !enumcreaturetype.isPersistent()) && difference > 0) {
|
|
- // Paper end
|
|
+ if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rareSpawn || !enumcreaturetype.isPersistent()) && info.canSpawnForCategory(enumcreaturetype, chunk.getPos(), limit)) { // Folia - region threading - revert per player mob caps
|
|
// CraftBukkit end
|
|
Objects.requireNonNull(info);
|
|
NaturalSpawner.SpawnPredicate spawnercreature_c = info::canSpawn;
|
|
|
|
Objects.requireNonNull(info);
|
|
- // Paper start
|
|
- int spawnCount = NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn,
|
|
- difference, world.paperConfig().entities.spawning.perPlayerMobSpawns ? world.getChunkSource().chunkMap::updatePlayerMobTypeMap : null);
|
|
- info.mobCategoryCounts.mergeInt(enumcreaturetype, spawnCount, Integer::sum);
|
|
- // Paper end
|
|
+ NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn); // Folia - region threading - revert per player mob caps
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java b/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java
|
|
index 3d377b9e461040405e0a7dcbd72d1506b48eb44e..782890e227ff9dab44dd92327979c201985f116e 100644
|
|
--- a/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java
|
|
+++ b/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java
|
|
@@ -7,6 +7,12 @@ public interface ServerLevelAccessor extends LevelAccessor {
|
|
|
|
ServerLevel getLevel();
|
|
|
|
+ // Folia start - region threading
|
|
+ default public StructureManager structureManager() {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
default void addFreshEntityWithPassengers(Entity entity) {
|
|
// CraftBukkit start
|
|
this.addFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
|
|
diff --git a/src/main/java/net/minecraft/world/level/StructureManager.java b/src/main/java/net/minecraft/world/level/StructureManager.java
|
|
index 09c85ed428b8eaf51f8b3c6e45cce925f05ab354..3d797880b5964dd07f3757495ea1255a034aad89 100644
|
|
--- a/src/main/java/net/minecraft/world/level/StructureManager.java
|
|
+++ b/src/main/java/net/minecraft/world/level/StructureManager.java
|
|
@@ -44,11 +44,8 @@ public class StructureManager {
|
|
}
|
|
|
|
public List<StructureStart> startsForStructure(ChunkPos pos, Predicate<Structure> predicate) {
|
|
- // Paper start
|
|
- return this.startsForStructure(pos, predicate, null);
|
|
- }
|
|
- public List<StructureStart> startsForStructure(ChunkPos pos, Predicate<Structure> predicate, @Nullable ServerLevelAccessor levelAccessor) {
|
|
- Map<Structure, LongSet> map = (levelAccessor == null ? this.level : levelAccessor).getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences();
|
|
+ // Folia - region threading
|
|
+ Map<Structure, LongSet> map = this.level.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences();
|
|
// Paper end
|
|
ImmutableList.Builder<StructureStart> builder = ImmutableList.builder();
|
|
|
|
@@ -113,18 +110,14 @@ public class StructureManager {
|
|
}
|
|
|
|
public StructureStart getStructureWithPieceAt(BlockPos pos, TagKey<Structure> structureTag) {
|
|
- // Paper start
|
|
- return this.getStructureWithPieceAt(pos, structureTag, null);
|
|
- }
|
|
- public StructureStart getStructureWithPieceAt(BlockPos pos, TagKey<Structure> structureTag, @Nullable ServerLevelAccessor levelAccessor) {
|
|
- // Paper end
|
|
+ // Folia - region threading
|
|
Registry<Structure> registry = this.registryAccess().registryOrThrow(Registries.STRUCTURE);
|
|
|
|
for(StructureStart structureStart : this.startsForStructure(new ChunkPos(pos), (structure) -> {
|
|
return registry.getHolder(registry.getId(structure)).map((reference) -> {
|
|
return reference.is(structureTag);
|
|
}).orElse(false);
|
|
- }, levelAccessor)) { // Paper
|
|
+ })) { // Paper // Folia - region threading
|
|
if (this.structureHasPieceAt(pos, structureStart)) {
|
|
return structureStart;
|
|
}
|
|
@@ -168,7 +161,7 @@ public class StructureManager {
|
|
}
|
|
|
|
public void addReference(StructureStart structureStart) {
|
|
- structureStart.addReference();
|
|
+ //structureStart.addReference(); // Folia - region threading - move to caller
|
|
this.structureCheck.incrementReference(structureStart.getChunkPos(), structureStart.getStructure());
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java
|
|
index 4f91e4832a94c3facbc711fcae4cb5ad540a5ca0..e5ca38b375becfb3a10fb94739bdaed354c426c6 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/Block.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/Block.java
|
|
@@ -394,8 +394,8 @@ public class Block extends BlockBehaviour implements ItemLike {
|
|
|
|
entityitem.setDefaultPickUpDelay();
|
|
// CraftBukkit start
|
|
- if (world.captureDrops != null) {
|
|
- world.captureDrops.add(entityitem);
|
|
+ if (world.getCurrentWorldData().captureDrops != null) { // Folia - region threading
|
|
+ world.getCurrentWorldData().captureDrops.add(entityitem); // Folia - region threading
|
|
} else {
|
|
world.addFreshEntity(entityitem);
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/BushBlock.java b/src/main/java/net/minecraft/world/level/block/BushBlock.java
|
|
index 03fde6e47c4a347c62fe9b4a3351769aedf874f6..d2e3e1d20d60f5edd0d93709b808f812c31e7491 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/BushBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/BushBlock.java
|
|
@@ -24,7 +24,7 @@ public class BushBlock extends Block {
|
|
public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) {
|
|
// CraftBukkit start
|
|
if (!state.canSurvive(world, pos)) {
|
|
- if (!(world instanceof net.minecraft.server.level.ServerLevel && ((net.minecraft.server.level.ServerLevel) world).hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper
|
|
+ if (!(world instanceof net.minecraft.server.level.ServerLevel && ((net.minecraft.server.level.ServerLevel) world).getCurrentWorldData().hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper // Folia - region threading
|
|
return Blocks.AIR.defaultBlockState();
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/CactusBlock.java b/src/main/java/net/minecraft/world/level/block/CactusBlock.java
|
|
index 7579946ce222b6ab3685a7fd9821bcd5a4babe33..08f0360e12087cdce0671a0f3f782b62a75bac47 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/CactusBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/CactusBlock.java
|
|
@@ -117,9 +117,9 @@ public class CactusBlock extends Block {
|
|
@Override
|
|
public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
|
|
if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper
|
|
- CraftEventFactory.blockDamage = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()); // CraftBukkit
|
|
+ CraftEventFactory.blockDamageRT.set(world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ())); // CraftBukkit // Folia - region threading
|
|
entity.hurt(world.damageSources().cactus(), 1.0F);
|
|
- CraftEventFactory.blockDamage = null; // CraftBukkit
|
|
+ CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java
|
|
index 219c87dcf065e86512f330fbeec59e55f4675083..90e56394554b2b6d5dd12fa7f1f5279dc9a38478 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java
|
|
@@ -94,9 +94,9 @@ public class CampfireBlock extends BaseEntityBlock implements SimpleWaterloggedB
|
|
public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
|
|
if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper
|
|
if ((Boolean) state.getValue(CampfireBlock.LIT) && entity instanceof LivingEntity && !EnchantmentHelper.hasFrostWalker((LivingEntity) entity)) {
|
|
- org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = CraftBlock.at(world, pos); // CraftBukkit
|
|
+ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // CraftBukkit // Folia - region threading
|
|
entity.hurt(world.damageSources().inFire(), (float) this.fireDamage);
|
|
- org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = null; // CraftBukkit
|
|
+ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Folia - region threading
|
|
}
|
|
|
|
super.entityInside(state, world, pos, entity);
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java b/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java
|
|
index 81376e725151f723dad8a7b5c1a4bd597e60294e..a9876256edd5354015e83d943a83d1c0e4060d4e 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java
|
|
@@ -113,7 +113,7 @@ public class DaylightDetectorBlock extends BaseEntityBlock {
|
|
}
|
|
|
|
private static void tickEntity(Level world, BlockPos pos, BlockState state, DaylightDetectorBlockEntity blockEntity) {
|
|
- if (world.getGameTime() % 20L == 0L) {
|
|
+ if (world.getRedstoneGameTime() % 20L == 0L) { // Folia - region threading
|
|
DaylightDetectorBlock.updateSignalStrength(state, world, pos);
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/DispenserBlock.java b/src/main/java/net/minecraft/world/level/block/DispenserBlock.java
|
|
index 8f55d0753fa26924235c943595f0d1a06a933a6f..a195d37847e3278d7721641b5db858913c0afe46 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/DispenserBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/DispenserBlock.java
|
|
@@ -47,7 +47,7 @@ public class DispenserBlock extends BaseEntityBlock {
|
|
object2objectopenhashmap.defaultReturnValue(new DefaultDispenseItemBehavior());
|
|
});
|
|
private static final int TRIGGER_DURATION = 4;
|
|
- public static boolean eventFired = false; // CraftBukkit
|
|
+ public static ThreadLocal<Boolean> eventFired = ThreadLocal.withInitial(() -> Boolean.FALSE); // CraftBukkit // Folia - region threading
|
|
|
|
public static void registerBehavior(ItemLike provider, DispenseItemBehavior behavior) {
|
|
DispenserBlock.DISPENSER_REGISTRY.put(provider.asItem(), behavior);
|
|
@@ -94,7 +94,7 @@ public class DispenserBlock extends BaseEntityBlock {
|
|
|
|
if (idispensebehavior != DispenseItemBehavior.NOOP) {
|
|
if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockPreDispenseEvent(world, pos, itemstack, i)) return; // Paper - BlockPreDispenseEvent is called here
|
|
- DispenserBlock.eventFired = false; // CraftBukkit - reset event status
|
|
+ DispenserBlock.eventFired.set(false); // CraftBukkit - reset event status // Folia - region threading
|
|
tileentitydispenser.setItem(i, idispensebehavior.dispense(sourceblock, itemstack));
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java b/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java
|
|
index 9db66b393e057d93a8025b803ae0ad2a1bca61f6..bace9a699aa6aeaec417434730a7ca6dd9983946 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java
|
|
@@ -95,7 +95,7 @@ public class DoublePlantBlock extends BushBlock {
|
|
|
|
protected static void preventCreativeDropFromBottomPart(Level world, BlockPos pos, BlockState state, Player player) {
|
|
// CraftBukkit start
|
|
- if (((net.minecraft.server.level.ServerLevel)world).hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper
|
|
+ if (((net.minecraft.server.level.ServerLevel)world).getCurrentWorldData().hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper // Folia - region threading
|
|
return;
|
|
}
|
|
// CraftBukkit end
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/FungusBlock.java b/src/main/java/net/minecraft/world/level/block/FungusBlock.java
|
|
index 9d0fc6b5b8d8fb31cacf7e8b346b9babf1a3e8a2..85701eba8dea71c64f92d5eaf99cb232bf2d9722 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/FungusBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/FungusBlock.java
|
|
@@ -61,9 +61,9 @@ public class FungusBlock extends BushBlock implements BonemealableBlock {
|
|
this.getFeature(world).ifPresent((holder) -> {
|
|
// CraftBukkit start
|
|
if (this == Blocks.WARPED_FUNGUS) {
|
|
- SaplingBlock.treeType = org.bukkit.TreeType.WARPED_FUNGUS;
|
|
+ SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.WARPED_FUNGUS); // Folia - region threading
|
|
} else if (this == Blocks.CRIMSON_FUNGUS) {
|
|
- SaplingBlock.treeType = org.bukkit.TreeType.CRIMSON_FUNGUS;
|
|
+ SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.CRIMSON_FUNGUS); // Folia - region threading
|
|
}
|
|
// CraftBukkit end
|
|
((ConfiguredFeature) holder.value()).place(world, world.getChunkSource().getGenerator(), random, pos);
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/HoneyBlock.java b/src/main/java/net/minecraft/world/level/block/HoneyBlock.java
|
|
index 29efe50dd3911a1d7ef5175034e82191130b8fd1..83810585125403a8ed1f85ff3dfc0f2d6ec40c75 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/HoneyBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/HoneyBlock.java
|
|
@@ -80,7 +80,7 @@ public class HoneyBlock extends HalfTransparentBlock {
|
|
}
|
|
|
|
private void maybeDoSlideAchievement(Entity entity, BlockPos pos) {
|
|
- if (entity instanceof ServerPlayer && entity.level.getGameTime() % 20L == 0L) {
|
|
+ if (entity instanceof ServerPlayer && entity.level.getRedstoneGameTime() % 20L == 0L) { // Folia - region threading
|
|
CriteriaTriggers.HONEY_BLOCK_SLIDE.trigger((ServerPlayer)entity, entity.level.getBlockState(pos));
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java b/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java
|
|
index da3b301a42a93c891d083a6e02d1be8ed35adf1d..f354981843868bf938be0b5ac1ef2ce39fb067ef 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java
|
|
@@ -112,7 +112,7 @@ public class LightningRodBlock extends RodBlock implements SimpleWaterloggedBloc
|
|
|
|
@Override
|
|
public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) {
|
|
- if (world.isThundering() && (long) world.random.nextInt(200) <= world.getGameTime() % 200L && pos.getY() == world.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1) {
|
|
+ if (world.isThundering() && (long) world.random.nextInt(200) <= world.getRedstoneGameTime() % 200L && pos.getY() == world.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1) { // Folia - region threading
|
|
ParticleUtils.spawnParticlesAlongAxis(((Direction) state.getValue(LightningRodBlock.FACING)).getAxis(), world, pos, 0.125D, ParticleTypes.ELECTRIC_SPARK, UniformInt.of(1, 2));
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java
|
|
index 12ffb5714f088f4aeafa1ad6a36f5b64a86c4c96..57eceff3fd313449c6db02c7719ad9f74ed15b17 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java
|
|
@@ -28,9 +28,9 @@ public class MagmaBlock extends Block {
|
|
@Override
|
|
public void stepOn(Level world, BlockPos pos, BlockState state, Entity entity) {
|
|
if (!entity.isSteppingCarefully() && entity instanceof LivingEntity && !EnchantmentHelper.hasFrostWalker((LivingEntity) entity)) {
|
|
- org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()); // CraftBukkit
|
|
+ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ())); // CraftBukkit // Folia - region threading
|
|
entity.hurt(world.damageSources().hotFloor(), 1.0F);
|
|
- org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = null; // CraftBukkit
|
|
+ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Folia - region threading
|
|
}
|
|
|
|
super.stepOn(world, pos, state, entity);
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/MushroomBlock.java b/src/main/java/net/minecraft/world/level/block/MushroomBlock.java
|
|
index f6f8e155223cba10c4073ddca602d1aa3aa872d7..bc42faf2f5cf54197849b1ad133a88510bfea09d 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/MushroomBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/MushroomBlock.java
|
|
@@ -92,7 +92,7 @@ public class MushroomBlock extends BushBlock implements BonemealableBlock {
|
|
return false;
|
|
} else {
|
|
world.removeBlock(pos, false);
|
|
- SaplingBlock.treeType = (this == Blocks.BROWN_MUSHROOM) ? TreeType.BROWN_MUSHROOM : TreeType.RED_MUSHROOM; // CraftBukkit // Paper
|
|
+ SaplingBlock.treeTypeRT.set((this == Blocks.BROWN_MUSHROOM) ? TreeType.BROWN_MUSHROOM : TreeType.RED_MUSHROOM); // CraftBukkit // Paper // Folia - region threading
|
|
if (((ConfiguredFeature) ((Holder) optional.get()).value()).place(world, world.getChunkSource().getGenerator(), random, pos)) {
|
|
return true;
|
|
} else {
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java
|
|
index 6b909d41ccdf6c1ac3ac0c4e673ff52f0d14a238..e7079e164f5943f21aff487c22c525f7bf98911f 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java
|
|
@@ -143,9 +143,9 @@ public class PointedDripstoneBlock extends Block implements Fallable, SimpleWate
|
|
@Override
|
|
public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) {
|
|
if (state.getValue(PointedDripstoneBlock.TIP_DIRECTION) == Direction.UP && state.getValue(PointedDripstoneBlock.THICKNESS) == DripstoneThickness.TIP) {
|
|
- CraftEventFactory.blockDamage = CraftBlock.at(world, pos); // CraftBukkit
|
|
+ CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // CraftBukkit // Folia - region threading
|
|
entity.causeFallDamage(fallDistance + 2.0F, 2.0F, world.damageSources().stalagmite());
|
|
- CraftEventFactory.blockDamage = null; // CraftBukkit
|
|
+ CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Folia - region threading
|
|
} else {
|
|
super.fallOn(world, state, pos, entity, fallDistance);
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java
|
|
index 5ea09cc455bd86beb450f0e0275d7c6c8da98084..cc019091a2decbc1e5c760129778b42ea1d449d6 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java
|
|
@@ -67,7 +67,7 @@ public class RedStoneWireBlock extends Block {
|
|
});
|
|
private static final float PARTICLE_DENSITY = 0.2F;
|
|
private final BlockState crossState;
|
|
- public boolean shouldSignal = true;
|
|
+ //public boolean shouldSignal = true; // Folia - region threading - move to regionised world data
|
|
|
|
public RedStoneWireBlock(BlockBehaviour.Properties settings) {
|
|
super(settings);
|
|
@@ -262,7 +262,7 @@ public class RedStoneWireBlock extends Block {
|
|
* Note: Added 'source' argument so as to help determine direction of information flow
|
|
*/
|
|
private void updateSurroundingRedstone(Level worldIn, BlockPos pos, BlockState state, BlockPos source) {
|
|
- if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT) {
|
|
+ if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT) { // Folia - region threading
|
|
turbo.updateSurroundingRedstone(worldIn, pos, state, source);
|
|
return;
|
|
}
|
|
@@ -282,11 +282,11 @@ public class RedStoneWireBlock extends Block {
|
|
int i = state.getValue(POWER);
|
|
int j = 0;
|
|
j = this.getPower(j, worldIn.getBlockState(pos2));
|
|
- this.shouldSignal = false;
|
|
+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading
|
|
int k = worldIn.getBestNeighborSignal(pos1);
|
|
- this.shouldSignal = true;
|
|
+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading
|
|
|
|
- if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) {
|
|
+ if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { // Folia - region threading
|
|
// This code is totally redundant to if statements just below the loop.
|
|
if (k > 0 && k > j - 1) {
|
|
j = k;
|
|
@@ -300,7 +300,7 @@ public class RedStoneWireBlock extends Block {
|
|
// redstone wire will be set to 'k'. If 'k' is already 15, then nothing inside the
|
|
// following loop can affect the power level of the wire. Therefore, the loop is
|
|
// skipped if k is already 15.
|
|
- if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA || k < 15) {
|
|
+ if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA || k < 15) { // Folia - region threading
|
|
for (Direction enumfacing : Direction.Plane.HORIZONTAL) {
|
|
BlockPos blockpos = pos1.relative(enumfacing);
|
|
boolean flag = blockpos.getX() != pos2.getX() || blockpos.getZ() != pos2.getZ();
|
|
@@ -319,7 +319,7 @@ public class RedStoneWireBlock extends Block {
|
|
}
|
|
}
|
|
|
|
- if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) {
|
|
+ if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { // Folia - region threading
|
|
// The old code would decrement the wire value only by 1 at a time.
|
|
if (l > j) {
|
|
j = l - 1;
|
|
@@ -403,10 +403,10 @@ public class RedStoneWireBlock extends Block {
|
|
}
|
|
|
|
private int calculateTargetStrength(Level world, BlockPos pos) {
|
|
- this.shouldSignal = false;
|
|
+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading
|
|
int i = world.getBestNeighborSignal(pos);
|
|
|
|
- this.shouldSignal = true;
|
|
+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading
|
|
int j = 0;
|
|
|
|
if (i < 15) {
|
|
@@ -455,7 +455,7 @@ public class RedStoneWireBlock extends Block {
|
|
public void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) {
|
|
if (!oldState.is(state.getBlock()) && !world.isClientSide) {
|
|
// Paper start - optimize redstone - replace call to updatePowerStrength
|
|
- if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) {
|
|
+ if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { // Folia - region threading
|
|
world.getWireHandler().onWireAdded(pos); // Alternate Current
|
|
} else {
|
|
this.updateSurroundingRedstone(world, pos, state, null); // vanilla/Eigencraft
|
|
@@ -488,7 +488,7 @@ public class RedStoneWireBlock extends Block {
|
|
}
|
|
|
|
// Paper start - optimize redstone - replace call to updatePowerStrength
|
|
- if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) {
|
|
+ if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { // Folia - region threading
|
|
world.getWireHandler().onWireRemoved(pos, state); // Alternate Current
|
|
} else {
|
|
this.updateSurroundingRedstone(world, pos, state, null); // vanilla/Eigencraft
|
|
@@ -529,7 +529,7 @@ public class RedStoneWireBlock extends Block {
|
|
if (!world.isClientSide) {
|
|
// Paper start - optimize redstone (Alternate Current)
|
|
// Alternate Current handles breaking of redstone wires in the WireHandler.
|
|
- if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) {
|
|
+ if (io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { // Folia - region threading
|
|
world.getWireHandler().onWireUpdated(pos);
|
|
} else
|
|
// Paper end
|
|
@@ -545,12 +545,12 @@ public class RedStoneWireBlock extends Block {
|
|
|
|
@Override
|
|
public int getDirectSignal(BlockState state, BlockGetter world, BlockPos pos, Direction direction) {
|
|
- return !this.shouldSignal ? 0 : state.getSignal(world, pos, direction);
|
|
+ return !io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal ? 0 : state.getSignal(world, pos, direction); // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public int getSignal(BlockState state, BlockGetter world, BlockPos pos, Direction direction) {
|
|
- if (this.shouldSignal && direction != Direction.DOWN) {
|
|
+ if (io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal && direction != Direction.DOWN) { // Folia - region threading
|
|
int i = (Integer) state.getValue(RedStoneWireBlock.POWER);
|
|
|
|
return i == 0 ? 0 : (direction != Direction.UP && !((RedstoneSide) this.getConnectionState(world, state, pos).getValue((Property) RedStoneWireBlock.PROPERTY_BY_DIRECTION.get(direction.getOpposite()))).isConnected() ? 0 : i);
|
|
@@ -577,7 +577,7 @@ public class RedStoneWireBlock extends Block {
|
|
|
|
@Override
|
|
public boolean isSignalSource(BlockState state) {
|
|
- return this.shouldSignal;
|
|
+ return io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal; // Folia - region threading
|
|
}
|
|
|
|
public static int getColorForPower(int powerLevel) {
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java b/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java
|
|
index da07fce0cf7c9fbdb57d2c59e431b59bf583bf50..16e46bb6205c3f7444e864c553e8072f0519746d 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java
|
|
@@ -73,10 +73,10 @@ public class RedstoneTorchBlock extends TorchBlock {
|
|
public void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
|
|
boolean flag = this.hasNeighborSignal(world, pos, state);
|
|
// Paper start
|
|
- java.util.ArrayDeque<RedstoneTorchBlock.Toggle> redstoneUpdateInfos = world.redstoneUpdateInfos;
|
|
+ java.util.ArrayDeque<RedstoneTorchBlock.Toggle> 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<RedstoneTorchBlock.Toggle> list = world.redstoneUpdateInfos;
|
|
+ java.util.ArrayDeque<RedstoneTorchBlock.Toggle> list = world.getCurrentWorldData().redstoneUpdateInfos; // Folia - region threading
|
|
if (list == null) {
|
|
- list = world.redstoneUpdateInfos = new java.util.ArrayDeque<>();
|
|
+ list = world.getCurrentWorldData().redstoneUpdateInfos = new java.util.ArrayDeque<>(); // Folia - region threading
|
|
}
|
|
|
|
|
|
if (addNew) {
|
|
- list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), world.getGameTime()));
|
|
+ list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), world.getRedstoneGameTime())); // Folia - region threading
|
|
}
|
|
|
|
int i = 0;
|
|
@@ -185,12 +185,18 @@ public class RedstoneTorchBlock extends TorchBlock {
|
|
|
|
public static class Toggle {
|
|
|
|
- final BlockPos pos;
|
|
- final long when;
|
|
+ public final BlockPos pos; // Folia - region threading
|
|
+ long when; // Folia - region ticking
|
|
|
|
public Toggle(BlockPos pos, long time) {
|
|
this.pos = pos;
|
|
this.when = time;
|
|
}
|
|
+
|
|
+ // Folia start - region ticking
|
|
+ public void offsetTime(long offset) {
|
|
+ this.when += offset;
|
|
+ }
|
|
+ // Folia end - region ticking
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/SaplingBlock.java b/src/main/java/net/minecraft/world/level/block/SaplingBlock.java
|
|
index 901978a338f0f1b6f20ffb65aac59704bfa6f36a..4ec92ebdfd803c181cc51a71ba910ed34643a578 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/SaplingBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/SaplingBlock.java
|
|
@@ -26,7 +26,7 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock {
|
|
protected static final float AABB_OFFSET = 6.0F;
|
|
protected static final VoxelShape SHAPE = Block.box(2.0D, 0.0D, 2.0D, 14.0D, 12.0D, 14.0D);
|
|
private final AbstractTreeGrower treeGrower;
|
|
- public static TreeType treeType; // CraftBukkit
|
|
+ public static final ThreadLocal<TreeType> treeTypeRT = new ThreadLocal<>(); // CraftBukkit // Folia - region threading
|
|
|
|
protected SaplingBlock(AbstractTreeGrower generator, BlockBehaviour.Properties settings) {
|
|
super(settings);
|
|
@@ -52,18 +52,19 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock {
|
|
world.setBlock(pos, (net.minecraft.world.level.block.state.BlockState) state.cycle(SaplingBlock.STAGE), 4);
|
|
} else {
|
|
// CraftBukkit start
|
|
- if (world.captureTreeGeneration) {
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
+ if (worldData.captureTreeGeneration) { // Folia - region threading
|
|
this.treeGrower.growTree(world, world.getChunkSource().getGenerator(), pos, state, random);
|
|
} else {
|
|
- world.captureTreeGeneration = true;
|
|
+ worldData.captureTreeGeneration = true; // Folia - region threading
|
|
this.treeGrower.growTree(world, world.getChunkSource().getGenerator(), pos, state, random);
|
|
- world.captureTreeGeneration = false;
|
|
- if (world.capturedBlockStates.size() > 0) {
|
|
- TreeType treeType = SaplingBlock.treeType;
|
|
- SaplingBlock.treeType = null;
|
|
+ worldData.captureTreeGeneration = false; // Folia - region threading
|
|
+ if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading
|
|
+ TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading
|
|
+ SaplingBlock.treeTypeRT.set(null); // Folia - region threading
|
|
Location location = new Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ());
|
|
- java.util.List<BlockState> blocks = new java.util.ArrayList<>(world.capturedBlockStates.values());
|
|
- world.capturedBlockStates.clear();
|
|
+ java.util.List<BlockState> blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading
|
|
+ worldData.capturedBlockStates.clear(); // Folia - region threading
|
|
StructureGrowEvent event = null;
|
|
if (treeType != null) {
|
|
event = new StructureGrowEvent(location, treeType, false, null, blocks);
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
|
|
index af46c05a34292d271fd4a809398e6b299e10b12b..2eeb9352bc35a31efe98be51746014afd7e5e71c 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
|
|
@@ -51,7 +51,7 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock {
|
|
|
|
@Override
|
|
public void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
|
|
- if (this instanceof GrassBlock && world.paperConfig().tickRates.grassSpread != 1 && (world.paperConfig().tickRates.grassSpread < 1 || (MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper
|
|
+ if (this instanceof GrassBlock && world.paperConfig().tickRates.grassSpread != 1 && (world.paperConfig().tickRates.grassSpread < 1 || (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + pos.hashCode()) % world.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper // Folia - regionised ticking
|
|
// Paper start
|
|
net.minecraft.world.level.chunk.ChunkAccess cachedBlockChunk = world.getChunkIfLoaded(pos);
|
|
if (cachedBlockChunk == null) { // Is this needed?
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java b/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java
|
|
index 08a11888133b97e52535cb49cad218a1b6c6ac97..9d9564d603d5b05e60dc48d85039c384157a0dad 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java
|
|
@@ -85,9 +85,9 @@ public class SweetBerryBushBlock extends BushBlock implements BonemealableBlock
|
|
double d1 = Math.abs(entity.getZ() - entity.zOld);
|
|
|
|
if (d0 >= 0.003000000026077032D || d1 >= 0.003000000026077032D) {
|
|
- CraftEventFactory.blockDamage = CraftBlock.at(world, pos); // CraftBukkit
|
|
+ CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // CraftBukkit // Folia - region threading
|
|
entity.hurt(world.damageSources().sweetBerryBush(), 1.0F);
|
|
- CraftEventFactory.blockDamage = null; // CraftBukkit
|
|
+ CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // Folia - region threading
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java
|
|
index b91effe91dad2e1aeea0ea31140f7432833b343f..46979b4ee8c24b499577aa64167c759664148240 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java
|
|
@@ -53,7 +53,7 @@ public class WitherSkullBlock extends SkullBlock {
|
|
}
|
|
|
|
public static void checkSpawn(Level world, BlockPos pos, SkullBlockEntity blockEntity) {
|
|
- if (world.captureBlockStates) return; // CraftBukkit
|
|
+ if (world.getCurrentWorldData().captureBlockStates) return; // CraftBukkit // Folia - region threading
|
|
if (!world.isClientSide) {
|
|
BlockState iblockdata = blockEntity.getBlockState();
|
|
boolean flag = iblockdata.is(Blocks.WITHER_SKELETON_SKULL) || iblockdata.is(Blocks.WITHER_SKELETON_WALL_SKULL);
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java
|
|
index ef740d1ad6352ca4af299001a081b720bc472d2e..cf41ff83b1465b89e96d029b72530cdba10f3d99 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java
|
|
@@ -202,7 +202,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name
|
|
}
|
|
|
|
i1 = blockEntity.levels;
|
|
- if (world.getGameTime() % 80L == 0L) {
|
|
+ if (world.getRedstoneGameTime() % 80L == 0L) { // Folia - region threading
|
|
if (!blockEntity.beamSections.isEmpty()) {
|
|
blockEntity.levels = BeaconBlockEntity.updateBase(world, i, j, k);
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java
|
|
index 1b248db497500aa6bd346b306dcb908af77626f3..ac1f6d5c78c1970b3242c017031679fb9a906fb0 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java
|
|
@@ -26,7 +26,7 @@ import co.aikar.timings.MinecraftTimings; // Paper
|
|
import co.aikar.timings.Timing; // Paper
|
|
|
|
public abstract class BlockEntity {
|
|
- static boolean ignoreTileUpdates; // Paper
|
|
+ static final ThreadLocal<Boolean> IGNORE_TILE_UPDATES = ThreadLocal.withInitial(() -> Boolean.FALSE); // Paper // Folia - region threading
|
|
|
|
public Timing tickTimer = MinecraftTimings.getTileEntityTimings(this); // Paper
|
|
// CraftBukkit start - data containers
|
|
@@ -42,6 +42,12 @@ public abstract class BlockEntity {
|
|
protected boolean remove;
|
|
private BlockState blockState;
|
|
|
|
+ // Folia start - region ticking
|
|
+ public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) {
|
|
+
|
|
+ }
|
|
+ // Folia end - region ticking
|
|
+
|
|
public BlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
|
|
this.type = type;
|
|
this.worldPosition = pos.immutable();
|
|
@@ -163,7 +169,7 @@ public abstract class BlockEntity {
|
|
|
|
public void setChanged() {
|
|
if (this.level != null) {
|
|
- if (ignoreTileUpdates) return; // Paper
|
|
+ if (IGNORE_TILE_UPDATES.get()) return; // Paper // Folia - region threading
|
|
BlockEntity.setChanged(this.level, this.worldPosition, this.blockState);
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java
|
|
index c57efcb9a79337ec791e4e8f6671612f0a82b441..526d1bfd5ad0de7bcfd0c2da902515f3dec94c54 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java
|
|
@@ -56,7 +56,7 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements
|
|
public int fuel;
|
|
protected final ContainerData dataAccess;
|
|
// CraftBukkit start - add fields and methods
|
|
- private int lastTick = MinecraftServer.currentTick;
|
|
+ //private int lastTick = MinecraftServer.currentTick; // Folia - region ticking - restore original timers
|
|
public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
|
|
private int maxStack = 64;
|
|
|
|
@@ -173,11 +173,10 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements
|
|
ItemStack itemstack1 = (ItemStack) blockEntity.items.get(3);
|
|
|
|
// CraftBukkit start - Use wall time instead of ticks for brewing
|
|
- int elapsedTicks = MinecraftServer.currentTick - blockEntity.lastTick;
|
|
- blockEntity.lastTick = MinecraftServer.currentTick;
|
|
+ // Folia - region ticking - restore original timers
|
|
|
|
if (flag1) {
|
|
- blockEntity.brewTime -= elapsedTicks;
|
|
+ --blockEntity.brewTime; // Folia - region ticking - restore original timers
|
|
boolean flag2 = blockEntity.brewTime <= 0; // == -> <=
|
|
// CraftBukkit end
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java
|
|
index 963a596154091b79ca139af6274aa323518ad1ad..57b11cb78270a8094f772da497ad3264a0a67db1 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java
|
|
@@ -87,7 +87,7 @@ public class ConduitBlockEntity extends BlockEntity {
|
|
|
|
public static void clientTick(Level world, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) {
|
|
++blockEntity.tickCount;
|
|
- long i = world.getGameTime();
|
|
+ long i = world.getRedstoneGameTime(); // Folia - region threading
|
|
List<BlockPos> list = blockEntity.effectBlocks;
|
|
|
|
if (i % 40L == 0L) {
|
|
@@ -105,7 +105,7 @@ public class ConduitBlockEntity extends BlockEntity {
|
|
|
|
public static void serverTick(Level world, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) {
|
|
++blockEntity.tickCount;
|
|
- long i = world.getGameTime();
|
|
+ long i = world.getRedstoneGameTime(); // Folia - region threading
|
|
List<BlockPos> list = blockEntity.effectBlocks;
|
|
|
|
if (i % 40L == 0L) {
|
|
@@ -235,11 +235,11 @@ public class ConduitBlockEntity extends BlockEntity {
|
|
|
|
if (blockEntity.destroyTarget != null) {
|
|
// CraftBukkit start
|
|
- CraftEventFactory.blockDamage = CraftBlock.at(world, pos);
|
|
+ CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // Folia - region threading
|
|
if (blockEntity.destroyTarget.hurt(world.damageSources().magic(), 4.0F)) {
|
|
world.playSound((Player) null, blockEntity.destroyTarget.getX(), blockEntity.destroyTarget.getY(), blockEntity.destroyTarget.getZ(), SoundEvents.CONDUIT_ATTACK_TARGET, SoundSource.BLOCKS, 1.0F, 1.0F);
|
|
}
|
|
- CraftEventFactory.blockDamage = null;
|
|
+ CraftEventFactory.blockDamageRT.set(null); // Folia - region threading
|
|
// CraftBukkit end
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java
|
|
index cba114f554644a37339c93026630c66c43f524b9..0134370081182260d578ee1d8a16d544c03b6a0d 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java
|
|
@@ -194,12 +194,11 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
|
|
}
|
|
|
|
// Paper start - Optimize Hoppers
|
|
- private static boolean skipPullModeEventFire;
|
|
- private static boolean skipPushModeEventFire;
|
|
- public static boolean skipHopperEvents;
|
|
+ // Folia - region threading - moved to RegionizedWorldData
|
|
|
|
private static boolean hopperPush(final Level level, final Container destination, final Direction direction, final HopperBlockEntity hopper) {
|
|
- skipPushModeEventFire = skipHopperEvents;
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading
|
|
+ worldData.skipPushModeEventFire = worldData.skipHopperEvents; // Folia - region threading
|
|
boolean foundItem = false;
|
|
for (int i = 0; i < hopper.getContainerSize(); ++i) {
|
|
final ItemStack item = hopper.getItem(i);
|
|
@@ -214,7 +213,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
|
|
|
|
// We only need to fire the event once to give protection plugins a chance to cancel this event
|
|
// Because nothing uses getItem, every event call should end up the same result.
|
|
- if (!skipPushModeEventFire) {
|
|
+ if (!worldData.skipPushModeEventFire) { // Folia - region threading
|
|
movedItem = callPushMoveEvent(destination, movedItem, hopper);
|
|
if (movedItem == null) { // cancelled
|
|
origItemStack.setCount(originalItemCount);
|
|
@@ -244,12 +243,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
|
|
}
|
|
|
|
private static boolean hopperPull(final Level level, final Hopper hopper, final Container container, ItemStack origItemStack, final int i) {
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading
|
|
ItemStack movedItem = origItemStack;
|
|
final int originalItemCount = origItemStack.getCount();
|
|
final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount);
|
|
movedItem.setCount(movedItemCount);
|
|
|
|
- if (!skipPullModeEventFire) {
|
|
+ if (!worldData.skipPullModeEventFire) { // Folia - region threading
|
|
movedItem = callPullMoveEvent(hopper, container, movedItem);
|
|
if (movedItem == null) { // cancelled
|
|
origItemStack.setCount(originalItemCount);
|
|
@@ -269,9 +269,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
|
|
origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount);
|
|
}
|
|
|
|
- ignoreTileUpdates = true;
|
|
+ IGNORE_TILE_UPDATES.set(true); // Folia - region threading
|
|
container.setItem(i, origItemStack);
|
|
- ignoreTileUpdates = false;
|
|
+ IGNORE_TILE_UPDATES.set(false); // Folia - region threading
|
|
container.setChanged();
|
|
return true;
|
|
}
|
|
@@ -286,12 +286,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
|
|
|
|
@Nullable
|
|
private static ItemStack callPushMoveEvent(Container iinventory, ItemStack itemstack, HopperBlockEntity hopper) {
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading
|
|
final Inventory destinationInventory = getInventory(iinventory);
|
|
final InventoryMoveItemEvent event = new InventoryMoveItemEvent(hopper.getOwner(false).getInventory(),
|
|
CraftItemStack.asCraftMirror(itemstack), destinationInventory, true);
|
|
final boolean result = event.callEvent();
|
|
if (!event.calledGetItem && !event.calledSetItem) {
|
|
- skipPushModeEventFire = true;
|
|
+ worldData.skipPushModeEventFire = true; // Folia - region threading
|
|
}
|
|
if (!result) {
|
|
cooldownHopper(hopper);
|
|
@@ -307,6 +308,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
|
|
|
|
@Nullable
|
|
private static ItemStack callPullMoveEvent(final Hopper hopper, final Container container, final ItemStack itemstack) {
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading
|
|
final Inventory sourceInventory = getInventory(container);
|
|
final Inventory destination = getInventory(hopper);
|
|
|
|
@@ -314,7 +316,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
|
|
final InventoryMoveItemEvent event = new InventoryMoveItemEvent(sourceInventory, CraftItemStack.asCraftMirror(itemstack), destination, false);
|
|
final boolean result = event.callEvent();
|
|
if (!event.calledGetItem && !event.calledSetItem) {
|
|
- skipPullModeEventFire = true;
|
|
+ worldData.skipPullModeEventFire = true; // Folia - region threading
|
|
}
|
|
if (!result) {
|
|
cooldownHopper(hopper);
|
|
@@ -459,13 +461,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
|
|
}
|
|
|
|
public static boolean suckInItems(Level world, Hopper hopper) {
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading
|
|
Container iinventory = HopperBlockEntity.getSourceContainer(world, hopper);
|
|
|
|
if (iinventory != null) {
|
|
Direction enumdirection = Direction.DOWN;
|
|
|
|
// Paper start - optimize hoppers and remove streams
|
|
- skipPullModeEventFire = skipHopperEvents;
|
|
+ worldData.skipPullModeEventFire = worldData.skipHopperEvents; // Folia - region threading
|
|
return !HopperBlockEntity.isEmptyContainer(iinventory, enumdirection) && anyMatch(iinventory, enumdirection, (item, i) -> {
|
|
// Logic copied from below to avoid extra getItem calls
|
|
if (!item.isEmpty() && canTakeItemFromContainer(hopper, iinventory, item, i, enumdirection)) {
|
|
@@ -646,9 +649,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
|
|
stack = stack.split(to.getMaxStackSize());
|
|
}
|
|
// Spigot end
|
|
- ignoreTileUpdates = true; // Paper
|
|
+ IGNORE_TILE_UPDATES.set(true); // Paper // Folia - region threading
|
|
to.setItem(slot, stack);
|
|
- ignoreTileUpdates = false; // Paper
|
|
+ IGNORE_TILE_UPDATES.set(false); // Paper // Folia - region threading
|
|
stack = leftover; // Paper
|
|
flag = true;
|
|
} else if (HopperBlockEntity.canMergeItems(itemstack1, stack)) {
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java
|
|
index 902f2b39104bf059849228829bfe93b6dbc757d4..9a6c51652bd2dbb3d474809372df03038e34ce75 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java
|
|
@@ -86,9 +86,9 @@ public class SculkCatalystBlockEntity extends BlockEntity implements GameEventLi
|
|
}
|
|
|
|
public static void serverTick(Level world, BlockPos pos, BlockState state, SculkCatalystBlockEntity blockEntity) {
|
|
- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = blockEntity.getBlockPos(); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep.
|
|
+ org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(blockEntity.getBlockPos()); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // Folia - region threading
|
|
blockEntity.sculkSpreader.updateCursors(world, pos, world.getRandom(), true);
|
|
- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = null; // CraftBukkit
|
|
+ org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(null); // CraftBukkit // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java
|
|
index c73024cc62490c336ffe26313580e88d25ca7078..ce8557aa7b208c2ea85b361b68306fcb5f420369 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java
|
|
@@ -51,9 +51,12 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity {
|
|
public long age;
|
|
private int teleportCooldown;
|
|
@Nullable
|
|
- public BlockPos exitPortal;
|
|
+ public volatile BlockPos exitPortal; // Folia - region threading - volatile
|
|
public boolean exactTeleport;
|
|
|
|
+ private static final java.util.concurrent.atomic.AtomicLong SEARCHING_FOR_EXIT_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); // Folia - region threading
|
|
+ private Long searchingForExitId; // Folia - region threading
|
|
+
|
|
public TheEndGatewayBlockEntity(BlockPos pos, BlockState state) {
|
|
super(BlockEntityType.END_GATEWAY, pos, state);
|
|
}
|
|
@@ -128,7 +131,7 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity {
|
|
}
|
|
|
|
public static boolean canEntityTeleport(Entity entity) {
|
|
- return EntitySelector.NO_SPECTATORS.test(entity) && !entity.getRootVehicle().isOnPortalCooldown();
|
|
+ return EntitySelector.NO_SPECTATORS.test(entity) && !entity.getRootVehicle().isOnPortalCooldown() && entity.canPortalAsync(true); // Folia - region threading - correct portal check
|
|
}
|
|
|
|
public boolean isSpawning() {
|
|
@@ -176,8 +179,112 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity {
|
|
}
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ private void trySearchForExit(ServerLevel world, BlockPos fromPos) {
|
|
+ if (this.searchingForExitId != null) {
|
|
+ return;
|
|
+ }
|
|
+ this.searchingForExitId = Long.valueOf(SEARCHING_FOR_EXIT_ID_GENERATOR.getAndIncrement());
|
|
+ int chunkX = fromPos.getX() >> 4;
|
|
+ int chunkZ = fromPos.getZ() >> 4;
|
|
+ world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel(
|
|
+ net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH,
|
|
+ chunkX, chunkZ,
|
|
+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL,
|
|
+ this.searchingForExitId
|
|
+ );
|
|
+
|
|
+ ca.spottedleaf.concurrentutil.completable.Completable<BlockPos> complete = new ca.spottedleaf.concurrentutil.completable.Completable<>();
|
|
+
|
|
+ complete.addWaiter((tpLoc, throwable) -> {
|
|
+ // create the exit portal
|
|
+ TheEndGatewayBlockEntity.LOGGER.debug("Creating portal at {}", tpLoc);
|
|
+ TheEndGatewayBlockEntity.spawnGatewayPortal(world, tpLoc, EndGatewayConfiguration.knownExit(fromPos, false));
|
|
+
|
|
+ // need to go onto the tick thread to avoid saving issues
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ world, chunkX, chunkZ,
|
|
+ () -> {
|
|
+ // update the exit portal location
|
|
+ TheEndGatewayBlockEntity.this.exitPortal = tpLoc;
|
|
+
|
|
+ // remove ticket keeping the gateway loaded
|
|
+ world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel(
|
|
+ net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH,
|
|
+ chunkX, chunkZ,
|
|
+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL,
|
|
+ this.searchingForExitId
|
|
+ );
|
|
+ TheEndGatewayBlockEntity.this.searchingForExitId = null;
|
|
+ }
|
|
+ );
|
|
+ });
|
|
+
|
|
+ findOrCreateValidTeleportPosRegionThreading(world, fromPos, complete);
|
|
+ }
|
|
+
|
|
+ private static void teleportRegionThreading(Level world, BlockPos pos, BlockState state, Entity entity, TheEndGatewayBlockEntity blockEntity) {
|
|
+ // can we even teleport in this dimension?
|
|
+ if (blockEntity.exitPortal == null && world.getTypeKey() != LevelStem.END) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ ServerLevel serverWorld = (ServerLevel)world;
|
|
+
|
|
+ // First, find the position we are trying to teleport to
|
|
+ BlockPos teleportPos = blockEntity.exitPortal;
|
|
+ boolean isExactTeleport = blockEntity.exactTeleport;
|
|
+
|
|
+ if (teleportPos == null) {
|
|
+ blockEntity.trySearchForExit(serverWorld, pos);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // This needs to be first, as we are only guaranteed to be on the corresponding region tick thread here
|
|
+ TheEndGatewayBlockEntity.triggerCooldown(world, pos, state, blockEntity);
|
|
+
|
|
+ if (isExactTeleport) {
|
|
+ // blind teleport
|
|
+ entity.teleportAsync(
|
|
+ serverWorld, Vec3.atCenterOf(teleportPos), null, null, null,
|
|
+ PlayerTeleportEvent.TeleportCause.END_GATEWAY, Entity.TELEPORT_FLAG_LOAD_CHUNK | Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS,
|
|
+ (Entity teleportedEntity) -> {
|
|
+ for (Entity passenger : teleportedEntity.getSelfAndPassengers().toList()) {
|
|
+ passenger.setPortalCooldown();
|
|
+ }
|
|
+ }
|
|
+ );
|
|
+ } else {
|
|
+ // we could hack around by first loading the chunks, then calling back to here and checking if the entity
|
|
+ // should be teleported, something something else...
|
|
+ // however, we know the target location cannot differ by one region section: so we can
|
|
+ // just teleport and adjust the position after
|
|
+ entity.teleportAsync(
|
|
+ serverWorld, Vec3.atCenterOf(teleportPos), null, null, null,
|
|
+ PlayerTeleportEvent.TeleportCause.END_GATEWAY, Entity.TELEPORT_FLAG_LOAD_CHUNK | Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS,
|
|
+ (Entity teleportedEntity) -> {
|
|
+ for (Entity passenger : teleportedEntity.getSelfAndPassengers().toList()) {
|
|
+ passenger.setPortalCooldown();
|
|
+ }
|
|
+
|
|
+ // adjust to the final exit position
|
|
+ Vec3 adjusted = Vec3.atCenterOf(TheEndGatewayBlockEntity.findExitPosition(serverWorld, teleportPos));
|
|
+ // teleportTo will adjust rider positions
|
|
+ teleportedEntity.teleportTo(adjusted.x, adjusted.y, adjusted.z);
|
|
+ }
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
public static void teleportEntity(Level world, BlockPos pos, BlockState state, Entity entity, TheEndGatewayBlockEntity blockEntity) {
|
|
if (world instanceof ServerLevel && !blockEntity.isCoolingDown()) {
|
|
+ // Folia start - region threading
|
|
+ if (true) {
|
|
+ teleportRegionThreading(world, pos, state, entity.getRootVehicle(), blockEntity);
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
ServerLevel worldserver = (ServerLevel) world;
|
|
|
|
blockEntity.teleportCooldown = 100;
|
|
@@ -281,6 +388,129 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity {
|
|
return TheEndGatewayBlockEntity.findTallestBlock(world, blockposition1, 16, true);
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ private static void findOrCreateValidTeleportPosRegionThreading(ServerLevel world, BlockPos pos,
|
|
+ ca.spottedleaf.concurrentutil.completable.Completable<BlockPos> complete) {
|
|
+ ca.spottedleaf.concurrentutil.completable.Completable<Vec3> tentativeSelection = new ca.spottedleaf.concurrentutil.completable.Completable<>();
|
|
+
|
|
+ tentativeSelection.addWaiter((vec3d, throwable) -> {
|
|
+ LevelChunk chunk = TheEndGatewayBlockEntity.getChunk(world, vec3d);
|
|
+ BlockPos blockposition1 = TheEndGatewayBlockEntity.findValidSpawnInChunk(chunk);
|
|
+ if (blockposition1 == null) {
|
|
+ BlockPos blockposition2 = new BlockPos(
|
|
+ io.papermc.paper.util.CoordinateUtils.getBlockCoordinate(vec3d.x + 0.5D),
|
|
+ io.papermc.paper.util.CoordinateUtils.getBlockCoordinate(75.0D),
|
|
+ io.papermc.paper.util.CoordinateUtils.getBlockCoordinate(vec3d.z + 0.5D)
|
|
+ );
|
|
+
|
|
+ TheEndGatewayBlockEntity.LOGGER.debug("Failed to find a suitable block to teleport to, spawning an island on {}", blockposition2);
|
|
+ world.registryAccess().registry(Registries.CONFIGURED_FEATURE).flatMap((iregistry) -> {
|
|
+ return iregistry.getHolder(EndFeatures.END_ISLAND);
|
|
+ }).ifPresent((holder_c) -> {
|
|
+ ((ConfiguredFeature) holder_c.value()).place(world, world.getChunkSource().getGenerator(), RandomSource.create(blockposition2.asLong()), blockposition2);
|
|
+ });
|
|
+ blockposition1 = blockposition2;
|
|
+ } else {
|
|
+ TheEndGatewayBlockEntity.LOGGER.debug("Found suitable block to teleport to: {}", blockposition1);
|
|
+ }
|
|
+
|
|
+ // Here, there is no guarantee the chunks in 1 radius are in this region due to the fact that we just chained
|
|
+ // possibly 16x chunk loads along an axis (findExitPortalXZPosTentativeRegionThreading) using the chunk queue
|
|
+ // (regioniser only guarantees at least 8 chunks along a single axis)
|
|
+ // so, we need to schedule for the next tick
|
|
+ int posX = blockposition1.getX();
|
|
+ int posZ = blockposition1.getZ();
|
|
+ int radius = 16;
|
|
+
|
|
+ BlockPos finalBlockPosition1 = blockposition1;
|
|
+ world.loadChunksAsync(blockposition1, radius,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL,
|
|
+ (List<net.minecraft.world.level.chunk.ChunkAccess> chunks) -> {
|
|
+ // make sure chunks are kept loaded
|
|
+ for (net.minecraft.world.level.chunk.ChunkAccess access : chunks) {
|
|
+ world.chunkSource.addTicketAtLevel(
|
|
+ net.minecraft.server.level.TicketType.DELAYED, access.getPos(),
|
|
+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL,
|
|
+ net.minecraft.util.Unit.INSTANCE
|
|
+ );
|
|
+ }
|
|
+ // now after the chunks are loaded, we can delay by one tick
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ world, posX >> 4, posZ >> 4, () -> {
|
|
+ // find final location
|
|
+ BlockPos tpLoc = TheEndGatewayBlockEntity.findTallestBlock(world, finalBlockPosition1, radius, true);
|
|
+
|
|
+ // done
|
|
+ complete.complete(tpLoc.above(10));
|
|
+ }
|
|
+ );
|
|
+ }
|
|
+ );
|
|
+ });
|
|
+
|
|
+ // fire off chain
|
|
+ findExitPortalXZPosTentativeRegionThreading(world, pos, tentativeSelection);
|
|
+ }
|
|
+
|
|
+ private static void findExitPortalXZPosTentativeRegionThreading(ServerLevel world, BlockPos pos,
|
|
+ ca.spottedleaf.concurrentutil.completable.Completable<Vec3> complete) {
|
|
+ Vec3 posDirFromOrigin = new Vec3(pos.getX(), 0.0D, pos.getZ()).normalize();
|
|
+ Vec3 posDirExtruded = posDirFromOrigin.scale(1024.0D);
|
|
+
|
|
+ class Vars {
|
|
+ int i = 16;
|
|
+ boolean mode = false;
|
|
+ Vec3 currPos = posDirExtruded;
|
|
+ }
|
|
+ Vars vars = new Vars();
|
|
+
|
|
+ Runnable handle = new Runnable() {
|
|
+ @Override
|
|
+ public void run() {
|
|
+ if (vars.mode != TheEndGatewayBlockEntity.isChunkEmpty(world, vars.currPos)) {
|
|
+ vars.i = 0; // fall back to completing
|
|
+ }
|
|
+
|
|
+ // try to load next chunk
|
|
+ if (vars.i-- <= 0) {
|
|
+ if (vars.mode) {
|
|
+ complete.complete(vars.currPos);
|
|
+ return;
|
|
+ }
|
|
+ vars.mode = true;
|
|
+ vars.i = 16;
|
|
+ }
|
|
+
|
|
+ vars.currPos = vars.currPos.add(posDirFromOrigin.scale(vars.mode ? 16.0 : -16.0));
|
|
+ // schedule next iteration
|
|
+ Runnable handleButInitialised = this;
|
|
+ world.chunkTaskScheduler.scheduleChunkLoad(
|
|
+ io.papermc.paper.util.CoordinateUtils.getChunkX(vars.currPos),
|
|
+ io.papermc.paper.util.CoordinateUtils.getChunkZ(vars.currPos),
|
|
+ net.minecraft.world.level.chunk.ChunkStatus.FULL,
|
|
+ true,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL,
|
|
+ (chunk) -> {
|
|
+ handleButInitialised.run();
|
|
+ }
|
|
+ );
|
|
+ }
|
|
+ };
|
|
+
|
|
+ // kick off first chunk load
|
|
+ world.chunkTaskScheduler.scheduleChunkLoad(
|
|
+ io.papermc.paper.util.CoordinateUtils.getChunkX(posDirExtruded),
|
|
+ io.papermc.paper.util.CoordinateUtils.getChunkZ(posDirExtruded),
|
|
+ net.minecraft.world.level.chunk.ChunkStatus.FULL,
|
|
+ true,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL,
|
|
+ (chunk) -> {
|
|
+ handle.run();
|
|
+ }
|
|
+ );
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
private static Vec3 findExitPortalXZPosTentative(ServerLevel world, BlockPos pos) {
|
|
Vec3 vec3d = (new Vec3((double) pos.getX(), 0.0D, (double) pos.getZ())).normalize();
|
|
boolean flag = true;
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java
|
|
index 28e3b73507b988f7234cbf29c4024c88180d0aef..c8facee29ee08e0975528083f89b64f0b593957f 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java
|
|
@@ -10,4 +10,6 @@ public interface TickingBlockEntity {
|
|
BlockPos getPos();
|
|
|
|
String getType();
|
|
+
|
|
+ BlockEntity getTileEntity(); // Folia - region threading
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/grower/AbstractTreeGrower.java b/src/main/java/net/minecraft/world/level/block/grower/AbstractTreeGrower.java
|
|
index a743f36f2682a6b72ffa6644782fc081d1479eb7..f5263a71d97f404c6f6dbd3354a5ed2e98bb71db 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/grower/AbstractTreeGrower.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/grower/AbstractTreeGrower.java
|
|
@@ -75,51 +75,53 @@ public abstract class AbstractTreeGrower {
|
|
// CraftBukkit start
|
|
protected void setTreeType(Holder<ConfiguredFeature<?, ?>> holder) {
|
|
ResourceKey<ConfiguredFeature<?, ?>> worldgentreeabstract = holder.unwrapKey().get();
|
|
+ TreeType treeType; // Folia - region threading
|
|
if (worldgentreeabstract == TreeFeatures.OAK || worldgentreeabstract == TreeFeatures.OAK_BEES_005) {
|
|
- SaplingBlock.treeType = TreeType.TREE;
|
|
+ treeType = TreeType.TREE; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.HUGE_RED_MUSHROOM) {
|
|
- SaplingBlock.treeType = TreeType.RED_MUSHROOM;
|
|
+ treeType = TreeType.RED_MUSHROOM; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.HUGE_BROWN_MUSHROOM) {
|
|
- SaplingBlock.treeType = TreeType.BROWN_MUSHROOM;
|
|
+ treeType = TreeType.BROWN_MUSHROOM; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.JUNGLE_TREE) {
|
|
- SaplingBlock.treeType = TreeType.COCOA_TREE;
|
|
+ treeType = TreeType.COCOA_TREE; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.JUNGLE_TREE_NO_VINE) {
|
|
- SaplingBlock.treeType = TreeType.SMALL_JUNGLE;
|
|
+ treeType = TreeType.SMALL_JUNGLE; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.PINE) {
|
|
- SaplingBlock.treeType = TreeType.TALL_REDWOOD;
|
|
+ treeType = TreeType.TALL_REDWOOD; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.SPRUCE) {
|
|
- SaplingBlock.treeType = TreeType.REDWOOD;
|
|
+ treeType = TreeType.REDWOOD; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.ACACIA) {
|
|
- SaplingBlock.treeType = TreeType.ACACIA;
|
|
+ treeType = TreeType.ACACIA; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.BIRCH || worldgentreeabstract == TreeFeatures.BIRCH_BEES_005) {
|
|
- SaplingBlock.treeType = TreeType.BIRCH;
|
|
+ treeType = TreeType.BIRCH; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.SUPER_BIRCH_BEES_0002) {
|
|
- SaplingBlock.treeType = TreeType.TALL_BIRCH;
|
|
+ treeType = TreeType.TALL_BIRCH; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.SWAMP_OAK) {
|
|
- SaplingBlock.treeType = TreeType.SWAMP;
|
|
+ treeType = TreeType.SWAMP; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.FANCY_OAK || worldgentreeabstract == TreeFeatures.FANCY_OAK_BEES_005) {
|
|
- SaplingBlock.treeType = TreeType.BIG_TREE;
|
|
+ treeType = TreeType.BIG_TREE; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.JUNGLE_BUSH) {
|
|
- SaplingBlock.treeType = TreeType.JUNGLE_BUSH;
|
|
+ treeType = TreeType.JUNGLE_BUSH; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.DARK_OAK) {
|
|
- SaplingBlock.treeType = TreeType.DARK_OAK;
|
|
+ treeType = TreeType.DARK_OAK; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.MEGA_SPRUCE) {
|
|
- SaplingBlock.treeType = TreeType.MEGA_REDWOOD;
|
|
+ treeType = TreeType.MEGA_REDWOOD; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.MEGA_PINE) {
|
|
- SaplingBlock.treeType = TreeType.MEGA_REDWOOD;
|
|
+ treeType = TreeType.MEGA_REDWOOD; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.MEGA_JUNGLE_TREE) {
|
|
- SaplingBlock.treeType = TreeType.JUNGLE;
|
|
+ treeType = TreeType.JUNGLE; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.AZALEA_TREE) {
|
|
- SaplingBlock.treeType = TreeType.AZALEA;
|
|
+ treeType = TreeType.AZALEA; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.MANGROVE) {
|
|
- SaplingBlock.treeType = TreeType.MANGROVE;
|
|
+ treeType = TreeType.MANGROVE; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.TALL_MANGROVE) {
|
|
- SaplingBlock.treeType = TreeType.TALL_MANGROVE;
|
|
+ treeType = TreeType.TALL_MANGROVE; // Folia - region threading
|
|
} else if (worldgentreeabstract == TreeFeatures.CHERRY || worldgentreeabstract == TreeFeatures.CHERRY_BEES_005) {
|
|
- SaplingBlock.treeType = TreeType.CHERRY;
|
|
+ treeType = TreeType.CHERRY; // Folia - region threading
|
|
} else {
|
|
throw new IllegalArgumentException("Unknown tree generator " + worldgentreeabstract);
|
|
}
|
|
+ SaplingBlock.treeTypeRT.set(treeType); // Folia - region threading
|
|
}
|
|
// CraftBukkit end
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
|
|
index 221c5d080d55326e458c1182823d6b49224ef498..0a1c876d68e834136f71f226c421699eb523de5a 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
|
|
@@ -144,8 +144,8 @@ public class PistonMovingBlockEntity extends BlockEntity {
|
|
|
|
entity.setDeltaMovement(e, g, h);
|
|
// Paper - EAR items stuck in in slime pushed by a piston
|
|
- entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10);
|
|
- entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10);
|
|
+ entity.activatedTick = Math.max(entity.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading
|
|
+ entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading
|
|
// Paper end
|
|
break;
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/border/WorldBorder.java b/src/main/java/net/minecraft/world/level/border/WorldBorder.java
|
|
index 204f008dc36212e696fba781fede88044b2f735a..1bc2b24deba7a534478184a6a3f3d41184a86734 100644
|
|
--- a/src/main/java/net/minecraft/world/level/border/WorldBorder.java
|
|
+++ b/src/main/java/net/minecraft/world/level/border/WorldBorder.java
|
|
@@ -33,19 +33,19 @@ public class WorldBorder {
|
|
|
|
public WorldBorder() {}
|
|
|
|
+ // Folia - region threading - TODO make this shit thread-safe
|
|
+
|
|
public boolean isWithinBounds(BlockPos pos) {
|
|
return (double) (pos.getX() + 1) > this.getMinX() && (double) pos.getX() < this.getMaxX() && (double) (pos.getZ() + 1) > this.getMinZ() && (double) pos.getZ() < this.getMaxZ();
|
|
}
|
|
|
|
// Paper start
|
|
- private final BlockPos.MutableBlockPos mutPos = new BlockPos.MutableBlockPos();
|
|
+ private static final ThreadLocal<BlockPos.MutableBlockPos> mutPos = ThreadLocal.withInitial(() -> new BlockPos.MutableBlockPos()); // Folia - region threading
|
|
public boolean isBlockInBounds(int chunkX, int chunkZ) {
|
|
- this.mutPos.set(chunkX, 64, chunkZ);
|
|
- return this.isWithinBounds(this.mutPos);
|
|
+ return this.isWithinBounds(mutPos.get().set(chunkX, 64, chunkZ)); // Folia - region threading
|
|
}
|
|
public boolean isChunkInBounds(int chunkX, int chunkZ) {
|
|
- this.mutPos.set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15);
|
|
- return this.isWithinBounds(this.mutPos);
|
|
+ return this.isWithinBounds(mutPos.get().set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15)); // Folia - region threading
|
|
}
|
|
// Paper end
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
|
|
index 42ecfc1237bfc724dc9134a6a8bae3670251449e..2fbfdf1fdba12417e2dbca041b9c7e29af27d02d 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
|
|
@@ -306,7 +306,7 @@ public abstract class ChunkGenerator {
|
|
return Pair.of(placement.getLocatePos(pos), holder);
|
|
}
|
|
|
|
- ChunkAccess ichunkaccess = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS);
|
|
+ ChunkAccess ichunkaccess = world.syncLoadNonFull(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); // Folia - region threading
|
|
|
|
structurestart = structureAccessor.getStartForStructure(SectionPos.bottomOf(ichunkaccess), (Structure) holder.value(), ichunkaccess);
|
|
} while (structurestart == null);
|
|
@@ -317,7 +317,7 @@ public abstract class ChunkGenerator {
|
|
}
|
|
|
|
private static boolean tryAddReference(StructureManager structureAccessor, StructureStart start) {
|
|
- if (start.canBeReferenced()) {
|
|
+ if (start.tryReference()) { // Folia - region threading
|
|
structureAccessor.addReference(start);
|
|
return true;
|
|
} else {
|
|
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
|
|
index 9599af33c683ec47e28b1c8e4dc965d30d9081a7..f4c4951f815062c2791531a703ed8cbe9337d574 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
|
|
@@ -61,6 +61,13 @@ public class LevelChunk extends ChunkAccess {
|
|
@Override
|
|
public void tick() {}
|
|
|
|
+ // Folia start - region threading
|
|
+ @Override
|
|
+ public BlockEntity getTileEntity() {
|
|
+ return null;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Override
|
|
public boolean isRemoved() {
|
|
return true;
|
|
@@ -233,51 +240,15 @@ public class LevelChunk extends ChunkAccess {
|
|
}
|
|
// Paper end
|
|
// Paper start - optimise checkDespawn
|
|
- private boolean playerGeneralAreaCacheSet;
|
|
- private com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> playerGeneralAreaCache;
|
|
-
|
|
- public com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> 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<net.minecraft.server.level.ServerPlayer> 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<Entity> predicate) {
|
|
- if (!this.playerGeneralAreaCacheSet) {
|
|
- this.updateGeneralAreaCache();
|
|
- }
|
|
-
|
|
- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> 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<net.minecraft.server.level.ServerPlayer> 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<Entity> predicate,
|
|
double range, java.util.List<net.minecraft.server.level.ServerPlayer> ret) {
|
|
- if (!this.playerGeneralAreaCacheSet) {
|
|
- this.updateGeneralAreaCache();
|
|
- }
|
|
-
|
|
- com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> 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<net.minecraft.server.level.ServerPlayer> nearby = this.level.getLocalPlayers();
|
|
+ for (int i = 0, len = nearby.size(); i < len; ++i) {
|
|
+ net.minecraft.server.level.ServerPlayer player = nearby.get(i);
|
|
|
|
if (range >= 0.0) {
|
|
double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ);
|
|
@@ -322,6 +279,7 @@ public class LevelChunk extends ChunkAccess {
|
|
ret.add(player);
|
|
}
|
|
}
|
|
+ // Folia end - region threading
|
|
}
|
|
// Paper end - optimise checkDespawn
|
|
|
|
@@ -557,7 +515,7 @@ public class LevelChunk extends ChunkAccess {
|
|
return null;
|
|
} else {
|
|
// CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled.
|
|
- if (!this.level.isClientSide && doPlace && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) {
|
|
+ if (!this.level.isClientSide && doPlace && (!this.level.getCurrentWorldData().captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // Folia - region threading
|
|
iblockdata.onPlace(this.level, blockposition, iblockdata1, flag);
|
|
}
|
|
|
|
@@ -604,7 +562,7 @@ public class LevelChunk extends ChunkAccess {
|
|
@Nullable
|
|
public BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) {
|
|
// CraftBukkit start
|
|
- BlockEntity tileentity = level.capturedTileEntities.get(pos);
|
|
+ BlockEntity tileentity = level.getCurrentWorldData().capturedTileEntities.get(pos); // Folia - region threading
|
|
if (tileentity == null) {
|
|
tileentity = (BlockEntity) this.blockEntities.get(pos);
|
|
}
|
|
@@ -889,13 +847,13 @@ public class LevelChunk extends ChunkAccess {
|
|
|
|
org.bukkit.World world = this.level.getWorld();
|
|
if (world != null) {
|
|
- this.level.populating = true;
|
|
+ this.level.getCurrentWorldData().populating = true; // Folia - region threading
|
|
try {
|
|
for (org.bukkit.generator.BlockPopulator populator : world.getPopulators()) {
|
|
populator.populate(world, random, bukkitChunk);
|
|
}
|
|
} finally {
|
|
- this.level.populating = false;
|
|
+ this.level.getCurrentWorldData().populating = false; // Folia - region threading
|
|
}
|
|
}
|
|
server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(this.bukkitChunk));
|
|
@@ -944,7 +902,7 @@ public class LevelChunk extends ChunkAccess {
|
|
@Override
|
|
public boolean isUnsaved() {
|
|
// Paper start - add dirty system to tick lists
|
|
- long gameTime = this.level.getLevelData().getGameTime();
|
|
+ long gameTime = this.level.getRedstoneGameTime(); // Folia - region threading
|
|
if (this.blockTicks.isDirty(gameTime) || this.fluidTicks.isDirty(gameTime)) {
|
|
return true;
|
|
}
|
|
@@ -1225,6 +1183,13 @@ public class LevelChunk extends ChunkAccess {
|
|
this.ticker = wrapped;
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ @Override
|
|
+ public BlockEntity getTileEntity() {
|
|
+ return this.ticker == null ? null : this.ticker.getTileEntity();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Override
|
|
public void tick() {
|
|
this.ticker.tick();
|
|
@@ -1261,6 +1226,13 @@ public class LevelChunk extends ChunkAccess {
|
|
this.ticker = blockentityticker;
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ @Override
|
|
+ public BlockEntity getTileEntity() {
|
|
+ return this.blockEntity;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Override
|
|
public void tick() {
|
|
if (!this.blockEntity.isRemoved() && this.blockEntity.hasLevel()) {
|
|
diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
|
|
index 9d6f4749ed72fe319754ccea28f20fa97286527d..3c9ae42290c2e0cb70bb08e97f3ea2c3fb594c7d 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
|
|
@@ -686,7 +686,7 @@ public class ChunkSerializer {
|
|
}
|
|
|
|
private static void saveTicks(ServerLevel world, CompoundTag nbt, ChunkAccess.TicksToSave tickSchedulers) {
|
|
- long i = world.getLevelData().getGameTime();
|
|
+ long i = world.getRedstoneGameTime(); // Folia - region threading
|
|
|
|
nbt.put("block_ticks", tickSchedulers.blocks().save(i, (block) -> {
|
|
return BuiltInRegistries.BLOCK.getKey(block).toString();
|
|
diff --git a/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java b/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java
|
|
index e9eb32469a5c03f7a3677ef50fd4541c1ed662ad..88b1a06fc44980d05dabd3943caa24091ff4de98 100644
|
|
--- a/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java
|
|
+++ b/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java
|
|
@@ -168,6 +168,7 @@ public class EndDragonFight {
|
|
if (!this.dragonEvent.getPlayers().isEmpty()) {
|
|
this.level.getChunkSource().addRegionTicket(TicketType.DRAGON, new ChunkPos(0, 0), 9, Unit.INSTANCE);
|
|
boolean bl = this.isArenaLoaded();
|
|
+ if (!bl) { return; }// Folia - region threading - don't tick if we don't own the entire region
|
|
if (this.needsStateScanning && bl) {
|
|
this.scanState();
|
|
this.needsStateScanning = false;
|
|
@@ -214,6 +215,12 @@ public class EndDragonFight {
|
|
}
|
|
|
|
List<? extends EnderDragon> list = this.level.getDragons();
|
|
+ // Folia start - region threading
|
|
+ // we do not want to deal with any dragons NOT nearby
|
|
+ list.removeIf((dragon) -> {
|
|
+ return !io.papermc.paper.util.TickThread.isTickThreadFor(dragon);
|
|
+ });
|
|
+ // Folia end - region threading
|
|
if (list.isEmpty()) {
|
|
this.dragonKilled = true;
|
|
} else {
|
|
@@ -324,8 +331,8 @@ public class EndDragonFight {
|
|
private boolean isArenaLoaded() {
|
|
for(int i = -8; i <= 8; ++i) {
|
|
for(int j = 8; j <= 8; ++j) {
|
|
- ChunkAccess chunkAccess = this.level.getChunk(i, j, ChunkStatus.FULL, false);
|
|
- if (!(chunkAccess instanceof LevelChunk)) {
|
|
+ ChunkAccess chunkAccess = this.level.getChunkIfLoaded(i, j); // Folia - region threading
|
|
+ if (!(chunkAccess instanceof LevelChunk) || !io.papermc.paper.util.TickThread.isTickThreadFor(this.level, i, j, this.level.regioniser.regionSectionChunkSize)) { // Folia - region threading
|
|
return false;
|
|
}
|
|
|
|
@@ -465,7 +472,7 @@ public class EndDragonFight {
|
|
}
|
|
|
|
public void onCrystalDestroyed(EndCrystal enderCrystal, DamageSource source) {
|
|
- if (this.respawnStage != null && this.respawnCrystals.contains(enderCrystal)) {
|
|
+ if (io.papermc.paper.util.TickThread.isTickThreadFor(this.level, 0, 0) && this.respawnStage != null && this.respawnCrystals.contains(enderCrystal)) {
|
|
LOGGER.debug("Aborting respawn sequence");
|
|
this.respawnStage = null;
|
|
this.respawnTime = 0;
|
|
@@ -486,7 +493,7 @@ public class EndDragonFight {
|
|
}
|
|
|
|
public void tryRespawn() {
|
|
- if (this.dragonKilled && this.respawnStage == null) {
|
|
+ if (this.dragonKilled && this.respawnStage == null && io.papermc.paper.util.TickThread.isTickThreadFor(this.level, 0, 0)) { // Folia - region threading
|
|
BlockPos blockPos = this.portalLocation;
|
|
if (blockPos == null) {
|
|
LOGGER.debug("Tried to respawn, but need to find the portal first.");
|
|
diff --git a/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java
|
|
index a908652f1ebb426d265ef614746f70cd1e538268..e615b79f68a0467aa8cfa1c61b06ae048a28ef9b 100644
|
|
--- a/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java
|
|
+++ b/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java
|
|
@@ -19,7 +19,7 @@ import net.minecraft.world.level.block.state.BlockState;
|
|
|
|
public class PatrolSpawner implements CustomSpawner {
|
|
|
|
- private int nextTick;
|
|
+ //private int nextTick; // Folia - region threading
|
|
|
|
public PatrolSpawner() {}
|
|
|
|
@@ -32,15 +32,16 @@ public class PatrolSpawner implements CustomSpawner {
|
|
return 0;
|
|
} else {
|
|
RandomSource randomsource = world.random;
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
|
|
// Paper start - Patrol settings
|
|
// Random player selection moved up for per player spawning and configuration
|
|
- int j = world.players().size();
|
|
+ int j = world.getLocalPlayers().size(); // Folia - region threading
|
|
if (j < 1) {
|
|
return 0;
|
|
}
|
|
|
|
- net.minecraft.server.level.ServerPlayer entityhuman = world.players().get(randomsource.nextInt(j));
|
|
+ net.minecraft.server.level.ServerPlayer entityhuman = world.getLocalPlayers().get(randomsource.nextInt(j)); // Folia - region threading
|
|
if (entityhuman.isSpectator()) {
|
|
return 0;
|
|
}
|
|
@@ -50,8 +51,8 @@ public class PatrolSpawner implements CustomSpawner {
|
|
--entityhuman.patrolSpawnDelay;
|
|
patrolSpawnDelay = entityhuman.patrolSpawnDelay;
|
|
} else {
|
|
- this.nextTick--;
|
|
- patrolSpawnDelay = this.nextTick;
|
|
+ worldData.patrolSpawnerNextTick--; // Folia - region threading
|
|
+ patrolSpawnDelay = worldData.patrolSpawnerNextTick; // Folia - region threading
|
|
}
|
|
|
|
if (patrolSpawnDelay > 0) {
|
|
@@ -66,7 +67,7 @@ public class PatrolSpawner implements CustomSpawner {
|
|
if (world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.perPlayer) {
|
|
entityhuman.patrolSpawnDelay += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200);
|
|
} else {
|
|
- this.nextTick += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200);
|
|
+ worldData.patrolSpawnerNextTick += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200); // Folia - region threading
|
|
}
|
|
|
|
if (days >= world.paperConfig().entities.behavior.pillagerPatrols.start.day && world.isDay()) {
|
|
diff --git a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java
|
|
index e8ae4449696d73c8c9b8b27d4d2e20db933a72cc..99959b50d06d550ce56bb26e35ec38ec741479da 100644
|
|
--- a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java
|
|
+++ b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java
|
|
@@ -24,7 +24,7 @@ import net.minecraft.world.level.material.FluidState;
|
|
|
|
public class PhantomSpawner implements CustomSpawner {
|
|
|
|
- private int nextTick;
|
|
+ //private int nextTick; // Folia - region threading
|
|
|
|
public PhantomSpawner() {}
|
|
|
|
@@ -42,20 +42,22 @@ public class PhantomSpawner implements CustomSpawner {
|
|
// Paper end
|
|
RandomSource randomsource = world.random;
|
|
|
|
- --this.nextTick;
|
|
- if (this.nextTick > 0) {
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
+
|
|
+ --worldData.phantomSpawnerNextTick; // Folia - region threading
|
|
+ if (worldData.phantomSpawnerNextTick > 0) { // Folia - region threading
|
|
return 0;
|
|
} else {
|
|
// Paper start
|
|
int spawnAttemptMinSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMinSeconds;
|
|
int spawnAttemptMaxSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds;
|
|
- this.nextTick += (spawnAttemptMinSeconds + randomsource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20;
|
|
+ worldData.phantomSpawnerNextTick += (spawnAttemptMinSeconds + randomsource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; // Folia - region threading
|
|
// Paper end
|
|
if (world.getSkyDarken() < 5 && world.dimensionType().hasSkyLight()) {
|
|
return 0;
|
|
} else {
|
|
int i = 0;
|
|
- Iterator iterator = world.players().iterator();
|
|
+ Iterator iterator = world.getLocalPlayers().iterator(); // Folia - region threading
|
|
|
|
while (iterator.hasNext()) {
|
|
Player entityhuman = (Player) iterator.next();
|
|
diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
|
|
index 1ca00340aaa201dd34e5c350d23ef53e126a0ca6..cf664ed17c6c138aed2a8680b51480ded47a2e50 100644
|
|
--- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
|
|
+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java
|
|
@@ -50,8 +50,101 @@ public class StructureCheck {
|
|
private final BiomeSource biomeSource;
|
|
private final long seed;
|
|
private final DataFixer fixerUpper;
|
|
- private final Long2ObjectMap<Object2IntMap<Structure>> loadedChunks = new Long2ObjectOpenHashMap<>();
|
|
- private final Map<Structure, Long2BooleanMap> 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<Object2IntMap<Structure>> loadedChunksSafe = new SynchronisedLong2ObjectMap<>(CHUNK_TOTAL_LIMIT);
|
|
+ private final java.util.concurrent.ConcurrentHashMap<Structure, SynchronisedLong2BooleanMap> featureChecksSafe = new java.util.concurrent.ConcurrentHashMap<>();
|
|
+
|
|
+ private static final class SynchronisedLong2ObjectMap<V> {
|
|
+ private final it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<V> 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<? super Long, ? super V, ? extends V> remappingFunction) {
|
|
+ synchronized (this.map) {
|
|
+ // first, compute the value - if one is added, it will be at the last entry
|
|
+ this.map.compute(key, remappingFunction);
|
|
+ // move the entry to first, just in case it was added at last
|
|
+ final V ret = this.map.getAndMoveToFirst(key);
|
|
+ // now purge the last entries
|
|
+ this.purgeEntries();
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ 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<net.minecraft.world.level.dimension.LevelStem> worldKey, ChunkGenerator chunkGenerator, RandomState noiseConfig, LevelHeightAccessor world, BiomeSource biomeSource, long seed, DataFixer dataFixer) { // Paper - fix missing CB diff
|
|
this.storageAccess = chunkIoWorker;
|
|
@@ -70,7 +163,7 @@ public class StructureCheck {
|
|
|
|
public StructureCheckResult checkStart(ChunkPos pos, Structure type, boolean skipReferencedStructures) {
|
|
long l = pos.toLong();
|
|
- Object2IntMap<Structure> object2IntMap = this.loadedChunks.get(l);
|
|
+ Object2IntMap<Structure> object2IntMap = this.loadedChunksSafe.get(l); // Folia - region threading
|
|
if (object2IntMap != null) {
|
|
return this.checkStructureInfo(object2IntMap, type, skipReferencedStructures);
|
|
} else {
|
|
@@ -78,9 +171,9 @@ public class StructureCheck {
|
|
if (structureCheckResult != null) {
|
|
return structureCheckResult;
|
|
} else {
|
|
- boolean bl = this.featureChecks.computeIfAbsent(type, (structure2) -> {
|
|
- return new Long2BooleanOpenHashMap();
|
|
- }).computeIfAbsent(l, (chunkPos) -> {
|
|
+ boolean bl = this.featureChecksSafe.computeIfAbsent(type, (structure2) -> { // Folia - region threading
|
|
+ return new SynchronisedLong2BooleanMap(PER_FEATURE_CHECK_LIMIT); // Folia - region threading
|
|
+ }).getOrCompute(l, (chunkPos) -> { // Folia - region threading
|
|
return this.canCreateStructure(pos, type);
|
|
});
|
|
return !bl ? StructureCheckResult.START_NOT_PRESENT : StructureCheckResult.CHUNK_LOAD_NEEDED;
|
|
@@ -193,17 +286,26 @@ public class StructureCheck {
|
|
}
|
|
|
|
private void storeFullResults(long pos, Object2IntMap<Structure> 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<Structure> fastClone ? fastClone.clone() : new Object2IntOpenHashMap<>(referencesByStructure);
|
|
}
|
|
+ // Folia end - region threading
|
|
|
|
referencesByStructure.computeInt(structure, (feature, references) -> {
|
|
return references == null ? 1 : references + 1;
|
|
diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java
|
|
index 6570e0b61d7602c57c61398ddce50418d0719ff2..bcee13beed247f7830ee85d099c367dbfd1ea51d 100644
|
|
--- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java
|
|
+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java
|
|
@@ -26,14 +26,14 @@ public final class StructureStart {
|
|
private final Structure structure;
|
|
private final PiecesContainer pieceContainer;
|
|
private final ChunkPos chunkPos;
|
|
- private int references;
|
|
+ private final java.util.concurrent.atomic.AtomicInteger references; // Folia - region threading
|
|
@Nullable
|
|
private volatile BoundingBox cachedBoundingBox;
|
|
|
|
public StructureStart(Structure structure, ChunkPos pos, int references, PiecesContainer children) {
|
|
this.structure = structure;
|
|
this.chunkPos = pos;
|
|
- this.references = references;
|
|
+ this.references = new java.util.concurrent.atomic.AtomicInteger(references); // Folia - region threading
|
|
this.pieceContainer = children;
|
|
}
|
|
|
|
@@ -101,7 +101,7 @@ public final class StructureStart {
|
|
compoundTag.putString("id", context.registryAccess().registryOrThrow(Registries.STRUCTURE).getKey(this.structure).toString());
|
|
compoundTag.putInt("ChunkX", chunkPos.x);
|
|
compoundTag.putInt("ChunkZ", chunkPos.z);
|
|
- compoundTag.putInt("references", this.references);
|
|
+ compoundTag.putInt("references", this.references.get()); // Folia - region threading
|
|
compoundTag.put("Children", this.pieceContainer.save(context));
|
|
return compoundTag;
|
|
} else {
|
|
@@ -119,15 +119,29 @@ public final class StructureStart {
|
|
}
|
|
|
|
public boolean canBeReferenced() {
|
|
- return this.references < this.getMaxReferences();
|
|
+ throw new UnsupportedOperationException("Use tryReference()"); // Folia - region threading
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ public boolean tryReference() {
|
|
+ for (int curr = this.references.get();;) {
|
|
+ if (curr >= this.getMaxReferences()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = this.references.compareAndExchange(curr, curr + 1))) {
|
|
+ return true;
|
|
+ } // else: try again
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
public void addReference() {
|
|
- ++this.references;
|
|
+ throw new UnsupportedOperationException("Use tryReference()"); // Folia - region threading
|
|
}
|
|
|
|
public int getReferences() {
|
|
- return this.references;
|
|
+ return this.references.get(); // Folia - region threading
|
|
}
|
|
|
|
protected int getMaxReferences() {
|
|
diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
|
|
index 92d13c9f1ec1e5ff72c1d68f924a8d1c86c91565..3f224315f1e0a8f69e9d84d27fd7dbe45ea75667 100644
|
|
--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
|
|
+++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
|
|
@@ -89,10 +89,10 @@ public class PortalForcer {
|
|
BlockPos blockposition1 = villageplacerecord.getPos();
|
|
|
|
this.level.getChunkSource().addRegionTicket(TicketType.PORTAL, new ChunkPos(blockposition1), 3, blockposition1);
|
|
- BlockState iblockdata = this.level.getBlockState(blockposition1);
|
|
+ BlockState iblockdata = this.level.getBlockStateFromEmptyChunk(blockposition1); // Folia - region threading
|
|
|
|
return BlockUtil.getLargestRectangleAround(blockposition1, (Direction.Axis) iblockdata.getValue(BlockStateProperties.HORIZONTAL_AXIS), 21, Direction.Axis.Y, 21, (blockposition2) -> {
|
|
- return this.level.getBlockState(blockposition2) == iblockdata;
|
|
+ return this.level.getBlockStateFromEmptyChunk(blockposition2) == iblockdata; // Folia - region threading
|
|
});
|
|
});
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java b/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java
|
|
index b1c594dc6a6b8a6c737b99272acab9e7dbd0ed63..7c1768452fa0f7278ccc84470ef0965a3f96b0df 100644
|
|
--- a/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java
|
|
+++ b/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java
|
|
@@ -46,6 +46,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater {
|
|
}
|
|
|
|
private void addAndRun(BlockPos pos, CollectingNeighborUpdater.NeighborUpdates entry) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread((net.minecraft.server.level.ServerLevel)this.level, pos, "Adding block without owning region"); // Folia - region threading
|
|
boolean bl = this.count > 0;
|
|
boolean bl2 = this.maxChainedNeighborUpdates >= 0 && this.count >= this.maxChainedNeighborUpdates;
|
|
++this.count;
|
|
diff --git a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java
|
|
index 353e602d476beea23e591ad770227c5d6c1e97fa..6f254ee89e1f2eb2b853888e2ff1259cd1f9a6db 100644
|
|
--- a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java
|
|
+++ b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java
|
|
@@ -10,7 +10,7 @@ import org.slf4j.Logger;
|
|
|
|
public abstract class SavedData {
|
|
private static final Logger LOGGER = LogUtils.getLogger();
|
|
- private boolean dirty;
|
|
+ private volatile boolean dirty; // Folia - make map data thread-safe
|
|
|
|
public abstract CompoundTag save(CompoundTag nbt);
|
|
|
|
@@ -28,6 +28,7 @@ public abstract class SavedData {
|
|
|
|
public void save(File file) {
|
|
if (this.isDirty()) {
|
|
+ this.setDirty(false); // Folia - make map data thread-safe - move before save, so that any changes after are not lost
|
|
CompoundTag compoundTag = new CompoundTag();
|
|
compoundTag.put("data", this.save(new CompoundTag()));
|
|
NbtUtils.addCurrentDataVersion(compoundTag);
|
|
@@ -38,7 +39,7 @@ public abstract class SavedData {
|
|
LOGGER.error("Could not save data {}", this, var4);
|
|
}
|
|
|
|
- this.setDirty(false);
|
|
+ // Folia - make map data thread-safe - move before save, so that any changes after are not lost
|
|
}
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java
|
|
index 9b2948b5150c8f039ca667a50765109721b93947..1b76e4edce628f2b25815e28cd4cb7504a83a00f 100644
|
|
--- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java
|
|
+++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java
|
|
@@ -27,17 +27,21 @@ public class MapIndex extends SavedData {
|
|
|
|
@Override
|
|
public CompoundTag save(CompoundTag nbt) {
|
|
+ synchronized (this.usedAuxIds) { // Folia - make map data thread-safe
|
|
for(Object2IntMap.Entry<String> entry : this.usedAuxIds.object2IntEntrySet()) {
|
|
nbt.putInt(entry.getKey(), entry.getIntValue());
|
|
}
|
|
+ } // Folia - make map data thread-safe
|
|
|
|
return nbt;
|
|
}
|
|
|
|
public int getFreeAuxValueForMap() {
|
|
+ synchronized (this.usedAuxIds) { // Folia - make map data thread-safe
|
|
int i = this.usedAuxIds.getInt("map") + 1;
|
|
this.usedAuxIds.put("map", i);
|
|
this.setDirty();
|
|
return i;
|
|
+ } // Folia - make map data thread-safe
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
|
|
index 50348a5eec1e2ffabfc8405d708461376a162913..56e277f328a18e342c1adbe19660a31cbb175ba9 100644
|
|
--- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
|
|
+++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
|
|
@@ -185,7 +185,7 @@ public class MapItemSavedData extends SavedData {
|
|
}
|
|
|
|
@Override
|
|
- public CompoundTag save(CompoundTag nbt) {
|
|
+ public synchronized CompoundTag save(CompoundTag nbt) { // Folia - make map data thread-safe
|
|
DataResult<Tag> dataresult = ResourceLocation.CODEC.encodeStart(NbtOps.INSTANCE, this.dimension.location()); // CraftBukkit - decompile error
|
|
Logger logger = MapItemSavedData.LOGGER;
|
|
|
|
@@ -242,7 +242,7 @@ public class MapItemSavedData extends SavedData {
|
|
return nbt;
|
|
}
|
|
|
|
- public MapItemSavedData locked() {
|
|
+ public synchronized MapItemSavedData locked() { // Folia - make map data thread-safe
|
|
MapItemSavedData worldmap = new MapItemSavedData(this.centerX, this.centerZ, this.scale, this.trackingPosition, this.unlimitedTracking, true, this.dimension);
|
|
|
|
worldmap.bannerMarkers.putAll(this.bannerMarkers);
|
|
@@ -253,11 +253,12 @@ public class MapItemSavedData extends SavedData {
|
|
return worldmap;
|
|
}
|
|
|
|
- public MapItemSavedData scaled(int zoomOutScale) {
|
|
+ public synchronized MapItemSavedData scaled(int zoomOutScale) { // Folia - make map data thread-safe
|
|
return MapItemSavedData.createFresh((double) this.centerX, (double) this.centerZ, (byte) Mth.clamp(this.scale + zoomOutScale, 0, 4), this.trackingPosition, this.unlimitedTracking, this.dimension);
|
|
}
|
|
|
|
- public void tickCarriedBy(Player player, ItemStack stack) {
|
|
+ public synchronized void tickCarriedBy(Player player, ItemStack stack) { // Folia - make map data thread-safe
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(player, "Ticking map player in incorrect region"); // Folia - region threading
|
|
if (!this.carriedByPlayers.containsKey(player)) {
|
|
MapItemSavedData.HoldingPlayer worldmap_worldmaphumantracker = new MapItemSavedData.HoldingPlayer(player);
|
|
|
|
@@ -368,7 +369,7 @@ public class MapItemSavedData extends SavedData {
|
|
rotation += rotation < 0.0D ? -8.0D : 8.0D;
|
|
b2 = (byte) ((int) (rotation * 16.0D / 360.0D));
|
|
if (this.dimension == Level.NETHER && world != null) {
|
|
- int j = (int) (world.getLevelData().getDayTime() / 10L);
|
|
+ int j = (int) (world.getLevelData().getDayTime() / 10L); // Folia - region threading - TODO
|
|
|
|
b2 = (byte) (j * j * 34187121 + j * 121 >> 15 & 15);
|
|
}
|
|
@@ -427,14 +428,14 @@ public class MapItemSavedData extends SavedData {
|
|
}
|
|
|
|
@Nullable
|
|
- public Packet<?> getUpdatePacket(int id, Player player) {
|
|
+ public synchronized Packet<?> getUpdatePacket(int id, Player player) { // Folia - make map data thread-safe
|
|
MapItemSavedData.HoldingPlayer worldmap_worldmaphumantracker = (MapItemSavedData.HoldingPlayer) this.carriedByPlayers.get(player);
|
|
|
|
return worldmap_worldmaphumantracker == null ? null : worldmap_worldmaphumantracker.nextUpdatePacket(id);
|
|
}
|
|
|
|
- public void setColorsDirty(int x, int z) {
|
|
- this.setDirty();
|
|
+ public synchronized void setColorsDirty(int x, int z) { // Folia - make map data thread-safe
|
|
+ // Folia - make dirty only after updating data - moved down
|
|
Iterator iterator = this.carriedBy.iterator();
|
|
|
|
while (iterator.hasNext()) {
|
|
@@ -442,15 +443,16 @@ public class MapItemSavedData extends SavedData {
|
|
|
|
worldmap_worldmaphumantracker.markColorsDirty(x, z);
|
|
}
|
|
-
|
|
+ this.setDirty(); // Folia - make dirty only after updating data - moved from above
|
|
}
|
|
|
|
- public void setDecorationsDirty() {
|
|
- this.setDirty();
|
|
+ public synchronized void setDecorationsDirty() { // Folia - make map data thread-safe
|
|
+ // Folia - make dirty only after updating data - moved down
|
|
this.carriedBy.forEach(MapItemSavedData.HoldingPlayer::markDecorationsDirty);
|
|
+ this.setDirty(); // Folia - make dirty only after updating data - moved from above
|
|
}
|
|
|
|
- public MapItemSavedData.HoldingPlayer getHoldingPlayer(Player player) {
|
|
+ public synchronized MapItemSavedData.HoldingPlayer getHoldingPlayer(Player player) { // Folia - make map data thread-safe
|
|
MapItemSavedData.HoldingPlayer worldmap_worldmaphumantracker = (MapItemSavedData.HoldingPlayer) this.carriedByPlayers.get(player);
|
|
|
|
if (worldmap_worldmaphumantracker == null) {
|
|
@@ -462,7 +464,7 @@ public class MapItemSavedData extends SavedData {
|
|
return worldmap_worldmaphumantracker;
|
|
}
|
|
|
|
- public boolean toggleBanner(LevelAccessor world, BlockPos pos) {
|
|
+ public synchronized boolean toggleBanner(LevelAccessor world, BlockPos pos) { // Folia - make map data thread-safe
|
|
double d0 = (double) pos.getX() + 0.5D;
|
|
double d1 = (double) pos.getZ() + 0.5D;
|
|
int i = 1 << this.scale;
|
|
@@ -471,7 +473,7 @@ public class MapItemSavedData extends SavedData {
|
|
boolean flag = true;
|
|
|
|
if (d2 >= -63.0D && d3 >= -63.0D && d2 <= 63.0D && d3 <= 63.0D) {
|
|
- MapBanner mapiconbanner = MapBanner.fromWorld(world, pos);
|
|
+ MapBanner mapiconbanner = world.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null || !io.papermc.paper.util.TickThread.isTickThreadFor(world.getMinecraftWorld(), pos) ? null : MapBanner.fromWorld(world, pos); // Folia - make map data thread-safe - don't sync load or read data we do not own
|
|
|
|
if (mapiconbanner == null) {
|
|
return false;
|
|
@@ -492,7 +494,7 @@ public class MapItemSavedData extends SavedData {
|
|
return false;
|
|
}
|
|
|
|
- public void checkBanners(BlockGetter world, int x, int z) {
|
|
+ public synchronized void checkBanners(BlockGetter world, int x, int z) { // Folia - make map data thread-safe
|
|
Iterator iterator = this.bannerMarkers.values().iterator();
|
|
|
|
while (iterator.hasNext()) {
|
|
@@ -514,12 +516,12 @@ public class MapItemSavedData extends SavedData {
|
|
return this.bannerMarkers.values();
|
|
}
|
|
|
|
- public void removedFromFrame(BlockPos pos, int id) {
|
|
+ public synchronized void removedFromFrame(BlockPos pos, int id) { // Folia - make map data thread-safe
|
|
this.removeDecoration("frame-" + id);
|
|
this.frameMarkers.remove(MapFrame.frameId(pos));
|
|
}
|
|
|
|
- public boolean updateColor(int x, int z, byte color) {
|
|
+ public synchronized boolean updateColor(int x, int z, byte color) { // Folia - make map data thread-safe
|
|
byte b1 = this.colors[x + z * 128];
|
|
|
|
if (b1 != color) {
|
|
@@ -530,12 +532,12 @@ public class MapItemSavedData extends SavedData {
|
|
}
|
|
}
|
|
|
|
- public void setColor(int x, int z, byte color) {
|
|
+ public synchronized void setColor(int x, int z, byte color) { // Folia - make map data thread-safe
|
|
this.colors[x + z * 128] = color;
|
|
this.setColorsDirty(x, z);
|
|
}
|
|
|
|
- public boolean isExplorationMap() {
|
|
+ public synchronized boolean isExplorationMap() { // Folia - make map data thread-safe
|
|
Iterator iterator = this.decorations.values().iterator();
|
|
|
|
MapDecoration mapicon;
|
|
@@ -570,7 +572,7 @@ public class MapItemSavedData extends SavedData {
|
|
return this.decorations.values();
|
|
}
|
|
|
|
- public boolean isTrackedCountOverLimit(int iconCount) {
|
|
+ public synchronized boolean isTrackedCountOverLimit(int iconCount) { // Folia - make map data thread-safe
|
|
return this.trackedDecorationCount >= iconCount;
|
|
}
|
|
|
|
@@ -696,11 +698,13 @@ public class MapItemSavedData extends SavedData {
|
|
}
|
|
|
|
public void applyToMap(MapItemSavedData mapState) {
|
|
+ synchronized (mapState) { // Folia - make map data thread-safe
|
|
for (int i = 0; i < this.width; ++i) {
|
|
for (int j = 0; j < this.height; ++j) {
|
|
mapState.setColor(this.startX + i, this.startY + j, this.mapColors[i + j * this.width]);
|
|
}
|
|
}
|
|
+ } // Folia - make map data thread-safe
|
|
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java
|
|
index defe31a5d3aa89a3d18b94f2ff005594e38754b3..55609e5bfb2e1d6d8d832355080f520ec7f5b7e7 100644
|
|
--- a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java
|
|
+++ b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java
|
|
@@ -36,6 +36,7 @@ public class DimensionDataStorage {
|
|
}
|
|
|
|
public <T extends SavedData> T computeIfAbsent(Function<CompoundTag, T> readFunction, Supplier<T> 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 extends SavedData> T get(Function<CompoundTag, T> 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<T> implements SerializableTickContainer<T>, 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<T>[] queue = this.tickQueue.toArray(new ScheduledTick[0]);
|
|
+ this.tickQueue.clear();
|
|
+ for (final ScheduledTick<T> entry : queue) {
|
|
+ final ScheduledTick<T> newEntry = new ScheduledTick<>(
|
|
+ entry.type(), entry.pos(), entry.triggerTick() + offset, entry.subTickOrder()
|
|
+ );
|
|
+ this.tickQueue.add(newEntry);
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
|
|
public LevelChunkTicks() {
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/ticks/LevelTicks.java b/src/main/java/net/minecraft/world/ticks/LevelTicks.java
|
|
index 1d7c663fa0e550bd0cfb9a4b83ccd7e2968666f0..f3df9c9b6cff85565514f990597f3fe53652812c 100644
|
|
--- a/src/main/java/net/minecraft/world/ticks/LevelTicks.java
|
|
+++ b/src/main/java/net/minecraft/world/ticks/LevelTicks.java
|
|
@@ -42,13 +42,70 @@ public class LevelTicks<T> implements LevelTickAccess<T> {
|
|
private final List<ScheduledTick<T>> alreadyRunThisTick = new ArrayList<>();
|
|
private final Set<ScheduledTick<?>> toRunThisTickSet = new ObjectOpenCustomHashSet<>(ScheduledTick.UNIQUE_TICK_HASH);
|
|
private final BiConsumer<LevelChunkTicks<T>, ScheduledTick<T>> 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<ProfilerFiller> profilerGetter) {
|
|
+ // Folia start - region threading
|
|
+ public final net.minecraft.server.level.ServerLevel world;
|
|
+ public final boolean isBlock;
|
|
+
|
|
+ public void merge(final LevelTicks<T> 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<Long2ObjectMap.Entry<LevelChunkTicks<T>>> iterator =
|
|
+ ((Long2ObjectOpenHashMap<LevelChunkTicks<T>>)this.allContainers).long2ObjectEntrySet().fastIterator();
|
|
+ iterator.hasNext();) {
|
|
+ final Long2ObjectMap.Entry<LevelChunkTicks<T>> entry = iterator.next();
|
|
+ final LevelChunkTicks<T> tickContainer = entry.getValue();
|
|
+ tickContainer.offsetTicks(tickOffset);
|
|
+ into.allContainers.put(entry.getLongKey(), tickContainer);
|
|
+ }
|
|
+ for (final java.util.Iterator<Long2LongMap.Entry> 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<LevelTicks<T>> regionToData) {
|
|
+ for (final java.util.Iterator<Long2ObjectMap.Entry<LevelChunkTicks<T>>> iterator =
|
|
+ ((Long2ObjectOpenHashMap<LevelChunkTicks<T>>)this.allContainers).long2ObjectEntrySet().fastIterator();
|
|
+ iterator.hasNext();) {
|
|
+ final Long2ObjectMap.Entry<LevelChunkTicks<T>> 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<Long2LongMap.Entry> 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<ProfilerFiller> 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<T> implements LevelTickAccess<T> {
|
|
this.nextTickForContainer.put(l, scheduledTick.triggerTick());
|
|
}
|
|
|
|
- scheduler.setOnTickAdded(this.chunkScheduleUpdater);
|
|
+ // Folia start - region threading
|
|
+ final boolean isBlock = this.isBlock;
|
|
+ final net.minecraft.server.level.ServerLevel world = this.world;
|
|
+ // make sure the lambda contains no reference to this LevelTicks
|
|
+ scheduler.setOnTickAdded((chunkTickScheduler, tick) -> {
|
|
+ if (tick.equals(chunkTickScheduler.peek())) {
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData();
|
|
+ (isBlock ? worldData.getBlockLevelTicks() : worldData.getFluidLevelTicks()).updateContainerScheduling((ScheduledTick)tick);
|
|
+ }
|
|
+ });
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
public void removeContainer(ChunkPos pos) {
|
|
@@ -76,6 +143,7 @@ public class LevelTicks<T> implements LevelTickAccess<T> {
|
|
|
|
@Override
|
|
public void schedule(ScheduledTick<T> 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<T> levelChunkTicks = this.allContainers.get(l);
|
|
if (levelChunkTicks == null) {
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
|
|
index e0b6c737f9de2b6e692d6813d8dea4c35f038573..851937baff90177e648d970cf1c462b897aaf9f5 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
|
|
@@ -303,6 +303,76 @@ public final class CraftServer implements Server {
|
|
CraftItemFactory.instance();
|
|
}
|
|
|
|
+ // Folia start - region threading API
|
|
+ private final io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler();
|
|
+ private final io.papermc.paper.threadedregions.scheduler.FoliaAsyncScheduler asyncScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaAsyncScheduler();
|
|
+ private final io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler globalRegionScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler();
|
|
+
|
|
+ @Override
|
|
+ public final io.papermc.paper.threadedregions.scheduler.RegionScheduler getRegionScheduler() {
|
|
+ return this.regionizedScheduler;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final io.papermc.paper.threadedregions.scheduler.AsyncScheduler getAsyncScheduler() {
|
|
+ return this.asyncScheduler;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler getGlobalRegionScheduler() {
|
|
+ return this.globalRegionScheduler;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final boolean isOwnedByCurrentRegion(World world, io.papermc.paper.math.Position position) {
|
|
+ return io.papermc.paper.util.TickThread.isTickThreadFor(
|
|
+ ((CraftWorld) world).getHandle(), position.blockX() >> 4, position.blockZ() >> 4
|
|
+ );
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final boolean isOwnedByCurrentRegion(World world, io.papermc.paper.math.Position position, int squareRadiusChunks) {
|
|
+ return io.papermc.paper.util.TickThread.isTickThreadFor(
|
|
+ ((CraftWorld) world).getHandle(), position.blockX() >> 4, position.blockZ() >> 4, squareRadiusChunks
|
|
+ );
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final boolean isOwnedByCurrentRegion(Location location) {
|
|
+ World world = location.getWorld();
|
|
+ return io.papermc.paper.util.TickThread.isTickThreadFor(
|
|
+ ((CraftWorld) world).getHandle(), location.getBlockX() >> 4, location.getBlockZ() >> 4
|
|
+ );
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final boolean isOwnedByCurrentRegion(Location location, int squareRadiusChunks) {
|
|
+ World world = location.getWorld();
|
|
+ return io.papermc.paper.util.TickThread.isTickThreadFor(
|
|
+ ((CraftWorld) world).getHandle(), location.getBlockX() >> 4, location.getBlockZ() >> 4, squareRadiusChunks
|
|
+ );
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final boolean isOwnedByCurrentRegion(World world, int chunkX, int chunkZ) {
|
|
+ return io.papermc.paper.util.TickThread.isTickThreadFor(
|
|
+ ((CraftWorld) world).getHandle(), chunkX, chunkZ
|
|
+ );
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final boolean isOwnedByCurrentRegion(World world, int chunkX, int chunkZ, int squareRadiusChunks) {
|
|
+ return io.papermc.paper.util.TickThread.isTickThreadFor(
|
|
+ ((CraftWorld) world).getHandle(), chunkX, chunkZ, squareRadiusChunks
|
|
+ );
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final boolean isOwnedByCurrentRegion(Entity entity) {
|
|
+ return io.papermc.paper.util.TickThread.isTickThreadFor(((org.bukkit.craftbukkit.entity.CraftEntity)entity).getHandle());
|
|
+ }
|
|
+ // Folia end - region threading API
|
|
+
|
|
public CraftServer(DedicatedServer console, PlayerList playerList) {
|
|
this.console = console;
|
|
this.playerList = (DedicatedPlayerList) playerList;
|
|
@@ -856,6 +926,9 @@ public final class CraftServer implements Server {
|
|
|
|
// NOTE: Should only be called from DedicatedServer.ah()
|
|
public boolean dispatchServerCommand(CommandSender sender, ConsoleInput serverCommand) {
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("May not dispatch server commands async");
|
|
+ // Folia end - region threading
|
|
if (sender instanceof Conversable) {
|
|
Conversable conversable = (Conversable) sender;
|
|
|
|
@@ -875,12 +948,44 @@ public final class CraftServer implements Server {
|
|
}
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ public void dispatchCmdAsync(CommandSender sender, String commandLine) {
|
|
+ if ((sender instanceof Entity entity)) {
|
|
+ ((org.bukkit.craftbukkit.entity.CraftEntity)entity).taskScheduler.schedule(
|
|
+ (nmsEntity) -> {
|
|
+ CraftServer.this.dispatchCommand(nmsEntity.getBukkitEntity(), commandLine);
|
|
+ },
|
|
+ null,
|
|
+ 1L
|
|
+ );
|
|
+ } else if (sender instanceof ConsoleCommandSender console) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
|
|
+ CraftServer.this.dispatchCommand(sender, commandLine);
|
|
+ });
|
|
+ } else {
|
|
+ // huh?
|
|
+ throw new UnsupportedOperationException("Dispatching command for " + sender);
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Override
|
|
public boolean dispatchCommand(CommandSender sender, String commandLine) {
|
|
Validate.notNull(sender, "Sender cannot be null");
|
|
Validate.notNull(commandLine, "CommandLine cannot be null");
|
|
org.spigotmc.AsyncCatcher.catchOp("command dispatch"); // Spigot
|
|
|
|
+ // Folia start - region threading
|
|
+ if ((sender instanceof Entity entity)) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(((org.bukkit.craftbukkit.entity.CraftEntity)entity).getHandle(), "Dispatching command async");
|
|
+ } else if (sender instanceof ConsoleCommandSender console) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Dispatching command async");
|
|
+ } else {
|
|
+ // huh?
|
|
+ throw new UnsupportedOperationException("Dispatching command for " + sender);
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
// Paper Start
|
|
if (!org.spigotmc.AsyncCatcher.shuttingDown && !Bukkit.isPrimaryThread()) {
|
|
final CommandSender fSender = sender;
|
|
@@ -2894,7 +2999,7 @@ public final class CraftServer implements Server {
|
|
|
|
@Override
|
|
public int getCurrentTick() {
|
|
- return net.minecraft.server.MinecraftServer.currentTick;
|
|
+ return (int)io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
index ff6559bf563f2fdcc0f2843d4f4aa24d7ddfb6db..aaeb1bdbf3f2f719f08777b2c2cc2196c49dee89 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
|
|
@@ -329,6 +329,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public Chunk getChunkAt(int x, int z) {
|
|
+ io.papermc.paper.util.TickThread.isTickThreadFor(this.getHandle(), x, z); // Folia - region threading
|
|
warnUnsafeChunk("getting a faraway chunk", x, z); // Paper
|
|
// Paper start - add ticket to hold chunk for a little while longer if plugin accesses it
|
|
net.minecraft.world.level.chunk.LevelChunk chunk = world.getChunkSource().getChunkAtIfLoadedImmediately(x, z);
|
|
@@ -342,7 +343,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
// Paper start
|
|
private void addTicket(int x, int z) {
|
|
- io.papermc.paper.util.MCUtil.MAIN_EXECUTOR.execute(() -> world.getChunkSource().addRegionTicket(TicketType.PLUGIN, new ChunkPos(x, z), 0, Unit.INSTANCE)); // Paper
|
|
+ world.getChunkSource().addRegionTicket(TicketType.PLUGIN, new ChunkPos(x, z), 0, Unit.INSTANCE); // Paper // Folia - region threading - does not need to be on the main thread anymore
|
|
}
|
|
// Paper end
|
|
|
|
@@ -361,10 +362,10 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
@Override
|
|
public boolean isChunkGenerated(int x, int z) {
|
|
// Paper start - Fix this method
|
|
- if (!Bukkit.isPrimaryThread()) {
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(this.getHandle(), x, z)) { // Folia - region threading
|
|
return java.util.concurrent.CompletableFuture.supplyAsync(() -> {
|
|
return CraftWorld.this.isChunkGenerated(x, z);
|
|
- }, world.getChunkSource().mainThreadProcessor).join();
|
|
+ }, (run) -> { io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(this.getHandle(), x, z, run);}).join(); // Folia - region threading
|
|
}
|
|
ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z);
|
|
if (chunk == null) {
|
|
@@ -530,6 +531,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public boolean loadChunk(int x, int z, boolean generate) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this.getHandle(), x, z, "May not sync load chunks asynchronously"); // Folia - region threading
|
|
org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot
|
|
warnUnsafeChunk("loading a faraway chunk", x, z); // Paper
|
|
// Paper start - Optimize this method
|
|
@@ -603,7 +605,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager;
|
|
|
|
if (chunkDistanceManager.addRegionTicketAtDistance(TicketType.PLUGIN_TICKET, new ChunkPos(x, z), 2, plugin)) { // keep in-line with force loading, add at level 31
|
|
- this.getChunkAt(x, z); // ensure loaded
|
|
+ //this.getChunkAt(x, z); // ensure loaded // Folia - region threading - do not load chunks for tickets anymore to make this mt-safe
|
|
return true;
|
|
}
|
|
|
|
@@ -788,13 +790,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.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
+ worldData.captureTreeGeneration = true; // Folia - region threading
|
|
+ worldData.captureBlockStates = true; // Folia - region threading
|
|
boolean grownTree = this.generateTree(loc, type);
|
|
- world.captureBlockStates = false;
|
|
- world.captureTreeGeneration = false;
|
|
+ worldData.captureBlockStates = false; // Folia - region threading
|
|
+ worldData.captureTreeGeneration = false; // Folia - region threading
|
|
if (grownTree) { // Copy block data to delegate
|
|
- for (BlockState blockstate : world.capturedBlockStates.values()) {
|
|
+ for (BlockState blockstate : worldData.capturedBlockStates.values()) { // Folia - region threading
|
|
BlockPos position = ((CraftBlockState) blockstate).getPosition();
|
|
net.minecraft.world.level.block.state.BlockState oldBlock = this.world.getBlockState(position);
|
|
int flag = ((CraftBlockState) blockstate).getFlag();
|
|
@@ -802,10 +805,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 +881,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public long getGameTime() {
|
|
- return world.levelData.getGameTime();
|
|
+ return this.getHandle().getGameTime(); // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
@@ -1858,7 +1861,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
if (!(entity instanceof CraftEntity craftEntity) || entity.getWorld() != this || sound == null || category == null) return;
|
|
|
|
ClientboundSoundEntityPacket packet = new ClientboundSoundEntityPacket(BuiltInRegistries.SOUND_EVENT.wrapAsHolder(CraftSound.getSoundEffect(sound)), net.minecraft.sounds.SoundSource.valueOf(category.name()), craftEntity.getHandle(), volume, pitch, this.getHandle().getRandom().nextLong());
|
|
- ChunkMap.TrackedEntity entityTracker = this.getHandle().getChunkSource().chunkMap.entityMap.get(entity.getEntityId());
|
|
+ ChunkMap.TrackedEntity entityTracker = ((CraftEntity) entity).getHandle().tracker; // Folia - region threading
|
|
if (entityTracker != null) {
|
|
entityTracker.broadcastAndSend(packet);
|
|
}
|
|
@@ -1869,7 +1872,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
if (!(entity instanceof CraftEntity craftEntity) || entity.getWorld() != this || sound == null || category == null) return;
|
|
|
|
ClientboundSoundEntityPacket packet = new ClientboundSoundEntityPacket(Holder.direct(SoundEvent.createVariableRangeEvent(new ResourceLocation(sound))), net.minecraft.sounds.SoundSource.valueOf(category.name()), craftEntity.getHandle(), volume, pitch, this.getHandle().getRandom().nextLong());
|
|
- ChunkMap.TrackedEntity entityTracker = this.getHandle().getChunkSource().chunkMap.entityMap.get(entity.getEntityId());
|
|
+ ChunkMap.TrackedEntity entityTracker = ((CraftEntity)entity).getHandle().tracker; // Folia - region threading
|
|
if (entityTracker != null) {
|
|
entityTracker.broadcastAndSend(packet);
|
|
}
|
|
@@ -2355,7 +2358,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
// Paper start
|
|
public java.util.concurrent.CompletableFuture<Chunk> getChunkAtAsync(int x, int z, boolean gen, boolean urgent) {
|
|
warnUnsafeChunk("getting a faraway chunk async", x, z); // Paper
|
|
- if (Bukkit.isPrimaryThread()) {
|
|
+ if (io.papermc.paper.util.TickThread.isTickThreadFor(this.getHandle(), x, z)) { // Folia - region threading
|
|
net.minecraft.world.level.chunk.LevelChunk immediate = this.world.getChunkSource().getChunkAtIfLoadedImmediately(x, z);
|
|
if (immediate != null) {
|
|
return java.util.concurrent.CompletableFuture.completedFuture(immediate.getBukkitChunk());
|
|
@@ -2372,7 +2375,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
java.util.concurrent.CompletableFuture<Chunk> ret = new java.util.concurrent.CompletableFuture<>();
|
|
|
|
io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> {
|
|
- net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(this.getHandle(), x, z, () -> { // Folia - region threading
|
|
net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c;
|
|
if (chunk != null) addTicket(x, z); // Paper
|
|
ret.complete(chunk == null ? null : chunk.getBukkitChunk());
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java b/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java
|
|
index fb6454cc64ebc549f61ad7d51efb16ef15f8384d..903408d4d8f9ce5c9566ec96312281ab15f16c2f 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java
|
|
@@ -40,6 +40,7 @@ public final class CapturedBlockState extends CraftBlockState {
|
|
|
|
for (int k = 0; k < j; ++k) {
|
|
Bee entitybee = new Bee(EntityType.BEE, generatoraccessseed.getMinecraftWorld());
|
|
+ entitybee.setPosRaw(blockposition1.getX(), blockposition1.getY(), blockposition1.getZ()); // Folia - region threading - set position so that thread checks do not fail
|
|
|
|
tileentitybeehive.addOccupantWithPresetTicks(entitybee, false, random.nextInt(599));
|
|
}
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java
|
|
index 962c950ca9c7e047a3aec215d4faa73676049d36..ed7c725aa4a5be4d3583dcd17bc57b5d7631e562 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.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
// SPIGOT-6895: Call StructureGrowEvent and BlockFertilizeEvent
|
|
- world.captureTreeGeneration = true;
|
|
+ worldData.captureTreeGeneration = true; // Folia - region threading
|
|
InteractionResult result = BoneMealItem.applyBonemeal(context);
|
|
- world.captureTreeGeneration = false;
|
|
+ worldData.captureTreeGeneration = false; // Folia - region threading
|
|
|
|
- if (world.capturedBlockStates.size() > 0) {
|
|
- TreeType treeType = SaplingBlock.treeType;
|
|
- SaplingBlock.treeType = null;
|
|
- List<BlockState> blocks = new ArrayList<>(world.capturedBlockStates.values());
|
|
- world.capturedBlockStates.clear();
|
|
+ if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading
|
|
+ TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading
|
|
+ SaplingBlock.treeTypeRT.set(null); // Folia - region threading
|
|
+ List<BlockState> 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..78f7e72f2912dae503c2dab7d1992b652b404ae5 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java
|
|
@@ -50,7 +50,7 @@ public class ConsoleCommandCompleter implements Completer {
|
|
return syncEvent.callEvent() ? syncEvent.getCompletions() : com.google.common.collect.ImmutableList.of();
|
|
}
|
|
};
|
|
- server.getServer().processQueue.add(syncCompletions);
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(syncCompletions); // Folia - region threading
|
|
try {
|
|
final List<String> legacyCompletions = syncCompletions.get();
|
|
completions.removeIf(it -> !legacyCompletions.contains(it.suggestion())); // remove any suggestions that were removed
|
|
@@ -98,7 +98,7 @@ public class ConsoleCommandCompleter implements Completer {
|
|
return tabEvent.isCancelled() ? Collections.EMPTY_LIST : tabEvent.getCompletions();
|
|
}
|
|
};
|
|
- server.getServer().processQueue.add(waitable); // Paper - Remove "this."
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(waitable); // Folia - region threading
|
|
try {
|
|
List<String> offers = waitable.get();
|
|
if (offers == null) {
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
|
|
index 57a0dbb23a32123d30c3b3572f4d129be9d97847..3f46d9398add0e97a2b8b072f66b48b0e056d023 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
|
|
@@ -203,6 +203,16 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
|
|
private EntityDamageEvent lastDamageEvent;
|
|
private final CraftPersistentDataContainer persistentDataContainer = new CraftPersistentDataContainer(CraftEntity.DATA_TYPE_REGISTRY);
|
|
protected net.kyori.adventure.pointer.Pointers adventure$pointers; // Paper - implement pointers
|
|
+ public final io.papermc.paper.threadedregions.EntityScheduler taskScheduler = new io.papermc.paper.threadedregions.EntityScheduler(this); // Folia - region threading
|
|
+
|
|
+ // Folia start - region threading API
|
|
+ private final io.papermc.paper.threadedregions.scheduler.FoliaEntityScheduler apiScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaEntityScheduler(this);
|
|
+
|
|
+ @Override
|
|
+ public final io.papermc.paper.threadedregions.scheduler.EntityScheduler getScheduler() {
|
|
+ return this.apiScheduler;
|
|
+ };
|
|
+ // Folia end - region threading API
|
|
|
|
public CraftEntity(final CraftServer server, final Entity entity) {
|
|
this.server = server;
|
|
@@ -567,6 +577,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
|
|
|
|
@Override
|
|
public boolean teleport(Location location, TeleportCause cause, io.papermc.paper.entity.TeleportFlag... flags) {
|
|
+ // Folia start - region threading
|
|
+ if (true) {
|
|
+ throw new UnsupportedOperationException("Must use teleportAsync while in region threading");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
// Paper end
|
|
Preconditions.checkArgument(location != null, "location cannot be null");
|
|
location.checkFinite();
|
|
@@ -1244,7 +1259,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
|
|
}
|
|
|
|
ServerLevel world = ((CraftWorld) this.getWorld()).getHandle();
|
|
- ChunkMap.TrackedEntity entityTracker = world.getChunkSource().chunkMap.entityMap.get(this.getEntityId());
|
|
+ ChunkMap.TrackedEntity entityTracker = this.getHandle().tracker; // Folia - region threading
|
|
|
|
if (entityTracker == null) {
|
|
return;
|
|
@@ -1308,30 +1323,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<Boolean> 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 e769c1c2a99a13941124c7442d44a509e7859666..867838df8a6144fd00e7cf01eb840ad836d6125b 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
|
|
@@ -564,7 +564,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
|
|
|
|
@Override
|
|
public void kickPlayer(String message) {
|
|
- org.spigotmc.AsyncCatcher.catchOp("player kick"); // Spigot
|
|
+ //org.spigotmc.AsyncCatcher.catchOp("player kick"); // Spigot // Folia - thread-safe now, as it will simply delay the kick
|
|
if (this.getHandle().connection == null) return;
|
|
|
|
this.getHandle().connection.disconnect(message == null ? "" : message, org.bukkit.event.player.PlayerKickEvent.Cause.PLUGIN); // Paper - kick event cause
|
|
@@ -1779,7 +1779,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());
|
|
}
|
|
@@ -1858,7 +1858,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
|
|
this.getHandle().connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(otherPlayer)));
|
|
}
|
|
|
|
- ChunkMap.TrackedEntity entry = tracker.entityMap.get(other.getId());
|
|
+ ChunkMap.TrackedEntity entry = other.tracker; // Folia - region threading
|
|
if (entry != null && !entry.seenBy.contains(this.getHandle().connection)) {
|
|
entry.updatePlayer(this.getHandle());
|
|
}
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
|
|
index cdb8ec04f4a19ec3dbedbd5b17a7d1f3afaa238e..49c68ee30436232663b8d4dd15991e19b5f59d09 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
|
|
@@ -227,8 +227,8 @@ import org.bukkit.potion.PotionEffect;
|
|
import org.bukkit.event.entity.SpawnerSpawnEvent; // Spigot
|
|
|
|
public class CraftEventFactory {
|
|
- public static org.bukkit.block.Block blockDamage; // For use in EntityDamageByBlockEvent
|
|
- public static Entity entityDamage; // For use in EntityDamageByEntityEvent
|
|
+ public static final ThreadLocal<org.bukkit.block.Block> blockDamageRT = new ThreadLocal<>(); // For use in EntityDamageByBlockEvent // Folia - region threading
|
|
+ public static final ThreadLocal<Entity> entityDamageRT = new ThreadLocal<>(); // For use in EntityDamageByEntityEvent // Folia - region threading
|
|
|
|
// helper methods
|
|
private static boolean canBuild(ServerLevel world, Player player, int x, int z) {
|
|
@@ -841,7 +841,7 @@ public class CraftEventFactory {
|
|
return CraftEventFactory.handleBlockSpreadEvent(world, source, target, block, 2);
|
|
}
|
|
|
|
- public static BlockPos sourceBlockOverride = null; // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep.
|
|
+ public static ThreadLocal<BlockPos> sourceBlockOverrideRT = new ThreadLocal<>(); // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // Folia - region threading
|
|
public static boolean handleBlockSpreadEvent(LevelAccessor world, BlockPos source, BlockPos target, net.minecraft.world.level.block.state.BlockState block, int flag) {
|
|
// Suppress during worldgen
|
|
if (!(world instanceof Level)) {
|
|
@@ -852,7 +852,7 @@ public class CraftEventFactory {
|
|
CraftBlockState state = CraftBlockStates.getBlockState(world, target, flag);
|
|
state.setData(block);
|
|
|
|
- BlockSpreadEvent event = new BlockSpreadEvent(state.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverride != null ? CraftEventFactory.sourceBlockOverride : source), state);
|
|
+ BlockSpreadEvent event = new BlockSpreadEvent(state.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverrideRT.get() != null ? CraftEventFactory.sourceBlockOverrideRT.get() : source), state); // Folia - region threading
|
|
Bukkit.getPluginManager().callEvent(event);
|
|
|
|
if (!event.isCancelled()) {
|
|
@@ -967,8 +967,8 @@ public class CraftEventFactory {
|
|
private static EntityDamageEvent handleEntityDamageEvent(Entity entity, DamageSource source, Map<DamageModifier, Double> modifiers, Map<DamageModifier, Function<? super Double, Double>> modifierFunctions, boolean cancelled) {
|
|
if (source.is(DamageTypeTags.IS_EXPLOSION)) {
|
|
DamageCause damageCause;
|
|
- Entity damager = CraftEventFactory.entityDamage;
|
|
- CraftEventFactory.entityDamage = null;
|
|
+ Entity damager = CraftEventFactory.entityDamageRT.get(); // Folia - region threading
|
|
+ CraftEventFactory.entityDamageRT.set(null); // Folia - region threading
|
|
EntityDamageEvent event;
|
|
if (damager == null) {
|
|
event = new EntityDamageByBlockEvent(null, entity.getBukkitEntity(), DamageCause.BLOCK_EXPLOSION, modifiers, modifierFunctions);
|
|
@@ -1024,13 +1024,13 @@ public class CraftEventFactory {
|
|
}
|
|
return event;
|
|
} else if (source.is(DamageTypes.LAVA)) {
|
|
- EntityDamageEvent event = (new EntityDamageByBlockEvent(CraftEventFactory.blockDamage, entity.getBukkitEntity(), DamageCause.LAVA, modifiers, modifierFunctions));
|
|
+ EntityDamageEvent event = (new EntityDamageByBlockEvent(CraftEventFactory.blockDamageRT.get(), entity.getBukkitEntity(), DamageCause.LAVA, modifiers, modifierFunctions)); // Folia - region threading
|
|
event.setCancelled(cancelled);
|
|
|
|
- Block damager = CraftEventFactory.blockDamage;
|
|
- CraftEventFactory.blockDamage = null; // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call
|
|
+ Block damager = CraftEventFactory.blockDamageRT.get();
|
|
+ CraftEventFactory.blockDamageRT.set(null); // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call // Folia - region threading
|
|
CraftEventFactory.callEvent(event);
|
|
- CraftEventFactory.blockDamage = damager; // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause
|
|
+ CraftEventFactory.blockDamageRT.set(damager); // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause // Folia - region threading
|
|
|
|
if (!event.isCancelled()) {
|
|
event.getEntity().setLastDamageCause(event);
|
|
@@ -1038,9 +1038,9 @@ public class CraftEventFactory {
|
|
entity.lastDamageCancelled = true; // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Keep track if the event was canceled
|
|
}
|
|
return event;
|
|
- } else if (CraftEventFactory.blockDamage != null) {
|
|
+ } else if (CraftEventFactory.blockDamageRT.get() != null) { // Folia - region threading
|
|
DamageCause cause = null;
|
|
- Block damager = CraftEventFactory.blockDamage;
|
|
+ Block damager = CraftEventFactory.blockDamageRT.get(); // Folia - region threading
|
|
if (source.is(DamageTypes.CACTUS) || source.is(DamageTypes.SWEET_BERRY_BUSH) || source.is(DamageTypes.STALAGMITE) || source.is(DamageTypes.FALLING_STALACTITE) || source.is(DamageTypes.FALLING_ANVIL)) {
|
|
cause = DamageCause.CONTACT;
|
|
} else if (source.is(DamageTypes.HOT_FLOOR)) {
|
|
@@ -1055,9 +1055,9 @@ public class CraftEventFactory {
|
|
EntityDamageEvent event = new EntityDamageByBlockEvent(damager, entity.getBukkitEntity(), cause, modifiers, modifierFunctions);
|
|
event.setCancelled(cancelled);
|
|
|
|
- CraftEventFactory.blockDamage = null; // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call
|
|
+ CraftEventFactory.blockDamageRT.set(null); // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call // Folia - region threading
|
|
CraftEventFactory.callEvent(event);
|
|
- CraftEventFactory.blockDamage = damager; // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause
|
|
+ CraftEventFactory.blockDamageRT.set(damager); // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause // Folia - region threading
|
|
|
|
if (!event.isCancelled()) {
|
|
event.getEntity().setLastDamageCause(event);
|
|
@@ -1065,10 +1065,10 @@ public class CraftEventFactory {
|
|
entity.lastDamageCancelled = true; // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Keep track if the event was canceled
|
|
}
|
|
return event;
|
|
- } else if (CraftEventFactory.entityDamage != null) {
|
|
+ } else if (CraftEventFactory.entityDamageRT.get() != null) { // Folia - region threading
|
|
DamageCause cause = null;
|
|
- CraftEntity damager = CraftEventFactory.entityDamage.getBukkitEntity();
|
|
- CraftEventFactory.entityDamage = null;
|
|
+ CraftEntity damager = CraftEventFactory.entityDamageRT.get().getBukkitEntity(); // Folia - region threading
|
|
+ CraftEventFactory.entityDamageRT.set(null); // Folia - region threading
|
|
if (source.is(DamageTypes.FALLING_STALACTITE) || source.is(DamageTypes.FALLING_BLOCK) || source.is(DamageTypes.FALLING_ANVIL)) {
|
|
cause = DamageCause.FALLING_BLOCK;
|
|
} else if (damager instanceof LightningStrike) {
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
|
|
index cdefb2025eedea7e204d70d568adaf1c1ec4c03c..9136fb30db749737e9f189d0901024fcad02e402 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
|
|
@@ -533,6 +533,7 @@ public class CraftScheduler implements BukkitScheduler {
|
|
}
|
|
|
|
protected CraftTask handle(final CraftTask task, final long delay) { // Paper
|
|
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
|
|
// Paper start
|
|
if (!this.isAsyncScheduler && !task.isSync()) {
|
|
this.asyncScheduler.handle(task, delay);
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
|
|
index 64c50c52c11214740de7903e5592b8b6b2c170b3..9f5e8a21de3785721897cf8cf75b17ce3968cb7e 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
|
|
@@ -373,6 +373,12 @@ public final class CraftMagicNumbers implements UnsafeValues {
|
|
String minimumVersion = MinecraftServer.getServer().server.minimumAPI;
|
|
int minimumIndex = CraftMagicNumbers.SUPPORTED_API.indexOf(minimumVersion);
|
|
|
|
+ // Folia start - block plugins not marked as supported
|
|
+ if (!pdf.isFoliaSupported()) {
|
|
+ throw new InvalidPluginException("Plugin " + pdf.getFullName() + " is not marked as supporting regionised multithreading");
|
|
+ }
|
|
+ // Folia end - block plugins not marked as supported
|
|
+
|
|
if (pdf.getAPIVersion() != null) {
|
|
int pluginIndex = CraftMagicNumbers.SUPPORTED_API.indexOf(pdf.getAPIVersion());
|
|
|
|
diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java
|
|
index e881584d38dc354204479863f004e974a0ac6c07..88536769e7605fafdf54494d0ebf17f306af8278 100644
|
|
--- a/src/main/java/org/spigotmc/ActivationRange.java
|
|
+++ b/src/main/java/org/spigotmc/ActivationRange.java
|
|
@@ -65,26 +65,27 @@ public class ActivationRange
|
|
|
|
private static int checkInactiveWakeup(Entity entity) {
|
|
Level world = entity.level;
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions
|
|
SpigotWorldConfig config = world.spigotConfig;
|
|
- long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
|
|
+ long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions
|
|
if (entity.activationType == ActivationType.VILLAGER) {
|
|
- if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) {
|
|
- world.wakeupInactiveRemainingVillagers--;
|
|
+ if (inactiveFor > config.wakeUpInactiveVillagersEvery && worldData.wakeupInactiveRemainingVillagers > 0) { // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingVillagers--; // Folia - threaded regions
|
|
return config.wakeUpInactiveVillagersFor;
|
|
}
|
|
} else if (entity.activationType == ActivationType.ANIMAL) {
|
|
- if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) {
|
|
- world.wakeupInactiveRemainingAnimals--;
|
|
+ if (inactiveFor > config.wakeUpInactiveAnimalsEvery && worldData.wakeupInactiveRemainingAnimals > 0) { // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingAnimals--; // Folia - threaded regions
|
|
return config.wakeUpInactiveAnimalsFor;
|
|
}
|
|
} else if (entity.activationType == ActivationType.FLYING_MONSTER) {
|
|
- if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) {
|
|
- world.wakeupInactiveRemainingFlying--;
|
|
+ if (inactiveFor > config.wakeUpInactiveFlyingEvery && worldData.wakeupInactiveRemainingFlying > 0) { // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingFlying--; // Folia - threaded regions
|
|
return config.wakeUpInactiveFlyingFor;
|
|
}
|
|
} else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) {
|
|
- if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) {
|
|
- world.wakeupInactiveRemainingMonsters--;
|
|
+ if (inactiveFor > config.wakeUpInactiveMonstersEvery && worldData.wakeupInactiveRemainingMonsters > 0) { // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingMonsters--; // Folia - threaded regions
|
|
return config.wakeUpInactiveMonstersFor;
|
|
}
|
|
}
|
|
@@ -174,10 +175,11 @@ public class ActivationRange
|
|
final int waterActivationRange = world.spigotConfig.waterActivationRange;
|
|
final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange;
|
|
final int villagerActivationRange = world.spigotConfig.villagerActivationRange;
|
|
- world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals);
|
|
- world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers);
|
|
- world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters);
|
|
- world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying);
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingAnimals = Math.min(worldData.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingVillagers = Math.min(worldData.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingMonsters = Math.min(worldData.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingFlying = Math.min(worldData.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); // Folia - threaded regions
|
|
final ServerChunkCache chunkProvider = (ServerChunkCache) world.getChunkSource();
|
|
// Paper end
|
|
|
|
@@ -191,9 +193,9 @@ public class ActivationRange
|
|
// Paper end
|
|
maxRange = Math.min( ( world.spigotConfig.simulationDistance << 4 ) - 8, maxRange );
|
|
|
|
- for ( Player player : world.players() )
|
|
+ for ( Player player : world.getLocalPlayers() ) // Folia - region threading
|
|
{
|
|
- player.activatedTick = MinecraftServer.currentTick;
|
|
+ player.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading
|
|
if ( world.spigotConfig.ignoreSpectatorActivation && player.isSpectator() )
|
|
{
|
|
continue;
|
|
@@ -215,7 +217,7 @@ public class ActivationRange
|
|
java.util.List<Entity> 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.RegionizedServer.getCurrentTick() > entity.activatedTick ) // Folia - threaded regions
|
|
{
|
|
if ( entity.defaultActivationState )
|
|
{
|
|
- entity.activatedTick = MinecraftServer.currentTick;
|
|
+ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions
|
|
return;
|
|
}
|
|
if ( entity.activationType.boundingBox.intersects( entity.getBoundingBox() ) )
|
|
{
|
|
- entity.activatedTick = MinecraftServer.currentTick;
|
|
+ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions
|
|
}
|
|
}
|
|
}
|
|
@@ -261,10 +263,10 @@ public class ActivationRange
|
|
if (entity.remainingFireTicks > 0) {
|
|
return 2;
|
|
}
|
|
- if (entity.activatedImmunityTick >= MinecraftServer.currentTick) {
|
|
+ if (entity.activatedImmunityTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) { // Folia - threaded regions
|
|
return 1;
|
|
}
|
|
- long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
|
|
+ long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions
|
|
// Paper end
|
|
// quick checks.
|
|
if ( (entity.activationType != ActivationType.WATER && entity.wasTouchingWater && entity.isPushedByFluid()) ) // Paper
|
|
@@ -387,19 +389,19 @@ public class ActivationRange
|
|
}
|
|
// Paper end
|
|
|
|
- boolean isActive = entity.activatedTick >= MinecraftServer.currentTick;
|
|
+ boolean isActive = entity.activatedTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions
|
|
entity.isTemporarilyActive = false; // Paper
|
|
|
|
// Should this entity tick?
|
|
if ( !isActive )
|
|
{
|
|
- if ( ( MinecraftServer.currentTick - entity.activatedTick - 1 ) % 20 == 0 )
|
|
+ if ( ( io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick - 1 ) % 20 == 0 ) // Folia - threaded regions
|
|
{
|
|
// Check immunities every 20 ticks.
|
|
// Paper start
|
|
int immunity = checkEntityImmunities(entity);
|
|
if (immunity >= 0) {
|
|
- entity.activatedTick = MinecraftServer.currentTick + immunity;
|
|
+ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + immunity; // Folia - threaded regions
|
|
} else {
|
|
entity.isTemporarilyActive = true;
|
|
}
|
|
diff --git a/src/main/java/org/spigotmc/SpigotCommand.java b/src/main/java/org/spigotmc/SpigotCommand.java
|
|
index 3112a8695639c402e9d18710acbc11cff5611e9c..b0249d9b61819d4a259753150df795a84ce1d4ff 100644
|
|
--- a/src/main/java/org/spigotmc/SpigotCommand.java
|
|
+++ b/src/main/java/org/spigotmc/SpigotCommand.java
|
|
@@ -29,6 +29,7 @@ public class SpigotCommand extends Command {
|
|
Command.broadcastCommandMessage(sender, ChatColor.RED + "Please note that this command is not supported and may cause issues.");
|
|
Command.broadcastCommandMessage(sender, ChatColor.RED + "If you encounter any issues please use the /stop command to restart your server.");
|
|
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
|
|
MinecraftServer console = MinecraftServer.getServer();
|
|
org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings"));
|
|
for (ServerLevel world : console.getAllLevels()) {
|
|
@@ -37,6 +38,7 @@ public class SpigotCommand extends Command {
|
|
console.server.reloadCount++;
|
|
|
|
Command.broadcastCommandMessage(sender, ChatColor.GREEN + "Reload complete.");
|
|
+ }); // Folia - region threading
|
|
}
|
|
|
|
return true;
|
|
diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java
|
|
index 612c3169c3463d702b85975e1db79ae6e47d60d0..6f77134ba451e7bd6bcba1000134ce8a2c762979 100644
|
|
--- a/src/main/java/org/spigotmc/SpigotConfig.java
|
|
+++ b/src/main/java/org/spigotmc/SpigotConfig.java
|
|
@@ -228,7 +228,7 @@ public class SpigotConfig
|
|
SpigotConfig.restartOnCrash = SpigotConfig.getBoolean( "settings.restart-on-crash", SpigotConfig.restartOnCrash );
|
|
SpigotConfig.restartScript = SpigotConfig.getString( "settings.restart-script", SpigotConfig.restartScript );
|
|
SpigotConfig.restartMessage = SpigotConfig.transform( SpigotConfig.getString( "messages.restart", "Server is restarting" ) );
|
|
- SpigotConfig.commands.put( "restart", new RestartCommand( "restart" ) );
|
|
+ //SpigotConfig.commands.put( "restart", new RestartCommand( "restart" ) ); // Folia - region threading
|
|
// WatchdogThread.doStart( timeoutTime, restartOnCrash ); // Paper - moved to after paper config initialization
|
|
}
|
|
|
|
@@ -283,7 +283,7 @@ public class SpigotConfig
|
|
|
|
private static void tpsCommand()
|
|
{
|
|
- SpigotConfig.commands.put( "tps", new TicksPerSecondCommand( "tps" ) );
|
|
+ //SpigotConfig.commands.put( "tps", new TicksPerSecondCommand( "tps" ) ); // Folia - region threading
|
|
}
|
|
|
|
public static int playerSample;
|
|
diff --git a/src/main/java/org/spigotmc/SpigotWorldConfig.java b/src/main/java/org/spigotmc/SpigotWorldConfig.java
|
|
index 5503ad6a93d331771a0e92c0da6adedf2ac81aff..a06408a10cb24c203cfc25f25ccd37ac1a587a1a 100644
|
|
--- a/src/main/java/org/spigotmc/SpigotWorldConfig.java
|
|
+++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java
|
|
@@ -425,7 +425,7 @@ public class SpigotWorldConfig
|
|
this.otherMultiplier = (float) this.getDouble( "hunger.other-multiplier", 0.0 );
|
|
}
|
|
|
|
- public int currentPrimedTnt = 0;
|
|
+ //public int currentPrimedTnt = 0; // Folia - region threading - moved to regionised world data
|
|
public int maxTntTicksPerTick;
|
|
private void maxTntPerTick() {
|
|
if ( SpigotConfig.version < 7 )
|