mirror of
https://github.com/PaperMC/Folia.git
synced 2024-11-25 12:35:23 +01:00
1e5e2154c9
Just need to change the executeBlocking() call to use the global region to execute the command instead.
22963 lines
1.1 MiB
22963 lines
1.1 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
|
|
|
|
See https://docs.papermc.io/folia/reference/overview and
|
|
https://docs.papermc.io/folia/reference/region-logic
|
|
|
|
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..0ce825d7af2a1dbeac5c22640534ee1901edce20
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java
|
|
@@ -0,0 +1,543 @@
|
|
+package ca.spottedleaf.concurrentutil.scheduler;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.TimeUtil;
|
|
+import com.mojang.logging.LogUtils;
|
|
+import io.papermc.paper.util.TraceUtil;
|
|
+import io.papermc.paper.util.set.LinkedSortedSet;
|
|
+import org.slf4j.Logger;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.BitSet;
|
|
+import java.util.Comparator;
|
|
+import java.util.PriorityQueue;
|
|
+import java.util.concurrent.ThreadFactory;
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
+import java.util.concurrent.atomic.AtomicLong;
|
|
+import java.util.concurrent.locks.LockSupport;
|
|
+import java.util.function.BooleanSupplier;
|
|
+
|
|
+public class SchedulerThreadPool {
|
|
+
|
|
+ private static final Logger LOGGER = LogUtils.getLogger();
|
|
+
|
|
+ public static final long DEADLINE_NOT_SET = Long.MIN_VALUE;
|
|
+
|
|
+ private static final Comparator<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;
|
|
+ }
|
|
+
|
|
+ public void dumpAliveThreadTraces(final String reason) {
|
|
+ for (final Thread thread : this.threads) {
|
|
+ if (thread.isAlive()) {
|
|
+ TraceUtil.dumpTraceForThread(thread, reason);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns an array of the underlying scheduling threads.
|
|
+ */
|
|
+ public Thread[] getThreads() {
|
|
+ return this.threads.clone();
|
|
+ }
|
|
+
|
|
+ private void insertFresh(final SchedulableTick task) {
|
|
+ final TickThreadRunner[] runners = this.runners;
|
|
+
|
|
+ final int firstIdleThread = this.idleThreads.nextSetBit(0);
|
|
+
|
|
+ if (firstIdleThread != -1) {
|
|
+ // push to idle thread
|
|
+ this.idleThreads.clear(firstIdleThread);
|
|
+ final TickThreadRunner runner = runners[firstIdleThread];
|
|
+ task.awaitingLink = this.awaiting.addLast(task);
|
|
+ runner.acceptTask(task);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // try to replace the last awaiting task
|
|
+ final SchedulableTick last = this.awaiting.last();
|
|
+
|
|
+ if (last != null && TICK_COMPARATOR_BY_TIME.compare(task, last) < 0) {
|
|
+ // need to replace the last task
|
|
+ this.awaiting.pollLast();
|
|
+ last.awaitingLink = null;
|
|
+ task.awaitingLink = this.awaiting.addLast(task);
|
|
+ // need to add task to queue to be picked up later
|
|
+ this.queued.add(last);
|
|
+
|
|
+ final TickThreadRunner runner = last.ownedBy;
|
|
+ runner.replaceTask(task);
|
|
+
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // add to queue, will be picked up later
|
|
+ this.queued.add(task);
|
|
+ }
|
|
+
|
|
+ private void takeTask(final TickThreadRunner runner, final SchedulableTick tick) {
|
|
+ if (!this.awaiting.remove(tick.awaitingLink)) {
|
|
+ throw new IllegalStateException("Task is not in awaiting");
|
|
+ }
|
|
+ tick.awaitingLink = null;
|
|
+ }
|
|
+
|
|
+ private SchedulableTick returnTask(final TickThreadRunner runner, final SchedulableTick reschedule) {
|
|
+ if (reschedule != null) {
|
|
+ this.queued.add(reschedule);
|
|
+ }
|
|
+ final SchedulableTick ret = this.queued.poll();
|
|
+ if (ret == null) {
|
|
+ this.idleThreads.set(runner.id);
|
|
+ } else {
|
|
+ ret.awaitingLink = this.awaiting.addLast(ret);
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ public void schedule(final SchedulableTick task) {
|
|
+ synchronized (this.scheduleLock) {
|
|
+ if (!task.tryMarkScheduled()) {
|
|
+ throw new IllegalStateException("Task " + task + " is already scheduled or cancelled");
|
|
+ }
|
|
+
|
|
+ task.schedulerOwnedBy = this;
|
|
+
|
|
+ this.insertFresh(task);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public boolean updateTickStartToMax(final SchedulableTick task, final long newStart) {
|
|
+ synchronized (this.scheduleLock) {
|
|
+ if (TimeUtil.compareTimes(newStart, task.getScheduledStart()) <= 0) {
|
|
+ return false;
|
|
+ }
|
|
+ if (this.queued.remove(task)) {
|
|
+ task.setScheduledStart(newStart);
|
|
+ this.queued.add(task);
|
|
+ return true;
|
|
+ }
|
|
+ if (task.awaitingLink != null) {
|
|
+ this.awaiting.remove(task.awaitingLink);
|
|
+ task.awaitingLink = null;
|
|
+
|
|
+ // re-queue task
|
|
+ task.setScheduledStart(newStart);
|
|
+ this.queued.add(task);
|
|
+
|
|
+ // now we need to replace the task the runner was waiting for
|
|
+ final TickThreadRunner runner = task.ownedBy;
|
|
+ final SchedulableTick replace = this.queued.poll();
|
|
+
|
|
+ // replace cannot be null, since we have added a task to queued
|
|
+ if (replace != task) {
|
|
+ runner.replaceTask(replace);
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns {@code null} if the task is not scheduled, returns {@code TRUE} if the task was cancelled
|
|
+ * and was queued to execute, returns {@code FALSE} if the task was cancelled but was executing.
|
|
+ */
|
|
+ public Boolean tryRetire(final SchedulableTick task) {
|
|
+ if (task.schedulerOwnedBy != this) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ synchronized (this.scheduleLock) {
|
|
+ if (this.queued.remove(task)) {
|
|
+ // cancelled, and no runner owns it - so return
|
|
+ return Boolean.TRUE;
|
|
+ }
|
|
+ if (task.awaitingLink != null) {
|
|
+ this.awaiting.remove(task.awaitingLink);
|
|
+ task.awaitingLink = null;
|
|
+ // here we need to replace the task the runner was waiting for
|
|
+ final TickThreadRunner runner = task.ownedBy;
|
|
+ final SchedulableTick replace = this.queued.poll();
|
|
+
|
|
+ if (replace == null) {
|
|
+ // nothing to replace with, set to idle
|
|
+ this.idleThreads.set(runner.id);
|
|
+ runner.forceIdle();
|
|
+ } else {
|
|
+ runner.replaceTask(replace);
|
|
+ }
|
|
+
|
|
+ return Boolean.TRUE;
|
|
+ }
|
|
+
|
|
+ // could not find it in queue
|
|
+ return task.tryMarkCancelled() ? Boolean.FALSE : null;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void notifyTasks(final SchedulableTick task) {
|
|
+ // Not implemented
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Represents a tickable task that can be scheduled into a {@link SchedulerThreadPool}.
|
|
+ * <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/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 e4fd372a1d585887287253a02531cd192929377b..772e3a864e0e70288a1c010d8bbb809d34d16a41 100644
|
|
--- a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
|
|
+++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
|
|
@@ -100,7 +100,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>() {
|
|
@@ -180,7 +180,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 23432eea862c6df716d7726a32da3a0612a3fb77..f59e8bb72c5233f26a8a0d506ac64bb37fef97a5 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 95e5073a68e4dd38b70e8268daf2160922c3a12f..19349637e4a38e697debe6d18ab8b759ede0359e 100644
|
|
--- a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
|
|
+++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java
|
|
@@ -92,6 +92,9 @@ public final class ChunkSystem {
|
|
for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) {
|
|
chunkMap.regionManagers.get(index).addChunk(holder.getPos().x, holder.getPos().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) {
|
|
@@ -99,34 +102,39 @@ public final class ChunkSystem {
|
|
for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) {
|
|
chunkMap.regionManagers.get(index).removeChunk(holder.getPos().x, holder.getPos().z);
|
|
}
|
|
+ // Folia start - threaded regions
|
|
+ level.regioniser.removeChunk(holder.pos.x, holder.pos.z);
|
|
+ // Folia end - threaded regions
|
|
}
|
|
|
|
public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
|
|
chunk.playerChunk = holder;
|
|
chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.FULL;
|
|
+ chunk.level.getCurrentWorldData().addChunk(chunk); // Folia - region threading
|
|
}
|
|
|
|
public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
|
|
chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.INACCESSIBLE;
|
|
+ chunk.level.getCurrentWorldData().removeChunk(chunk); // Folia - region threading
|
|
}
|
|
|
|
public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
|
|
- chunk.level.getChunkSource().tickingChunks.add(chunk);
|
|
+ chunk.level.getCurrentWorldData().addTickingChunk(chunk); // Folia - region threading
|
|
chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.BLOCK_TICKING;
|
|
}
|
|
|
|
public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
|
|
- chunk.level.getChunkSource().tickingChunks.remove(chunk);
|
|
+ chunk.level.getCurrentWorldData().removeTickingChunk(chunk); // Folia - region threading
|
|
chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.FULL;
|
|
}
|
|
|
|
public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
|
|
- chunk.level.getChunkSource().entityTickingChunks.add(chunk);
|
|
+ chunk.level.getCurrentWorldData().addEntityTickingChunk(chunk); // Folia - region threading
|
|
chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.ENTITY_TICKING;
|
|
}
|
|
|
|
public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
|
|
- chunk.level.getChunkSource().entityTickingChunks.remove(chunk);
|
|
+ chunk.level.getCurrentWorldData().removeEntityTickingChunk(chunk); // Folia - region threading
|
|
chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.BLOCK_TICKING;
|
|
}
|
|
|
|
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 1b090f1e79b996e52097afc49c1cec85936653e6..07abf5a326cc7aa8a449b74bd7ac8a43b98528c0 100644
|
|
--- a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java
|
|
+++ b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java
|
|
@@ -141,6 +141,7 @@ public class RegionizedPlayerChunkLoader {
|
|
public void updatePlayer(final ServerPlayer player) {
|
|
final PlayerChunkLoaderData loader = player.chunkLoader;
|
|
if (loader != null) {
|
|
+ player.serverLevel().chunkSource.chunkMap.getNearbyPlayers().tickPlayer(player); // Folia - region threading
|
|
loader.update();
|
|
}
|
|
}
|
|
@@ -154,7 +155,7 @@ public class RegionizedPlayerChunkLoader {
|
|
final PlayerChunkLoaderData loader = player.chunkLoader;
|
|
|
|
if (loader == null) {
|
|
- return;
|
|
+ throw new IllegalStateException("Player is already removed from player chunk loader"); // Folia - region threading
|
|
}
|
|
|
|
loader.remove();
|
|
@@ -233,7 +234,7 @@ public class RegionizedPlayerChunkLoader {
|
|
public void tick() {
|
|
TickThread.ensureTickThread("Cannot tick player chunk loader async");
|
|
long currTime = System.nanoTime();
|
|
- for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) {
|
|
+ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.getLocalPlayers())) { // Folia - region threding
|
|
final PlayerChunkLoaderData loader = player.chunkLoader;
|
|
if (loader == null || loader.world != this.world) {
|
|
// not our problem anymore
|
|
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 15ee41452992714108efe53b708b5a4e1da7c1ff..5bef4f50082e56b89239cfd62dd7429926b71c09 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
|
|
@@ -191,7 +191,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
|
|
}
|
|
|
|
public Entity[] getAllCopy() {
|
|
@@ -277,7 +282,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);
|
|
}
|
|
|
|
@@ -291,7 +298,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);
|
|
}
|
|
}
|
|
@@ -434,6 +443,8 @@ 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;
|
|
@@ -450,6 +461,19 @@ public final class EntityLookup implements LevelEntityGetter<Entity> {
|
|
return slices == null || !slices.isPreventingStatusUpdates();
|
|
}
|
|
|
|
+ // 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;
|
|
@@ -864,12 +888,18 @@ public final class EntityLookup implements LevelEntityGetter<Entity> {
|
|
@Override
|
|
public void onMove() {
|
|
final Entity entity = this.entity;
|
|
+ final io.papermc.paper.threadedregions.RegionizedWorldData regionData = entity.level().getCurrentWorldData(); // Folia - region threading
|
|
final Visibility oldVisibility = getEntityStatus(entity);
|
|
final ChunkEntitySlices newSlices = EntityLookup.this.moveEntity(this.entity);
|
|
if (newSlices == null) {
|
|
// no new section, so didn't change sections
|
|
return;
|
|
}
|
|
+ // Folia start - region threading
|
|
+ if (entity instanceof net.minecraft.server.level.ServerPlayer player) {
|
|
+ regionData.getNearbyPlayers().tickPlayer(player);
|
|
+ }
|
|
+ // Folia end - region threading
|
|
final Visibility newVisibility = getEntityStatus(entity);
|
|
|
|
EntityLookup.this.entityStatusChange(entity, newSlices, oldVisibility, newVisibility, true, false, false);
|
|
@@ -886,6 +916,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 abd0217cf0bff183c8e262edc173a53403797c1a..40411b335e99f67d6a82e70db6e5e4c0372102ec 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
|
|
@@ -53,6 +53,14 @@ import java.util.concurrent.atomic.AtomicReference;
|
|
import java.util.concurrent.locks.LockSupport;
|
|
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 it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
|
|
+// Folia end - region threading
|
|
+
|
|
public final class ChunkHolderManager {
|
|
|
|
private static final Logger LOGGER = LogUtils.getClassLogger();
|
|
@@ -112,27 +120,92 @@ public final class ChunkHolderManager {
|
|
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);
|
|
+ });
|
|
+
|
|
+ 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 int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
|
|
+ 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;
|
|
|
|
- if (saveTickCompare != 0) {
|
|
- return saveTickCompare;
|
|
+ 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
|
|
+ }
|
|
}
|
|
+ }
|
|
|
|
- final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
|
|
- final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
|
|
+ private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() {
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region =
|
|
+ TickRegionScheduler.getCurrentRegion();
|
|
|
|
- if (coord1 == coord2) {
|
|
- throw new IllegalStateException("Duplicate chunkholder in auto save queue");
|
|
+ if (region == null) {
|
|
+ return null;
|
|
}
|
|
|
|
- return Long.compare(coord1, coord2);
|
|
- });
|
|
+ if (this.world != null && this.world != region.getData().world) {
|
|
+ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey());
|
|
+ }
|
|
+
|
|
+ return region.getData().getHolderManagerRegionData();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
|
|
public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
|
|
this.world = world;
|
|
@@ -166,8 +239,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() + "'");
|
|
@@ -177,9 +255,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();
|
|
}
|
|
@@ -200,27 +279,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(); // Folia - region threading
|
|
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); // Folia - region threading
|
|
|
|
holder.lastAutoSave = currentTick;
|
|
if (holder.save(false, false) != null) {
|
|
@@ -234,15 +320,38 @@ public final class ChunkHolderManager {
|
|
|
|
for (final NewChunkHolder holder : reschedule) {
|
|
if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) {
|
|
- this.autoSaveQueue.add(holder);
|
|
+ regionData.autoSaveQueue.add(holder); // Folia start - region threading
|
|
}
|
|
}
|
|
}
|
|
|
|
public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) {
|
|
- final List<NewChunkHolder> holders = this.getChunkHolders();
|
|
+ // 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) {
|
|
+ final List<NewChunkHolder> holders = new java.util.ArrayList<>(this.chunkHolders.size() / 10);
|
|
+ // we could iterate through all chunk holders with thread checks, however for many regions the iteration cost alone
|
|
+ // will multiply. to avoid this, we can simply iterate through all owned sections
|
|
+ final int regionShift = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().regioniser.sectionChunkShift;
|
|
+ for (final LongIterator iterator = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getOwnedSectionsUnsynchronised(); iterator.hasNext();) {
|
|
+ final long sectionKey = iterator.nextLong();
|
|
+ final int width = 1 << regionShift;
|
|
+ final int offsetX = CoordinateUtils.getChunkX(sectionKey) << regionShift;
|
|
+ final int offsetZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift;
|
|
+
|
|
+ for (int dz = 0; dz < width; ++dz) {
|
|
+ for (int dx = 0; dx < width; ++dx) {
|
|
+ final NewChunkHolder holder = this.getChunkHolder(offsetX | dx, offsetZ | dz);
|
|
+ if (holder != null) {
|
|
+ holders.add(holder);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
|
|
- if (logProgress) {
|
|
+ if (first && logProgress) { // Folia - region threading
|
|
LOGGER.info("Saving all chunkholders for world '" + this.world.getWorld().getName() + "'");
|
|
}
|
|
|
|
@@ -250,7 +359,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;
|
|
@@ -261,6 +370,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) {
|
|
@@ -293,7 +408,7 @@ public final class ChunkHolderManager {
|
|
}
|
|
}
|
|
}
|
|
- if (flush) {
|
|
+ if (last && flush) { // Folia - region threading
|
|
RegionFileIOThread.flush();
|
|
if (this.world.paperConfig().chunks.flushRegionsOnSave) {
|
|
try {
|
|
@@ -706,6 +821,13 @@ public final class ChunkHolderManager {
|
|
}
|
|
|
|
public void tick() {
|
|
+ // Folia start - region threading
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region =
|
|
+ TickRegionScheduler.getCurrentRegion();
|
|
+ if (region == null) {
|
|
+ throw new IllegalStateException("Not running tick() while on a region");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
final int sectionShift = TickRegions.getRegionChunkShift();
|
|
|
|
final Predicate<Ticket<?>> expireNow = (final Ticket<?> ticket) -> {
|
|
@@ -715,12 +837,12 @@ public final class ChunkHolderManager {
|
|
return --ticket.removeDelay <= 0L;
|
|
};
|
|
|
|
- for (final Iterator<RegionFileIOThread.ChunkCoordinate> iterator = this.sectionToChunkToExpireCount.keySet().iterator(); iterator.hasNext();) {
|
|
- final RegionFileIOThread.ChunkCoordinate section = iterator.next();
|
|
- final long sectionKey = section.key;
|
|
-
|
|
+ // Folia start - region threading
|
|
+ for (final LongIterator iterator = region.getOwnedSectionsUnsynchronised(); iterator.hasNext();) {
|
|
+ final long sectionKey = iterator.nextLong();
|
|
+ final RegionFileIOThread.ChunkCoordinate section = new RegionFileIOThread.ChunkCoordinate(sectionKey);
|
|
if (!this.sectionToChunkToExpireCount.containsKey(section)) {
|
|
- // removed concurrently
|
|
+ // Folia end - region threading
|
|
continue;
|
|
}
|
|
|
|
@@ -1023,19 +1145,51 @@ 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));
|
|
+ // Folia start - region threading
|
|
+ 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);
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
+ // Folia start - region threading
|
|
+ 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);
|
|
+ // Folia end - region threading
|
|
}
|
|
}
|
|
}
|
|
@@ -1043,8 +1197,9 @@ public final class ChunkHolderManager {
|
|
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
|
|
synchronized (this.chunkHolders) {
|
|
this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ));
|
|
}
|
|
@@ -1058,7 +1213,7 @@ public final class ChunkHolderManager {
|
|
throw new IllegalStateException("Cannot unload chunks recursively");
|
|
}
|
|
final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift
|
|
- final List<ChunkQueue.SectionToUnload> unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions();
|
|
+ final List<ChunkQueue.SectionToUnload> unloadSectionsForRegion = this.unloadQueue.retrieveForCurrentRegion(); // Folia - threaded regions
|
|
int unloadCountTentative = 0;
|
|
for (final ChunkQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
|
|
final ChunkQueue.UnloadSection section
|
|
@@ -1371,7 +1526,13 @@ public final class ChunkHolderManager {
|
|
|
|
// only call on tick thread
|
|
protected final boolean processPendingFullUpdate() {
|
|
- final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
|
|
+ // Folia start - region threading
|
|
+ final HolderManagerRegionData data = this.getCurrentRegionData();
|
|
+ if (data == null) {
|
|
+ return false;
|
|
+ }
|
|
+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = data.pendingFullLoadUpdate;
|
|
+ // Folia end - region threading
|
|
|
|
boolean ret = false;
|
|
|
|
@@ -1382,9 +1543,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); // Folia - region threading
|
|
changedFullStatus.clear();
|
|
}
|
|
}
|
|
@@ -1398,7 +1557,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);
|
|
diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkQueue.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkQueue.java
|
|
index 4cc1b3ba6d093a9683dbd8b7fe76106ae391e019..47bbd5de5849bef5392e3783322a19de5b351beb 100644
|
|
--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkQueue.java
|
|
+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkQueue.java
|
|
@@ -1,5 +1,8 @@
|
|
package io.papermc.paper.chunk.system.scheduling;
|
|
|
|
+import io.papermc.paper.threadedregions.ThreadedRegionizer;
|
|
+import io.papermc.paper.threadedregions.TickRegionScheduler;
|
|
+import io.papermc.paper.threadedregions.TickRegions;
|
|
import it.unimi.dsi.fastutil.HashCommon;
|
|
import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
|
|
import java.util.ArrayList;
|
|
@@ -45,6 +48,39 @@ public final class ChunkQueue {
|
|
return ret;
|
|
}
|
|
|
|
+ // Folia start - threaded regions
|
|
+ public List<SectionToUnload> retrieveForCurrentRegion() {
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region =
|
|
+ TickRegionScheduler.getCurrentRegion();
|
|
+ final ThreadedRegionizer<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> regionizer = region.regioniser;
|
|
+ final int shift = this.coordinateShift;
|
|
+
|
|
+ final List<SectionToUnload> ret = new ArrayList<>();
|
|
+
|
|
+ for (final Map.Entry<Coordinate, UnloadSection> entry : this.unloadSections.entrySet()) {
|
|
+ final Coordinate coord = entry.getKey();
|
|
+ final long key = coord.key;
|
|
+ final UnloadSection section = entry.getValue();
|
|
+ final int sectionX = Coordinate.x(key);
|
|
+ final int sectionZ = Coordinate.z(key);
|
|
+ final int chunkX = sectionX << shift;
|
|
+ final int chunkZ = sectionZ << shift;
|
|
+
|
|
+ if (regionizer.getRegionAtUnsynchronised(chunkX, chunkZ) != region) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ ret.add(new SectionToUnload(sectionX, sectionZ, coord, section.order, section.chunks.size()));
|
|
+ }
|
|
+
|
|
+ ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> {
|
|
+ return Long.compare(s1.order, s2.order);
|
|
+ });
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ // Folia end - threaded regions
|
|
+
|
|
public UnloadSection getSectionUnsynchronized(final int sectionX, final int sectionZ) {
|
|
final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ));
|
|
return this.unloadSections.get(coordinate);
|
|
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 f975cb93716e137d973ff2f9011acdbef58859a2..b84bfc9513a13e365f2bd471b3c77058638d1384 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
|
|
@@ -114,7 +114,7 @@ public final class ChunkTaskScheduler {
|
|
private final PrioritisedThreadPool.PrioritisedPoolExecutor radiusAwareGenExecutor;
|
|
public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor;
|
|
|
|
- private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue();
|
|
+ // Folia - regionised ticking
|
|
|
|
public final ChunkHolderManager chunkHolderManager;
|
|
|
|
@@ -191,7 +191,7 @@ public final class ChunkTaskScheduler {
|
|
// it must be >= ticket propagator section shift so that the ticket propagator can assume that owning a position implies owning
|
|
// the entire section
|
|
// we just take the max, as we want the smallest shift that satifies these properties
|
|
- private static final int LOCK_SHIFT = ThreadedTicketLevelPropagator.SECTION_SHIFT;
|
|
+ private static final int LOCK_SHIFT = Math.max(ThreadedTicketLevelPropagator.SECTION_SHIFT, io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift()); // Folia - region threading
|
|
public static int getChunkSystemLockShift() {
|
|
return LOCK_SHIFT;
|
|
}
|
|
@@ -300,14 +300,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) {
|
|
@@ -327,7 +326,7 @@ public final class ChunkTaskScheduler {
|
|
public void scheduleTickingState(final int chunkX, final int chunkZ, final 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);
|
|
@@ -483,7 +482,7 @@ public final class ChunkTaskScheduler {
|
|
|
|
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);
|
|
@@ -511,7 +510,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);
|
|
@@ -551,7 +550,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 {
|
|
@@ -565,7 +566,7 @@ public final class ChunkTaskScheduler {
|
|
tasks.get(i).schedule();
|
|
}
|
|
|
|
- if (!scheduled) {
|
|
+ if (loadCallback != null && !scheduled) {
|
|
// couldn't schedule
|
|
try {
|
|
loadCallback.accept(chunk);
|
|
@@ -754,7 +755,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
|
|
}
|
|
|
|
/**
|
|
@@ -762,7 +763,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) {
|
|
@@ -771,28 +772,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
|
|
+ }
|
|
+
|
|
+ // 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
|
|
|
|
- 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 - regionised ticking
|
|
|
|
public boolean halt(final boolean sync, final long maxWaitNS) {
|
|
this.radiusAwareGenExecutor.halt();
|
|
@@ -800,6 +806,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.radiusAwareGenExecutor.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 b66a7d4aab887309579154815a0d4abf9de506b0..08075b8895f816420c2a940bf551dfada3c0cd9e 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
|
|
@@ -726,7 +726,7 @@ public final class NewChunkHolder {
|
|
boolean killed;
|
|
|
|
// must hold scheduling lock
|
|
- private void checkUnload() {
|
|
+ void checkUnload() { // Folia - region threading
|
|
if (this.killed) {
|
|
return;
|
|
}
|
|
@@ -1440,7 +1440,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);
|
|
@@ -1483,7 +1483,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);
|
|
@@ -1744,7 +1744,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) {}
|
|
|
|
@@ -1894,7 +1894,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);
|
|
}
|
|
@@ -1942,7 +1942,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;
|
|
@@ -1968,7 +1968,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/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java
|
|
index 7b58b2d6297800c2dcdbf7539e5ab8e7703f39f1..a587d83b78af4efc484f939529acf70834f60d7e 100644
|
|
--- a/src/main/java/io/papermc/paper/command/PaperCommands.java
|
|
+++ b/src/main/java/io/papermc/paper/command/PaperCommands.java
|
|
@@ -19,6 +19,7 @@ public final class PaperCommands {
|
|
COMMANDS.put("paper", new PaperCommand("paper"));
|
|
COMMANDS.put("callback", new CallbackCommand("callback"));
|
|
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/EntityCommand.java b/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java
|
|
index 5f43aedc6596e2b1ac7af9711515714752c262e3..e71265c6f248a909aa3017a3cceacb8c0850d6e2 100644
|
|
--- a/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java
|
|
+++ b/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java
|
|
@@ -129,7 +129,7 @@ public final class EntityCommand implements PaperSubcommand {
|
|
final int z = (e.getKey().z << 4) + 8;
|
|
final Component message = text(" " + e.getValue() + ": " + e.getKey().x + ", " + e.getKey().z + (chunkProviderServer.isPositionTicking(e.getKey().toLong()) ? " (Ticking)" : " (Non-Ticking)"))
|
|
.hoverEvent(HoverEvent.showText(text("Click to teleport to chunk", GREEN)))
|
|
- .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey() + " run tp " + x + " " + (world.getWorld().getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING) + 1) + " " + z));
|
|
+ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey() + " run tp " + x + " " + (128) + " " + z)); // Folia - region threading - avoid sync load here
|
|
sender.sendMessage(message);
|
|
});
|
|
} else {
|
|
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/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 a6f58b3457b7477015c5c6d969e7d83017dd3fa1..057df4b34a9eb252965aa2510b364e444a8f7e4b 100644
|
|
--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
|
|
+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
|
|
@@ -341,4 +341,17 @@ public class GlobalConfiguration extends ConfigurationPart {
|
|
public boolean disableChorusPlantUpdates = false;
|
|
public boolean disableMushroomBlockUpdates = false;
|
|
}
|
|
+
|
|
+ // Folia start - threaded regions
|
|
+ public ThreadedRegions threadedRegions;
|
|
+ public class ThreadedRegions extends ConfigurationPart {
|
|
+
|
|
+ public int threads = -1;
|
|
+
|
|
+ @PostProcess
|
|
+ public void postProcess() {
|
|
+ io.papermc.paper.threadedregions.TickRegions.init(this);
|
|
+ }
|
|
+ }
|
|
+ // Folia end - threaded regions
|
|
}
|
|
diff --git a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java
|
|
index ed79d30f33b2674863b2d73b1abdb48433c33412..f1c4263e1c9ba532450c155acceeab4f663b13c4 100644
|
|
--- a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java
|
|
+++ b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java
|
|
@@ -468,6 +468,14 @@ public class WorldConfiguration extends ConfigurationPart {
|
|
public Chunks chunks;
|
|
|
|
public class Chunks extends ConfigurationPart {
|
|
+
|
|
+ // Folia start - region threading - force prevent moving into unloaded chunks
|
|
+ @PostProcess
|
|
+ public void postProcess() {
|
|
+ this.preventMovingIntoUnloadedChunks = true;
|
|
+ }
|
|
+ // Folia end - region threading - force prevent moving into unloaded chunks
|
|
+
|
|
public AutosavePeriod autoSaveInterval = AutosavePeriod.def();
|
|
public int maxAutoSaveChunksPerTick = 24;
|
|
public int fixedChunkInhabitedTime = -1;
|
|
diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
|
|
index 9c7552968b8c017c71a7a77557a66a03ed89f125..3b5572662c53715cd63772db90904dd1bed32390 100644
|
|
--- a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
|
|
+++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
|
|
@@ -240,10 +240,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/RegionShutdownThread.java b/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..2b48833023771fa965f131890ade98e9da3f5976
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java
|
|
@@ -0,0 +1,175 @@
|
|
+package io.papermc.paper.threadedregions;
|
|
+
|
|
+import com.mojang.logging.LogUtils;
|
|
+import io.papermc.paper.util.TickThread;
|
|
+import net.minecraft.server.MinecraftServer;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.world.entity.Entity;
|
|
+import net.minecraft.world.level.ChunkPos;
|
|
+import org.slf4j.Logger;
|
|
+import java.util.ArrayList;
|
|
+import java.util.List;
|
|
+import java.util.concurrent.TimeUnit;
|
|
+
|
|
+public final class RegionShutdownThread extends TickThread {
|
|
+
|
|
+ private static final Logger LOGGER = LogUtils.getClassLogger();
|
|
+
|
|
+ ThreadedRegionizer.ThreadedRegion<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.setLevel(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");
|
|
+ TickRegions.getScheduler().dumpAliveThreadTraces("Did not shut down in time");
|
|
+ }
|
|
+
|
|
+ MinecraftServer.getServer().stopServer(); // stop part 1: most logic, kicking players, plugins, etc
|
|
+ // halt all chunk systems first so that any in-progress chunk generation stops
|
|
+ LOGGER.info("Halting chunk systems");
|
|
+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
|
|
+ try {
|
|
+ world.chunkTaskScheduler.halt(false, 0L);
|
|
+ } catch (final Throwable throwable) {
|
|
+ LOGGER.error("Failed to soft halt chunk system for world '" + world.getWorld().getName() + "'", throwable);
|
|
+ }
|
|
+ }
|
|
+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
|
|
+ this.haltChunkSystem(world);
|
|
+ }
|
|
+ LOGGER.info("Halted chunk systems");
|
|
+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
|
|
+ final List<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..1e3117ccd51be58d988089207a3efdbff02fc374
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java
|
|
@@ -0,0 +1,455 @@
|
|
+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.common.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.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);
|
|
+ }
|
|
+
|
|
+ public 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 final java.util.Random random = new java.util.Random(4L);
|
|
+ private final List<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void>> walkers =
|
|
+ new java.util.ArrayList<>();
|
|
+ static final int PLAYERS = 500;
|
|
+ static final int RAD_BLOCKS = 1000;
|
|
+ static final int RAD = RAD_BLOCKS >> 4;
|
|
+ static final int RAD_BIG_BLOCKS = 100_000;
|
|
+ static final int RAD_BIG = RAD_BIG_BLOCKS >> 4;
|
|
+ static final int VD = 4 + 12;
|
|
+ static final int BIG_PLAYERS = 250;
|
|
+ static final double WALK_CHANCE = 0.3;
|
|
+ static final double TP_CHANCE = 0.2;
|
|
+ static final double TASK_CHANCE = 0.2;
|
|
+
|
|
+ private ServerLevel getWorld() {
|
|
+ return this.worlds.get(0);
|
|
+ }
|
|
+
|
|
+ private void init2() {
|
|
+ for (int i = 0; i < PLAYERS; ++i) {
|
|
+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD;
|
|
+ int posX = this.random.nextInt(-rad, rad + 1);
|
|
+ int posZ = this.random.nextInt(-rad, rad + 1);
|
|
+
|
|
+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) {
|
|
+ @Override
|
|
+ protected void addCallback(Void parameter, int chunkX, int chunkZ) {
|
|
+ ServerLevel world = RegionizedServer.this.getWorld();
|
|
+ if (RegionizedServer.this.random.nextDouble() <= TASK_CHANCE) {
|
|
+ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {
|
|
+ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {});
|
|
+ });
|
|
+ }
|
|
+ world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel(
|
|
+ net.minecraft.server.level.TicketType.PLAYER, chunkX, chunkZ, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, new net.minecraft.world.level.ChunkPos(posX, posZ)
|
|
+ );
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected void removeCallback(Void parameter, int chunkX, int chunkZ) {
|
|
+ ServerLevel world = RegionizedServer.this.getWorld();
|
|
+ if (RegionizedServer.this.random.nextDouble() <= TASK_CHANCE) {
|
|
+ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {
|
|
+ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {});
|
|
+ });
|
|
+ }
|
|
+ world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel(
|
|
+ net.minecraft.server.level.TicketType.PLAYER, chunkX, chunkZ, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, new net.minecraft.world.level.ChunkPos(posX, posZ)
|
|
+ );
|
|
+ }
|
|
+ };
|
|
+
|
|
+ map.add(posX, posZ, VD);
|
|
+
|
|
+ walkers.add(map);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void randomWalk() {
|
|
+ if (this.walkers.isEmpty()) {
|
|
+ this.init2();
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ for (int i = 0; i < PLAYERS; ++i) {
|
|
+ if (this.random.nextDouble() > WALK_CHANCE) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = this.walkers.get(i);
|
|
+
|
|
+ int updateX = this.random.nextInt(-1, 2);
|
|
+ int updateZ = this.random.nextInt(-1, 2);
|
|
+
|
|
+ map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD);
|
|
+ }
|
|
+
|
|
+ for (int i = 0; i < PLAYERS; ++i) {
|
|
+ if (random.nextDouble() >= TP_CHANCE) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD;
|
|
+ int posX = random.nextInt(-rad, rad + 1);
|
|
+ int posZ = random.nextInt(-rad, rad + 1);
|
|
+
|
|
+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void> map = walkers.get(i);
|
|
+
|
|
+ map.update(posX, posZ, VD);
|
|
+ }
|
|
+ }
|
|
+ */
|
|
+
|
|
+ private void globalTick(final int tickCount) {
|
|
+ /*
|
|
+ if (false) {
|
|
+ io.papermc.paper.threadedregions.ThreadedTicketLevelPropagator.main(null);
|
|
+ }
|
|
+ this.randomWalk();
|
|
+ */
|
|
+ ++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();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static boolean isNotOwnedByGlobalRegion(final Connection conn) {
|
|
+ final PacketListener packetListener = conn.getPacketListener();
|
|
+
|
|
+ if (packetListener instanceof ServerGamePacketListenerImpl gamePacketListener) {
|
|
+ return !gamePacketListener.waitingForSwitchToConfig;
|
|
+ }
|
|
+
|
|
+ if (conn.getPacketListener() instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) {
|
|
+ return configurationPacketListener.switchToMain;
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ 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 (isNotOwnedByGlobalRegion(conn)) {
|
|
+ // we actually require that the owning regions remove the connection for us, as it is possible
|
|
+ // that ownership is transferred back to us
|
|
+ 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 {}", conn.getLoggableAddress(MinecraftServer.getServer().logIPs()), exception);
|
|
+ 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();
|
|
+
|
|
+ world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); // required to eventually process ticket updates
|
|
+ }
|
|
+
|
|
+ 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..98ddb674b63a4777a98152ea960debf48aa2bc35
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java
|
|
@@ -0,0 +1,768 @@
|
|
+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 java.util.concurrent.ConcurrentHashMap<io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkCoordinate, AtomicLong> referenceCounters = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - use area based lock to reduce contention
|
|
+
|
|
+ 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
|
|
+ );
|
|
+ }
|
|
+
|
|
+ // Folia start - use area based lock to reduce contention
|
|
+ private void processTicketUpdates(final long coord) {
|
|
+ this.world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(CoordinateUtils.getChunkX(coord), CoordinateUtils.getChunkZ(coord));
|
|
+ }
|
|
+ // Folia end - use area based lock to reduce contention
|
|
+
|
|
+ private void decrementReference(final AtomicLong reference, final long coord) {
|
|
+ final long val = reference.decrementAndGet();
|
|
+ if (val == 0L) {
|
|
+ final int chunkX = CoordinateUtils.getChunkX(coord); // Folia - use area based lock to reduce contention
|
|
+ final int chunkZ = CoordinateUtils.getChunkZ(coord); // Folia - use area based lock to reduce contention
|
|
+ final io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkCoordinate key = new io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkCoordinate(coord); // Folia - use area based lock to reduce contention
|
|
+ final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node ticketLock = this.world.chunkTaskScheduler.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ); // Folia - use area based lock to reduce contention
|
|
+ try {
|
|
+ if (this.referenceCounters.remove(key, reference)) { // Folia - use area based lock to reduce contention
|
|
+ WorldRegionTaskData.this.removeTicket(coord);
|
|
+ } // else: race condition, something replaced our reference - not our issue anymore
|
|
+ } finally {
|
|
+ this.world.chunkTaskScheduler.chunkHolderManager.ticketLockArea.unlock(ticketLock); // Folia - use area based lock to reduce contention
|
|
+ }
|
|
+ } else if (val < 0L) {
|
|
+ throw new IllegalStateException("Reference count < 0: " + val);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private AtomicLong incrementReference(final long coord) {
|
|
+ final io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkCoordinate key = new io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkCoordinate(coord); // Folia - use area based lock to reduce contention
|
|
+ final AtomicLong ret = this.referenceCounters.get(key); // Folia - use area based lock to reduce contention
|
|
+ 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 int chunkX = CoordinateUtils.getChunkX(coord); // Folia - use area based lock to reduce contention
|
|
+ final int chunkZ = CoordinateUtils.getChunkZ(coord); // Folia - use area based lock to reduce contention
|
|
+ final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node ticketLock = this.world.chunkTaskScheduler.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ); // Folia - use area based lock to reduce contention
|
|
+ final AtomicLong ret2;
|
|
+ final boolean processTicketUpdates;
|
|
+ try {
|
|
+ final AtomicLong replace = new AtomicLong(1L);
|
|
+ final AtomicLong valueInMap = this.referenceCounters.putIfAbsent(key, replace); // Folia - use area based lock to reduce contention
|
|
+ if (valueInMap == null) {
|
|
+ // replaced, we should usually be here
|
|
+ this.addTicket(coord);
|
|
+ ret2 = replace;
|
|
+ processTicketUpdates = true;
|
|
+ } else {
|
|
+ processTicketUpdates = false;
|
|
+ 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(key, replace); // Folia - use area based lock to reduce contention
|
|
+ ret2 = replace;
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = valueInMap.compareAndExchange(curr, curr + 1L))) {
|
|
+ // acquired
|
|
+ ret2 = valueInMap;
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ ++failures;
|
|
+ }
|
|
+ }
|
|
+ } finally {
|
|
+ this.world.chunkTaskScheduler.chunkHolderManager.ticketLockArea.unlock(ticketLock); // Folia - use area based lock to reduce contention
|
|
+ }
|
|
+
|
|
+ if (processTicketUpdates) {
|
|
+ this.processTicketUpdates(coord);
|
|
+ }
|
|
+
|
|
+ return ret2;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ 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() {
|
|
+ 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
|
|
+ this.worldRegionTaskData.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;
|
|
+ mergeInto(target, this.queues);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private static void mergeInto(final PrioritisedQueue target, final ArrayDeque<ChunkBasedPriorityTask>[] thisQueues) {
|
|
+ synchronized (target) {
|
|
+ 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
|
|
+ synchronise = true;
|
|
+ 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..7ca275826609bcf96f103a8c50beaa47c3b4068b
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java
|
|
@@ -0,0 +1,785 @@
|
|
+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 io.papermc.paper.util.player.NearbyPlayers;
|
|
+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.common.ClientboundDisconnectPacket;
|
|
+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.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.Explosion;
|
|
+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.HashMap;
|
|
+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 = into.redstoneTime - from.redstoneTime;
|
|
+ // entities
|
|
+ for (final ServerPlayer player : from.localPlayers) {
|
|
+ into.localPlayers.add(player);
|
|
+ into.nearbyPlayers.addPlayer(player);
|
|
+ }
|
|
+ for (final Entity entity : from.allEntities) {
|
|
+ into.allEntities.add(entity);
|
|
+ entity.updateTicks(fromTickOffset, fromRedstoneTimeOffset);
|
|
+ }
|
|
+ for (final Entity entity : from.loadedEntities) {
|
|
+ into.loadedEntities.add(entity);
|
|
+ }
|
|
+ for (final Entity entity : from.toProcessTrackingUnloading) {
|
|
+ into.toProcessTrackingUnloading.add(entity);
|
|
+ }
|
|
+ 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());
|
|
+ }
|
|
+ for (final Iterator<LevelChunk> iterator = from.tickingChunks.unsafeIterator(); iterator.hasNext();) {
|
|
+ into.tickingChunks.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.
|
|
+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift));
|
|
+ into.localPlayers.add(player);
|
|
+ into.nearbyPlayers.addPlayer(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: loadedEntities is a subset of allEntities
|
|
+ if (from.loadedEntities.contains(entity)) {
|
|
+ into.loadedEntities.add(entity);
|
|
+ }
|
|
+ // Note: toProcessTrackingUnloading is not a subset of allEntities, but for the cases where it is not
|
|
+ // do not matter as the tracker removal is handled
|
|
+ if (from.toProcessTrackingUnloading.contains(entity)) {
|
|
+ into.toProcessTrackingUnloading.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);
|
|
+ }
|
|
+ for (final Iterator<LevelChunk> iterator = from.tickingChunks.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))
|
|
+ .tickingChunks.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 NearbyPlayers nearbyPlayers;
|
|
+ private final ReferenceList<Entity> allEntities = new ReferenceList<>();
|
|
+ private final ReferenceList<Entity> loadedEntities = new ReferenceList<>();
|
|
+ private final ReferenceList<Entity> toProcessTrackingUnloading = 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<>();
|
|
+ private final IteratorSafeOrderedReferenceSet<LevelChunk> tickingChunks = new IteratorSafeOrderedReferenceSet<>();
|
|
+ private final IteratorSafeOrderedReferenceSet<LevelChunk> chunks = new IteratorSafeOrderedReferenceSet<>();
|
|
+
|
|
+ // Paper/CB api hook misc
|
|
+ // don't bother to merge/split these, no point
|
|
+ // From ServerLevel
|
|
+ public boolean hasPhysicsEvent = true; // Paper
|
|
+ public boolean hasEntityMoveEvent = false; // Paper
|
|
+ // Paper start - Optimize Hoppers
|
|
+ public boolean skipPullModeEventFire = false;
|
|
+ public boolean skipPushModeEventFire = false;
|
|
+ public boolean skipHopperEvents = false;
|
|
+ // Paper end - Optimize Hoppers
|
|
+ public long lastMidTickExecuteFailure;
|
|
+ public long lastMidTickExecute;
|
|
+ // From Level
|
|
+ public boolean populating;
|
|
+ public final NeighborUpdater neighborUpdater;
|
|
+ public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710
|
|
+ public boolean captureBlockStates = false;
|
|
+ public boolean captureTreeGeneration = false;
|
|
+ public final Map<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;
|
|
+ public final Map<Explosion.CacheKey, Float> explosionDensityCache = new HashMap<>(64, 0.25f);
|
|
+
|
|
+ // 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);
|
|
+ this.nearbyPlayers = new NearbyPlayers(world);
|
|
+
|
|
+ // 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;
|
|
+ }
|
|
+
|
|
+ private long lagCompensationTick;
|
|
+
|
|
+ public long getLagCompensationTick() {
|
|
+ return this.lagCompensationTick;
|
|
+ }
|
|
+
|
|
+ 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
|
|
+ // always subtract from server init so that the tick starts at zero, allowing us to cast to int without much worry
|
|
+ this.lagCompensationTick = (System.nanoTime() - MinecraftServer.SERVER_INIT) / TickRegionScheduler.TIME_BETWEEN_TICKS;
|
|
+ }
|
|
+
|
|
+ public NearbyPlayers getNearbyPlayers() {
|
|
+ return this.nearbyPlayers;
|
|
+ }
|
|
+
|
|
+ private static void cleanUpConnection(final Connection 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.serverLevel().chunkSource.removeTicketAtLevel(
|
|
+ ServerGamePacketListenerImpl.DISCONNECT_TICKET, player.connection.disconnectPos,
|
|
+ ChunkHolderManager.MAX_TICKET_LEVEL,
|
|
+ player.connection.disconnectTicketId
|
|
+ );
|
|
+ }
|
|
+
|
|
+ // 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();
|
|
+ // global tick thread will not remove connections not owned by it, so we need to
|
|
+ RegionizedServer.getInstance().removeConnection(conn);
|
|
+ this.connections.remove(conn);
|
|
+ cleanUpConnection(conn);
|
|
+ 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 {}", conn.getLoggableAddress(MinecraftServer.getServer().logIPs()), exception);
|
|
+ MutableComponent ichatmutablecomponent = Component.literal("Internal server error");
|
|
+
|
|
+ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> {
|
|
+ conn.disconnect(ichatmutablecomponent);
|
|
+ }));
|
|
+ conn.setReadOnly();
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // entities hooks
|
|
+ public int getEntityCount() {
|
|
+ return this.allEntities.size();
|
|
+ }
|
|
+
|
|
+ public int getPlayerCount() {
|
|
+ return this.localPlayers.size();
|
|
+ }
|
|
+
|
|
+ public Iterable<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 addLoadedEntity(final Entity entity) {
|
|
+ if (this.loadedEntities.add(entity)) {
|
|
+ this.toProcessTrackingUnloading.remove(entity);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public boolean hasLoadedEntity(final Entity entity) {
|
|
+ return this.loadedEntities.contains(entity);
|
|
+ }
|
|
+
|
|
+ public void removeLoadedEntity(final Entity entity) {
|
|
+ if (this.loadedEntities.remove(entity)) {
|
|
+ this.toProcessTrackingUnloading.add(entity);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public Iterable<Entity> getLoadedEntities() {
|
|
+ return this.loadedEntities;
|
|
+ }
|
|
+
|
|
+ public Entity[] takeTrackingUnloads() {
|
|
+ final Entity[] ret = Arrays.copyOf(this.toProcessTrackingUnloading.getRawData(), this.toProcessTrackingUnloading.size(), Entity[].class);
|
|
+
|
|
+ this.toProcessTrackingUnloading.clear();
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ public void addEntityTickingEntity(final Entity entity) {
|
|
+ if (!TickThread.isTickThreadFor(entity)) {
|
|
+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
|
|
+ }
|
|
+ this.entityTickList.add(entity);
|
|
+ TickRegions.RegionStats.updateCurrentRegion();
|
|
+ }
|
|
+
|
|
+ public boolean hasEntityTickingEntity(final Entity entity) {
|
|
+ return this.entityTickList.contains(entity);
|
|
+ }
|
|
+
|
|
+ public void removeEntityTickingEntity(final Entity entity) {
|
|
+ if (!TickThread.isTickThreadFor(entity)) {
|
|
+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
|
|
+ }
|
|
+ this.entityTickList.remove(entity);
|
|
+ TickRegions.RegionStats.updateCurrentRegion();
|
|
+ }
|
|
+
|
|
+ public void forEachTickingEntity(final Consumer<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);
|
|
+ this.nearbyPlayers.addPlayer(player);
|
|
+ }
|
|
+ TickRegions.RegionStats.updateCurrentRegion();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public boolean hasEntity(final Entity entity) {
|
|
+ return this.allEntities.contains(entity);
|
|
+ }
|
|
+
|
|
+ public void removeEntity(final Entity entity) {
|
|
+ if (!TickThread.isTickThreadFor(entity)) {
|
|
+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
|
|
+ }
|
|
+ if (this.allEntities.remove(entity)) {
|
|
+ if (entity instanceof ServerPlayer player) {
|
|
+ this.localPlayers.remove(player);
|
|
+ this.nearbyPlayers.removePlayer(player);
|
|
+ }
|
|
+ TickRegions.RegionStats.updateCurrentRegion();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void addNavigatingMob(final Mob mob) {
|
|
+ if (!TickThread.isTickThreadFor(mob)) {
|
|
+ throw new IllegalArgumentException("Entity " + mob + " is not under this region's control");
|
|
+ }
|
|
+ this.navigatingMobs.add(mob);
|
|
+ }
|
|
+
|
|
+ public void removeNavigatingMob(final Mob mob) {
|
|
+ if (!TickThread.isTickThreadFor(mob)) {
|
|
+ throw new IllegalArgumentException("Entity " + mob + " is not under this region's control");
|
|
+ }
|
|
+ this.navigatingMobs.remove(mob);
|
|
+ }
|
|
+
|
|
+ public Iterator<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 addEntityTickingChunk(final LevelChunk levelChunk) {
|
|
+ this.entityTickingChunks.add(levelChunk);
|
|
+ TickRegions.RegionStats.updateCurrentRegion();
|
|
+ }
|
|
+
|
|
+ public void removeEntityTickingChunk(final LevelChunk levelChunk) {
|
|
+ this.entityTickingChunks.remove(levelChunk);
|
|
+ TickRegions.RegionStats.updateCurrentRegion();
|
|
+ }
|
|
+
|
|
+ public IteratorSafeOrderedReferenceSet<LevelChunk> getEntityTickingChunks() {
|
|
+ return this.entityTickingChunks;
|
|
+ }
|
|
+
|
|
+ public void addTickingChunk(final LevelChunk levelChunk) {
|
|
+ this.tickingChunks.add(levelChunk);
|
|
+ TickRegions.RegionStats.updateCurrentRegion();
|
|
+ }
|
|
+
|
|
+ public void removeTickingChunk(final LevelChunk levelChunk) {
|
|
+ this.tickingChunks.remove(levelChunk);
|
|
+ TickRegions.RegionStats.updateCurrentRegion();
|
|
+ }
|
|
+
|
|
+ public IteratorSafeOrderedReferenceSet<LevelChunk> getTickingChunks() {
|
|
+ return this.tickingChunks;
|
|
+ }
|
|
+
|
|
+ public void addChunk(final LevelChunk levelChunk) {
|
|
+ this.chunks.add(levelChunk);
|
|
+ TickRegions.RegionStats.updateCurrentRegion();
|
|
+ }
|
|
+
|
|
+ public void removeChunk(final LevelChunk levelChunk) {
|
|
+ this.chunks.remove(levelChunk);
|
|
+ TickRegions.RegionStats.updateCurrentRegion();
|
|
+ }
|
|
+
|
|
+ public IteratorSafeOrderedReferenceSet<LevelChunk> getChunks() {
|
|
+ return this.chunks;
|
|
+ }
|
|
+
|
|
+ public int getEntityTickingChunkCount() {
|
|
+ return this.entityTickingChunks.size();
|
|
+ }
|
|
+
|
|
+ public int getChunkCount() {
|
|
+ return this.chunks.size();
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/Schedule.java b/src/main/java/io/papermc/paper/threadedregions/Schedule.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..112d24a93bddf3d81c9176c05340c94ecd1a40a3
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/Schedule.java
|
|
@@ -0,0 +1,91 @@
|
|
+package io.papermc.paper.threadedregions;
|
|
+
|
|
+/**
|
|
+ * A Schedule is an object that can be used to maintain a periodic schedule for an event of interest.
|
|
+ */
|
|
+public final class Schedule {
|
|
+
|
|
+ private long lastPeriod;
|
|
+
|
|
+ /**
|
|
+ * Initialises a schedule with the provided period.
|
|
+ * @param firstPeriod The last time an event of interest occurred.
|
|
+ * @see #setLastPeriod(long)
|
|
+ */
|
|
+ public Schedule(final long firstPeriod) {
|
|
+ this.lastPeriod = firstPeriod;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Updates the last period to the specified value. This call sets the last "time" the event
|
|
+ * of interest took place at. Thus, the value returned by {@link #getDeadline(long)} is
|
|
+ * the provided time plus the period length provided to {@code getDeadline}.
|
|
+ * @param value The value to set the last period to.
|
|
+ */
|
|
+ public void setLastPeriod(final long value) {
|
|
+ this.lastPeriod = value;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the last time the event of interest should have taken place.
|
|
+ */
|
|
+ public long getLastPeriod() {
|
|
+ return this.lastPeriod;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the number of times the event of interest should have taken place between the last
|
|
+ * period and the provided time given the period between each event.
|
|
+ * @param periodLength The length of the period between events in ns.
|
|
+ * @param time The provided time.
|
|
+ */
|
|
+ public int getPeriodsAhead(final long periodLength, final long time) {
|
|
+ final long difference = time - this.lastPeriod;
|
|
+ final int ret = (int)(Math.abs(difference) / periodLength);
|
|
+ return difference >= 0 ? ret : -ret;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the next starting deadline for the event of interest to take place,
|
|
+ * given the provided period length.
|
|
+ * @param periodLength The provided period length.
|
|
+ */
|
|
+ public long getDeadline(final long periodLength) {
|
|
+ return this.lastPeriod + periodLength;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Adjusts the last period so that the next starting deadline returned is the next period specified,
|
|
+ * given the provided period length.
|
|
+ * @param nextPeriod The specified next starting deadline.
|
|
+ * @param periodLength The specified period length.
|
|
+ */
|
|
+ public void setNextPeriod(final long nextPeriod, final long periodLength) {
|
|
+ this.lastPeriod = nextPeriod - periodLength;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Increases the last period by the specified number of periods and period length.
|
|
+ * The specified number of periods may be < 0, in which case the last period
|
|
+ * will decrease.
|
|
+ * @param periods The specified number of periods.
|
|
+ * @param periodLength The specified period length.
|
|
+ */
|
|
+ public void advanceBy(final int periods, final long periodLength) {
|
|
+ this.lastPeriod += (long)periods * periodLength;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Sets the last period so that it is the specified number of periods ahead
|
|
+ * given the specified time and period length.
|
|
+ * @param periodsToBeAhead Specified number of periods to be ahead by.
|
|
+ * @param periodLength The specified period length.
|
|
+ * @param time The specified time.
|
|
+ */
|
|
+ public void setPeriodsAhead(final int periodsToBeAhead, final long periodLength, final long time) {
|
|
+ final int periodsAhead = this.getPeriodsAhead(periodLength, time);
|
|
+ final int periodsToAdd = periodsToBeAhead - periodsAhead;
|
|
+
|
|
+ this.lastPeriod -= (long)periodsToAdd * periodLength;
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java b/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..7b31c4ea6d01f936271bdadc3626201dcf32a683
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java
|
|
@@ -0,0 +1,70 @@
|
|
+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) {
|
|
+ if (onComplete != 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) -> {
|
|
+ if (onComplete != null) {
|
|
+ onComplete.accept(null);
|
|
+ }
|
|
+ },
|
|
+ 1L
|
|
+ );
|
|
+ if (!scheduled) {
|
|
+ if (onComplete != null) {
|
|
+ onComplete.accept(null);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ );
|
|
+
|
|
+ final boolean scheduled = to.getBukkitEntity().taskScheduler.schedule(
|
|
+ (final Entity target) -> {
|
|
+ positionCompletable.complete(target.getBukkitEntity().getLocation());
|
|
+ },
|
|
+ (final Entity retired) -> {
|
|
+ if (onComplete != null) {
|
|
+ onComplete.accept(null);
|
|
+ }
|
|
+ },
|
|
+ 1L
|
|
+ );
|
|
+ if (!scheduled) {
|
|
+ if (onComplete != null) {
|
|
+ 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..531aa50f2c84e13358e8918bb0c15ea3cd036cb5
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java
|
|
@@ -0,0 +1,1405 @@
|
|
+package io.papermc.paper.threadedregions;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import com.destroystokyo.paper.util.SneakyThrow;
|
|
+import com.mojang.logging.LogUtils;
|
|
+import io.papermc.paper.util.CoordinateUtils;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.longs.LongArrayList;
|
|
+import it.unimi.dsi.fastutil.longs.LongComparator;
|
|
+import it.unimi.dsi.fastutil.longs.LongIterator;
|
|
+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.Set;
|
|
+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 int computeForRegions(final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ,
|
|
+ final Consumer<Set<ThreadedRegion<R, S>>> consumer) {
|
|
+ final int shift = this.sectionChunkShift;
|
|
+ final int fromSectionX = fromChunkX >> shift;
|
|
+ final int fromSectionZ = fromChunkZ >> shift;
|
|
+ final int toSectionX = toChunkX >> shift;
|
|
+ final int toSectionZ = toChunkZ >> shift;
|
|
+ this.acquireWriteLock();
|
|
+ try {
|
|
+ final ReferenceOpenHashSet<ThreadedRegion<R, S>> set = new ReferenceOpenHashSet<>();
|
|
+
|
|
+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
|
|
+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
|
|
+ final ThreadedRegionSection<R, S> section = this.sections.get(CoordinateUtils.getChunkKey(currX, currZ));
|
|
+ if (section != null) {
|
|
+ set.add(section.getRegionPlain());
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ consumer.accept(set);
|
|
+
|
|
+ return set.size();
|
|
+ } finally {
|
|
+ this.releaseWriteLock();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ 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);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (newSections.isEmpty()) {
|
|
+ // if we didn't add any sections, then we don't need to merge any regions or create a region
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ 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;
|
|
+ if (firstUnlockedRegion.state == ThreadedRegion.STATE_READY && (!firstUnlockedRegion.mergeIntoLater.isEmpty() || !firstUnlockedRegion.expectingMergeFrom.isEmpty())) {
|
|
+ throw new IllegalStateException("Illegal state for unlocked region " + firstUnlockedRegion);
|
|
+ }
|
|
+ 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
|
|
+
|
|
+ for (final ThreadedRegion<R, S> region : nearbyRegions) {
|
|
+ if (region == regionOfInterest) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (!region.killAndMergeInto(regionOfInterest)) {
|
|
+ // note: the region may already be a merge target
|
|
+ regionOfInterest.mergeIntoLater(region);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (firstUnlockedRegion != null && firstUnlockedRegion.state == ThreadedRegion.STATE_READY) {
|
|
+ // we need to retire this region if the merges added other pending merges
|
|
+ if (!firstUnlockedRegion.mergeIntoLater.isEmpty() || !firstUnlockedRegion.expectingMergeFrom.isEmpty()) {
|
|
+ firstUnlockedRegion.state = ThreadedRegion.STATE_TRANSIENT;
|
|
+ this.callbacks.onRegionInactive(firstUnlockedRegion);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // need to set alive if we created it and there are no pending merges
|
|
+ regionOfInterestAlive = firstUnlockedRegion == null && 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);
|
|
+ }
|
|
+
|
|
+ if (regionOfInterest.state == ThreadedRegion.STATE_READY) {
|
|
+ if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) {
|
|
+ throw new IllegalStateException("Should not happen on region " + this);
|
|
+ }
|
|
+ }
|
|
+ } 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.killAndMergeInto(region)) {
|
|
+ throw new IllegalStateException("Merge from region " + mergeFrom + " should be killable! Trying to merge into " + 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;
|
|
+ }
|
|
+
|
|
+ final List<ThreadedRegion<R, S>> newRegionObjects = new ArrayList<>(newRegions.size());
|
|
+ for (int i = 0, len = newRegions.size(); i < len; ++i) {
|
|
+ newRegionObjects.add(new ThreadedRegion<>(this));
|
|
+ }
|
|
+
|
|
+ this.callbacks.preSplit(region, newRegionObjects);
|
|
+
|
|
+ // 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<>(newRegionObjects);
|
|
+
|
|
+ for (int i = 0, len = newRegions.size(); i < len; i++) {
|
|
+ final List<ThreadedRegionSection<R, S>> sections = newRegions.get(i);
|
|
+ final ThreadedRegion<R, S> newRegion = newRegionObjects.get(i);
|
|
+
|
|
+ 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();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * returns an iterator directly over the sections map. This is only to be used by a thread which is _ticking_
|
|
+ * 'this' region.
|
|
+ */
|
|
+ public LongIterator getOwnedSectionsUnsynchronised() {
|
|
+ return this.sectionByKey.keySet().iterator();
|
|
+ }
|
|
+
|
|
+ public LongArrayList getOwnedChunks() {
|
|
+ final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread();
|
|
+ if (lock) {
|
|
+ this.regioniser.regionLock.readLock();
|
|
+ }
|
|
+ try {
|
|
+ final LongArrayList ret = new LongArrayList();
|
|
+ for (final ThreadedRegionSection<R, S> section : this.sectionByKey.values()) {
|
|
+ ret.addAll(section.getChunks());
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ } finally {
|
|
+ if (lock) {
|
|
+ this.regioniser.regionLock.tryUnlockRead();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public Long getCenterSection() {
|
|
+ final LongArrayList sections = this.getOwnedSections();
|
|
+
|
|
+ final LongComparator comparator = (final long k1, final long k2) -> {
|
|
+ final int x1 = CoordinateUtils.getChunkX(k1);
|
|
+ final int x2 = CoordinateUtils.getChunkX(k2);
|
|
+
|
|
+ final int z1 = CoordinateUtils.getChunkZ(x1);
|
|
+ final int z2 = CoordinateUtils.getChunkZ(x2);
|
|
+
|
|
+ final int zCompare = Integer.compare(z1, z2);
|
|
+ if (zCompare != 0) {
|
|
+ return zCompare;
|
|
+ }
|
|
+
|
|
+ return Integer.compare(x1, x2);
|
|
+ };
|
|
+
|
|
+ // note: regions don't always have a chunk section at this point, because the region may have been killed
|
|
+ if (sections.isEmpty()) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ sections.sort(comparator);
|
|
+
|
|
+ return Long.valueOf(sections.getLong(sections.size() >> 1));
|
|
+ }
|
|
+
|
|
+ public ChunkPos getCenterChunk() {
|
|
+ final LongArrayList chunks = this.getOwnedChunks();
|
|
+
|
|
+ final LongComparator comparator = (final long k1, final long k2) -> {
|
|
+ final int x1 = CoordinateUtils.getChunkX(k1);
|
|
+ final int x2 = CoordinateUtils.getChunkX(k2);
|
|
+
|
|
+ final int z1 = CoordinateUtils.getChunkZ(k1);
|
|
+ final int z2 = CoordinateUtils.getChunkZ(k2);
|
|
+
|
|
+ final int zCompare = Integer.compare(z1, z2);
|
|
+ if (zCompare != 0) {
|
|
+ return zCompare;
|
|
+ }
|
|
+
|
|
+ return Integer.compare(x1, x2);
|
|
+ };
|
|
+ chunks.sort(comparator);
|
|
+
|
|
+ // note: regions don't always have a chunk at this point, because the region may have been killed
|
|
+ if (chunks.isEmpty()) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ final long middle = chunks.getLong(chunks.size() >> 1);
|
|
+
|
|
+ return new ChunkPos(CoordinateUtils.getChunkX(middle), CoordinateUtils.getChunkZ(middle));
|
|
+ }
|
|
+
|
|
+ private void onCreate() {
|
|
+ this.regioniser.onRegionCreate(this);
|
|
+ this.regioniser.callbacks.onRegionCreate(this);
|
|
+ }
|
|
+
|
|
+ private void onRemove(final boolean wasActive) {
|
|
+ if (wasActive) {
|
|
+ this.regioniser.callbacks.onRegionInactive(this);
|
|
+ }
|
|
+ this.regioniser.callbacks.onRegionDestroy(this);
|
|
+ this.regioniser.onRegionDestroy(this);
|
|
+ }
|
|
+
|
|
+ private final boolean hasNoAliveSections() {
|
|
+ return this.deadSections.size() == this.sectionByKey.size();
|
|
+ }
|
|
+
|
|
+ private final double getDeadSectionPercent() {
|
|
+ return (double)this.deadSections.size() / (double)this.sectionByKey.size();
|
|
+ }
|
|
+
|
|
+ private void split(final Long2ReferenceOpenHashMap<ThreadedRegion<R, S>> into, final ReferenceOpenHashSet<ThreadedRegion<R, S>> regions) {
|
|
+ if (this.data != null) {
|
|
+ this.data.split(this.regioniser, into, regions);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ boolean killAndMergeInto(final ThreadedRegion<R, S> mergeTarget) {
|
|
+ if (this.state == STATE_TICKING) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ this.regioniser.callbacks.preMerge(this, mergeTarget);
|
|
+
|
|
+ this.tryKill();
|
|
+
|
|
+ this.mergeInto(mergeTarget);
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ 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;
|
|
+ }
|
|
+
|
|
+ public LongArrayList getChunks() {
|
|
+ final LongArrayList ret = new LongArrayList();
|
|
+
|
|
+ if (this.chunkCount == 0) {
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ final int shift = this.regionChunkShift;
|
|
+ final int mask = this.regionChunkMask;
|
|
+ final int offsetX = this.sectionX << shift;
|
|
+ final int offsetZ = this.sectionZ << shift;
|
|
+
|
|
+ final long[] bitset = this.chunksBitset;
|
|
+ for (int arrIdx = 0, arrLen = bitset.length; arrIdx < arrLen; ++arrIdx) {
|
|
+ long value = bitset[arrIdx];
|
|
+
|
|
+ for (int i = 0, bits = Long.bitCount(value); i < bits; ++i) {
|
|
+ final int valueIdx = Long.numberOfTrailingZeros(value);
|
|
+ value ^= io.papermc.paper.util.IntegerUtil.getTrailingBit(value);
|
|
+
|
|
+ final int idx = valueIdx | (arrIdx << 6);
|
|
+
|
|
+ final int localX = idx & mask;
|
|
+ final int localZ = (idx >>> shift) & mask;
|
|
+
|
|
+ ret.add(CoordinateUtils.getChunkKey(localX | offsetX, localZ | offsetZ));
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ private boolean isEmpty() {
|
|
+ return this.chunkCount == 0;
|
|
+ }
|
|
+
|
|
+ private boolean hasOnlyOneChunk() {
|
|
+ return this.chunkCount == 1;
|
|
+ }
|
|
+
|
|
+ public boolean hasNonEmptyNeighbours() {
|
|
+ return this.nonEmptyNeighbours != 0;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the section data associated with this region section. May be {@code null}.
|
|
+ */
|
|
+ public S getData() {
|
|
+ return this.data;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the region that owns this section. Unsynchronised access may produce outdateed or transient results.
|
|
+ */
|
|
+ public ThreadedRegion<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);
|
|
+
|
|
+ /**
|
|
+ * Callback for when a region (from) is about to be merged into a target region (into). Note that
|
|
+ * {@code from} is still alive and is a distinct 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 from The region that will be merged into the target.
|
|
+ * @param into The target of the merge.
|
|
+ */
|
|
+ public void preMerge(final ThreadedRegion<R, S> from, final ThreadedRegion<R, S> into);
|
|
+
|
|
+ /**
|
|
+ * Callback for when a region (from) is about to be split into a list of target region (into). Note that
|
|
+ * {@code from} is still alive, while the list of target regions are not initialised.
|
|
+ * <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 from The region that will be merged into the target.
|
|
+ * @param into The list of regions to split into.
|
|
+ */
|
|
+ public void preSplit(final ThreadedRegion<R, S> from, final List<ThreadedRegion<R, S>> into);
|
|
+ }
|
|
+}
|
|
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..150610d7bf25416dbbde7f003c47da562acc68ba
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java
|
|
@@ -0,0 +1,565 @@
|
|
+package io.papermc.paper.threadedregions;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
|
|
+import ca.spottedleaf.concurrentutil.util.TimeUtil;
|
|
+import com.mojang.logging.LogUtils;
|
|
+import io.papermc.paper.util.TickThread;
|
|
+import net.minecraft.server.MinecraftServer;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.world.level.ChunkPos;
|
|
+import org.slf4j.Logger;
|
|
+import java.lang.management.ManagementFactory;
|
|
+import java.lang.management.ThreadMXBean;
|
|
+import java.util.concurrent.ThreadFactory;
|
|
+import java.util.concurrent.TimeUnit;
|
|
+import java.util.concurrent.atomic.AtomicBoolean;
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
+import java.util.function.BooleanSupplier;
|
|
+
|
|
+public final class TickRegionScheduler {
|
|
+
|
|
+ private static final Logger LOGGER = LogUtils.getLogger();
|
|
+ private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean();
|
|
+ private static final boolean MEASURE_CPU_TIME;
|
|
+ static {
|
|
+ MEASURE_CPU_TIME = THREAD_MX_BEAN.isThreadCpuTimeSupported();
|
|
+ if (MEASURE_CPU_TIME) {
|
|
+ THREAD_MX_BEAN.setThreadCpuTimeEnabled(true);
|
|
+ } else {
|
|
+ LOGGER.warn("TickRegionScheduler CPU time measurement is not available");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static final int TICK_RATE = 20;
|
|
+ public static final long TIME_BETWEEN_TICKS = 1_000_000_000L / TICK_RATE; // ns
|
|
+
|
|
+ private final SchedulerThreadPool scheduler;
|
|
+
|
|
+ public TickRegionScheduler(final int threads) {
|
|
+ this.scheduler = new SchedulerThreadPool(threads, new ThreadFactory() {
|
|
+ private final AtomicInteger idGenerator = new AtomicInteger();
|
|
+
|
|
+ @Override
|
|
+ public Thread newThread(final Runnable run) {
|
|
+ final Thread ret = new TickThreadRunner(run, "Region Scheduler Thread #" + this.idGenerator.getAndIncrement());
|
|
+ ret.setUncaughtExceptionHandler(TickRegionScheduler.this::uncaughtException);
|
|
+ return ret;
|
|
+ }
|
|
+ });
|
|
+ }
|
|
+
|
|
+ public int getTotalThreadCount() {
|
|
+ return this.scheduler.getThreads().length;
|
|
+ }
|
|
+
|
|
+ private static void setTickingRegion(final ThreadedRegionizer.ThreadedRegion<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);
|
|
+ }
|
|
+
|
|
+ void dumpAliveThreadTraces(final String reason) {
|
|
+ this.scheduler.dumpAliveThreadTraces(reason);
|
|
+ }
|
|
+
|
|
+ public void setHasTasks(final RegionScheduleHandle region) {
|
|
+ this.scheduler.notifyTasks(region);
|
|
+ }
|
|
+
|
|
+ public void init() {
|
|
+ this.scheduler.start();
|
|
+ }
|
|
+
|
|
+ private void uncaughtException(final Thread thread, final Throwable thr) {
|
|
+ LOGGER.error("Uncaught exception in tick thread \"" + thread.getName() + "\"", thr);
|
|
+
|
|
+ // prevent further ticks from occurring
|
|
+ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD
|
|
+ this.scheduler.halt(false, 0L);
|
|
+
|
|
+ MinecraftServer.getServer().stopServer();
|
|
+ }
|
|
+
|
|
+ private void regionFailed(final RegionScheduleHandle handle, final boolean executingTasks, final Throwable thr) {
|
|
+ // when a region fails, we need to shut down the server gracefully
|
|
+
|
|
+ // prevent further ticks from occurring
|
|
+ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD
|
|
+ this.scheduler.halt(false, 0L);
|
|
+
|
|
+ final ChunkPos center = handle.region == null ? null : handle.region.region.getCenterChunk();
|
|
+ final ServerLevel world = handle.region == null ? null : handle.region.world;
|
|
+
|
|
+ LOGGER.error("Region #" + (handle.region == null ? -1L : handle.region.id) + " centered at chunk " + center + " in world '" + (world == null ? "null" : world.getWorld().getName()) + "' failed to " + (executingTasks ? "execute tasks" : "tick") + ":", thr);
|
|
+
|
|
+ MinecraftServer.getServer().stopServer();
|
|
+ }
|
|
+
|
|
+ // By using our own thread object, we can use a field for the current region rather than a ThreadLocal.
|
|
+ // This is much faster than a thread local, since the thread local has to use a map lookup.
|
|
+ private static final class TickThreadRunner extends TickThread {
|
|
+
|
|
+ private ThreadedRegionizer.ThreadedRegion<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 {
|
|
+ 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);
|
|
+ TickRegionScheduler.setTickTask(null);
|
|
+ if (this.region != null) {
|
|
+ TickRegionScheduler.setTickingRegion(null);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ 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 {
|
|
+ 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);
|
|
+ TickRegionScheduler.setTickTask(null);
|
|
+ if (this.region != null) {
|
|
+ TickRegionScheduler.setTickingRegion(null);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // Only AFTER updating the tickStart
|
|
+ return this.markNotTicking() && !this.cancelled.get();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Only safe to call if this tick data matches the current ticking region.
|
|
+ */
|
|
+ protected 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
|
|
index d5d39e9c1f326e91010237b0db80d527ac52f4d6..902e82854c89779c7e23c63d1be5b04dad2a61e3 100644
|
|
--- a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java
|
|
@@ -1,9 +1,404 @@
|
|
package io.papermc.paper.threadedregions;
|
|
|
|
-// placeholder class for Folia
|
|
-public class TickRegions {
|
|
+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
|
|
+import ca.spottedleaf.concurrentutil.util.TimeUtil;
|
|
+import com.mojang.logging.LogUtils;
|
|
+import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager;
|
|
+import io.papermc.paper.configuration.GlobalConfiguration;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
|
|
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
|
|
+import net.minecraft.server.MinecraftServer;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import org.slf4j.Logger;
|
|
+import java.util.Iterator;
|
|
+import java.util.concurrent.TimeUnit;
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
+import java.util.concurrent.atomic.AtomicLong;
|
|
+import java.util.function.BooleanSupplier;
|
|
+
|
|
+public final class TickRegions implements ThreadedRegionizer.RegionCallbacks<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 = tickThreads / 4;
|
|
+ }
|
|
+ } else {
|
|
+ tickThreads = config.threads;
|
|
+ }
|
|
+
|
|
+ scheduler = new TickRegionScheduler(tickThreads);
|
|
+ LOGGER.info("Regionised ticking is enabled with " + tickThreads + " tick threads");
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public TickRegionData createNewData(final ThreadedRegionizer.ThreadedRegion<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) {
|
|
+ final TickRegionData data = region.getData();
|
|
+ // post-region merge/split regioninfo update
|
|
+ data.getRegionStats().updateFrom(data.getOrCreateRegionizedData(data.world.worldRegionData));
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void onRegionDestroy(final ThreadedRegionizer.ThreadedRegion<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();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void preMerge(final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> from,
|
|
+ final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> into) {
|
|
+
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void preSplit(final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> from,
|
|
+ final java.util.List<ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData>> into) {
|
|
+
|
|
+ }
|
|
+
|
|
+ public static final class TickRegionSectionData implements ThreadedRegionizer.ThreadedRegionSectionData {}
|
|
+
|
|
+ public static final class RegionStats {
|
|
+
|
|
+ private final AtomicInteger entityCount = new AtomicInteger();
|
|
+ private final AtomicInteger playerCount = new AtomicInteger();
|
|
+ private final AtomicInteger chunkCount = new AtomicInteger();
|
|
+
|
|
+ public int getEntityCount() {
|
|
+ return this.entityCount.get();
|
|
+ }
|
|
+
|
|
+ public int getPlayerCount() {
|
|
+ return this.playerCount.get();
|
|
+ }
|
|
+
|
|
+ public int getChunkCount() {
|
|
+ return this.chunkCount.get();
|
|
+ }
|
|
+
|
|
+ void updateFrom(final RegionizedWorldData data) {
|
|
+ this.entityCount.setRelease(data == null ? 0 : data.getEntityCount());
|
|
+ this.playerCount.setRelease(data == null ? 0 : data.getPlayerCount());
|
|
+ this.chunkCount.setRelease(data == null ? 0 : data.getChunkCount());
|
|
+ }
|
|
+
|
|
+ static void updateCurrentRegion() {
|
|
+ TickRegionScheduler.getCurrentRegion().getData().getRegionStats().updateFrom(TickRegionScheduler.getCurrentRegionizedWorldData());
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static final class TickRegionData implements ThreadedRegionizer.ThreadedRegionData<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();
|
|
+
|
|
+ // async-safe read-only region data
|
|
+ private final RegionStats regionStats;
|
|
+
|
|
+ private TickRegionData(final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region) {
|
|
+ this.region = region;
|
|
+ this.world = region.regioniser.world;
|
|
+ this.taskQueueData = new RegionizedTaskQueue.RegionTaskQueueData(this.world.taskQueueRegionData);
|
|
+ this.regionStats = new RegionStats();
|
|
+ }
|
|
+
|
|
+ public RegionStats getRegionStats() {
|
|
+ return this.regionStats;
|
|
+ }
|
|
+
|
|
+ public RegionizedTaskQueue.RegionTaskQueueData getTaskQueueData() {
|
|
+ return this.taskQueueData;
|
|
+ }
|
|
+
|
|
+ // the value returned can be invalidated at any time, except when the caller
|
|
+ // is ticking this region
|
|
+ public TickRegionScheduler.RegionScheduleHandle getRegionSchedulingHandle() {
|
|
+ return this.tickHandle;
|
|
+ }
|
|
+
|
|
+ public long getCurrentTick() {
|
|
+ return this.tickHandle.getCurrentTick();
|
|
+ }
|
|
+
|
|
+ public ChunkHolderManager.HolderManagerRegionData getHolderManagerRegionData() {
|
|
+ return this.holderManagerRegionData;
|
|
+ }
|
|
+
|
|
+ <T> T getRegionizedData(final RegionizedData<T> regionizedData) {
|
|
+ return (T)this.regionizedData.get(regionizedData);
|
|
+ }
|
|
+
|
|
+ <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;
|
|
+
|
|
+ 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
|
|
+ this.region.world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates();
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected boolean hasIntermediateTasks() {
|
|
+ return this.region.taskQueueData.hasTasks();
|
|
+ }
|
|
+ }
|
|
}
|
|
diff --git a/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..3bcb1dc98c61e025874cc9e008faa722581a530c
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java
|
|
@@ -0,0 +1,355 @@
|
|
+package io.papermc.paper.threadedregions.commands;
|
|
+
|
|
+import io.papermc.paper.threadedregions.RegionizedServer;
|
|
+import io.papermc.paper.threadedregions.RegionizedWorldData;
|
|
+import io.papermc.paper.threadedregions.ThreadedRegionizer;
|
|
+import io.papermc.paper.threadedregions.TickData;
|
|
+import io.papermc.paper.threadedregions.TickRegionScheduler;
|
|
+import io.papermc.paper.threadedregions.TickRegions;
|
|
+import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
|
|
+import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair;
|
|
+import net.kyori.adventure.text.Component;
|
|
+import net.kyori.adventure.text.TextComponent;
|
|
+import net.kyori.adventure.text.event.ClickEvent;
|
|
+import net.kyori.adventure.text.event.HoverEvent;
|
|
+import net.kyori.adventure.text.format.NamedTextColor;
|
|
+import net.kyori.adventure.text.format.TextColor;
|
|
+import net.kyori.adventure.text.format.TextDecoration;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.world.level.ChunkPos;
|
|
+import org.bukkit.Bukkit;
|
|
+import org.bukkit.World;
|
|
+import org.bukkit.command.Command;
|
|
+import org.bukkit.command.CommandSender;
|
|
+import org.bukkit.craftbukkit.CraftWorld;
|
|
+import org.bukkit.entity.Entity;
|
|
+import org.bukkit.entity.Player;
|
|
+import java.text.DecimalFormat;
|
|
+import java.util.ArrayList;
|
|
+import java.util.Arrays;
|
|
+import java.util.List;
|
|
+import java.util.Locale;
|
|
+
|
|
+public final class CommandServerHealth extends Command {
|
|
+
|
|
+ private static final ThreadLocal<DecimalFormat> TWO_DECIMAL_PLACES = ThreadLocal.withInitial(() -> {
|
|
+ return new DecimalFormat("#,##0.00");
|
|
+ });
|
|
+ private static final ThreadLocal<DecimalFormat> ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> {
|
|
+ return new DecimalFormat("#,##0.0");
|
|
+ });
|
|
+ private static final ThreadLocal<DecimalFormat> NO_DECIMAL_PLACES = ThreadLocal.withInitial(() -> {
|
|
+ return new DecimalFormat("#,##0");
|
|
+ });
|
|
+
|
|
+ private static final TextColor HEADER = TextColor.color(79, 164, 240);
|
|
+ private static final TextColor PRIMARY = TextColor.color(48, 145, 237);
|
|
+ private static final TextColor SECONDARY = TextColor.color(104, 177, 240);
|
|
+ private static final TextColor INFORMATION = TextColor.color(145, 198, 243);
|
|
+ private static final TextColor LIST = TextColor.color(33, 97, 188);
|
|
+
|
|
+ public CommandServerHealth() {
|
|
+ super("tps");
|
|
+ this.setUsage("/<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.get().format(util * 100.0), CommandUtil.getUtilisationColourRegion(util)))
|
|
+ .append(Component.text("% util at ", PRIMARY))
|
|
+ .append(Component.text(TWO_DECIMAL_PLACES.get().format(mspt), CommandUtil.getColourForMSPT(mspt)))
|
|
+ .append(Component.text(" MSPT at ", PRIMARY))
|
|
+ .append(Component.text(TWO_DECIMAL_PLACES.get().format(tps), CommandUtil.getColourForTPS(tps)))
|
|
+ .append(Component.text(" TPS" + (newline ? "\n" : ""), PRIMARY))
|
|
+ .build();
|
|
+ }
|
|
+
|
|
+ private static Component formatRegionStats(final TickRegions.RegionStats stats, final boolean newline) {
|
|
+ return Component.text()
|
|
+ .append(Component.text("Chunks: ", PRIMARY))
|
|
+ .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getChunkCount()), INFORMATION))
|
|
+ .append(Component.text(" Players: ", PRIMARY))
|
|
+ .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getPlayerCount()), INFORMATION))
|
|
+ .append(Component.text(" Entities: ", PRIMARY))
|
|
+ .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getEntityCount()) + (newline ? "\n" : ""), INFORMATION))
|
|
+ .build();
|
|
+ }
|
|
+
|
|
+ private static boolean executeRegion(final CommandSender sender, final String commandLabel, final String[] args) {
|
|
+ final ThreadedRegionizer.ThreadedRegion<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, true)
|
|
+ )
|
|
+ .append(
|
|
+ formatRegionStats(region.getData().getRegionStats(), false)
|
|
+ )
|
|
+
|
|
+ .build();
|
|
+
|
|
+ sender.sendMessage(line);
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ private static boolean executeServer(final CommandSender sender, final String commandLabel, final String[] args) {
|
|
+ final int lowestRegionsCount;
|
|
+ if (args.length < 2) {
|
|
+ lowestRegionsCount = 3;
|
|
+ } else {
|
|
+ try {
|
|
+ lowestRegionsCount = Integer.parseInt(args[1]);
|
|
+ } catch (final NumberFormatException ex) {
|
|
+ sender.sendMessage(Component.text("Highest utilisation count '" + args[1] + "' must be an integer", NamedTextColor.RED));
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final List<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();
|
|
+ if (chunkCenter == null) {
|
|
+ // region does not exist anymore
|
|
+ continue;
|
|
+ }
|
|
+ final int centerBlockX = ((chunkCenter.x << 4) | 7);
|
|
+ final int centerBlockZ = ((chunkCenter.z << 4) | 7);
|
|
+ final double util = report.utilisation();
|
|
+ final double tps = report.tpsData().segmentAll().average();
|
|
+ final double mspt = report.timePerTickData().segmentAll().average() / 1.0E6;
|
|
+
|
|
+ final int yLoc = 80;
|
|
+ final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]";
|
|
+ final Component line = Component.text()
|
|
+ .append(Component.text(" - ", LIST, TextDecoration.BOLD))
|
|
+ .append(Component.text("Region around block ", PRIMARY))
|
|
+ .append(Component.text(location, INFORMATION))
|
|
+ .append(Component.text(":\n", PRIMARY))
|
|
+
|
|
+ .append(Component.text(" ", PRIMARY))
|
|
+ .append(Component.text(ONE_DECIMAL_PLACES.get().format(util * 100.0), CommandUtil.getUtilisationColourRegion(util)))
|
|
+ .append(Component.text("% util at ", PRIMARY))
|
|
+ .append(Component.text(TWO_DECIMAL_PLACES.get().format(mspt), CommandUtil.getColourForMSPT(mspt)))
|
|
+ .append(Component.text(" MSPT at ", PRIMARY))
|
|
+ .append(Component.text(TWO_DECIMAL_PLACES.get().format(tps), CommandUtil.getColourForTPS(tps)))
|
|
+ .append(Component.text(" TPS\n", PRIMARY))
|
|
+
|
|
+ .append(Component.text(" ", PRIMARY))
|
|
+ .append(formatRegionStats(region.getData().getRegionStats(), (i + 1) != len))
|
|
+ .build()
|
|
+
|
|
+ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey().toString() + " run tp " + centerBlockX + ".5 " + yLoc + " " + centerBlockZ + ".5"))
|
|
+ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("Click to teleport to " + location, SECONDARY)));
|
|
+
|
|
+ lowestRegionsBuilder.append(line);
|
|
+ }
|
|
+
|
|
+ sender.sendMessage(
|
|
+ Component.text()
|
|
+ .append(Component.text("Server Health Report\n", HEADER, TextDecoration.BOLD))
|
|
+
|
|
+ .append(Component.text(" - ", LIST, TextDecoration.BOLD))
|
|
+ .append(Component.text("Online Players: ", PRIMARY))
|
|
+ .append(Component.text(Bukkit.getOnlinePlayers().size() + "\n", INFORMATION))
|
|
+
|
|
+ .append(Component.text(" - ", LIST, TextDecoration.BOLD))
|
|
+ .append(Component.text("Total regions: ", PRIMARY))
|
|
+ .append(Component.text(regions.size() + "\n", INFORMATION))
|
|
+
|
|
+ .append(Component.text(" - ", LIST, TextDecoration.BOLD))
|
|
+ .append(Component.text("Utilisation: ", PRIMARY))
|
|
+ .append(Component.text(ONE_DECIMAL_PLACES.get().format(totalUtil * 100.0), CommandUtil.getUtilisationColourRegion(totalUtil / (double)maxThreadCount)))
|
|
+ .append(Component.text("% / ", PRIMARY))
|
|
+ .append(Component.text(ONE_DECIMAL_PLACES.get().format(maxThreadCount * 100.0), INFORMATION))
|
|
+ .append(Component.text("%\n", PRIMARY))
|
|
+
|
|
+ .append(Component.text(" - ", LIST, TextDecoration.BOLD))
|
|
+ .append(Component.text("Lowest Region TPS: ", PRIMARY))
|
|
+ .append(Component.text(TWO_DECIMAL_PLACES.get().format(minTps) + "\n", CommandUtil.getColourForTPS(minTps)))
|
|
+
|
|
+
|
|
+ .append(Component.text(" - ", LIST, TextDecoration.BOLD))
|
|
+ .append(Component.text("Median Region TPS: ", PRIMARY))
|
|
+ .append(Component.text(TWO_DECIMAL_PLACES.get().format(medianTps) + "\n", CommandUtil.getColourForTPS(medianTps)))
|
|
+
|
|
+ .append(Component.text(" - ", LIST, TextDecoration.BOLD))
|
|
+ .append(Component.text("Highest Region TPS: ", PRIMARY))
|
|
+ .append(Component.text(TWO_DECIMAL_PLACES.get().format(maxTps) + "\n", CommandUtil.getColourForTPS(maxTps)))
|
|
+
|
|
+ .append(Component.text("Highest ", HEADER, TextDecoration.BOLD))
|
|
+ .append(Component.text(Integer.toString(lowestRegionsCount), INFORMATION, TextDecoration.BOLD))
|
|
+ .append(Component.text(" utilisation regions\n", HEADER, TextDecoration.BOLD))
|
|
+
|
|
+ .append(lowestRegionsBuilder.build())
|
|
+ .build()
|
|
+ );
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) {
|
|
+ final String type;
|
|
+ if (args.length < 1) {
|
|
+ type = "server";
|
|
+ } else {
|
|
+ type = args[0];
|
|
+ }
|
|
+
|
|
+ switch (type.toLowerCase(Locale.ROOT)) {
|
|
+ case "server": {
|
|
+ return executeServer(sender, commandLabel, args);
|
|
+ }
|
|
+ case "region": {
|
|
+ if (!(sender instanceof Entity)) {
|
|
+ sender.sendMessage(Component.text("Cannot see current region information as console", NamedTextColor.RED));
|
|
+ return true;
|
|
+ }
|
|
+ return executeRegion(sender, commandLabel, args);
|
|
+ }
|
|
+ default: {
|
|
+ sender.sendMessage(Component.text("Type '" + args[0] + "' must be one of: [server, region]", NamedTextColor.RED));
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public List<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/FoliaRegionScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..aa10f3273e3bb35cf59d324644c269893cc12e99
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java
|
|
@@ -0,0 +1,422 @@
|
|
+package io.papermc.paper.threadedregions.scheduler;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager;
|
|
+import io.papermc.paper.threadedregions.RegionizedData;
|
|
+import io.papermc.paper.threadedregions.RegionizedServer;
|
|
+import io.papermc.paper.threadedregions.TickRegionScheduler;
|
|
+import io.papermc.paper.threadedregions.TickRegions;
|
|
+import io.papermc.paper.util.CoordinateUtils;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.server.level.TicketType;
|
|
+import net.minecraft.util.Unit;
|
|
+import org.bukkit.Bukkit;
|
|
+import org.bukkit.World;
|
|
+import org.bukkit.craftbukkit.CraftWorld;
|
|
+import org.bukkit.plugin.IllegalPluginAccessException;
|
|
+import org.bukkit.plugin.Plugin;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.ArrayList;
|
|
+import java.util.Iterator;
|
|
+import java.util.List;
|
|
+import java.util.function.Consumer;
|
|
+import java.util.logging.Level;
|
|
+
|
|
+public final class FoliaRegionScheduler implements RegionScheduler {
|
|
+
|
|
+ private static Runnable wrap(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) {
|
|
+ return () -> {
|
|
+ try {
|
|
+ run.run();
|
|
+ } catch (final Throwable throwable) {
|
|
+ plugin.getLogger().log(Level.WARNING, "Location task for " + plugin.getDescription().getFullName()
|
|
+ + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable);
|
|
+ }
|
|
+ };
|
|
+ }
|
|
+
|
|
+ private static final RegionizedData<Scheduler> SCHEDULER_DATA = new RegionizedData<>(null, Scheduler::new, Scheduler.REGIONISER_CALLBACK);
|
|
+
|
|
+ private static void scheduleInternalOnRegion(final LocationScheduledTask task, final long delay) {
|
|
+ SCHEDULER_DATA.get().queueTask(task, delay);
|
|
+ }
|
|
+
|
|
+ private static void scheduleInternalOffRegion(final LocationScheduledTask task, final long delay) {
|
|
+ final World world = task.world;
|
|
+ if (world == null) {
|
|
+ // cancelled
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ ((CraftWorld) world).getHandle(), task.chunkX, task.chunkZ, () -> {
|
|
+ scheduleInternalOnRegion(task, delay);
|
|
+ }
|
|
+ );
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void execute(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(world, "World may not be null");
|
|
+ Validate.notNull(run, "Runnable may not be null");
|
|
+
|
|
+ RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
|
|
+ ((CraftWorld) world).getHandle(), chunkX, chunkZ, wrap(plugin, world, chunkX, chunkZ, run)
|
|
+ );
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ScheduledTask run(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Consumer<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 long delayTicks) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(world, "World may not be null");
|
|
+ Validate.notNull(task, "Task may not be null");
|
|
+ if (delayTicks <= 0) {
|
|
+ throw new IllegalArgumentException("Delay ticks may not be <= 0");
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled");
|
|
+ }
|
|
+
|
|
+ final LocationScheduledTask ret = new LocationScheduledTask(plugin, world, chunkX, chunkZ, -1, task);
|
|
+
|
|
+ if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
|
|
+ scheduleInternalOnRegion(ret, delayTicks);
|
|
+ } else {
|
|
+ scheduleInternalOffRegion(ret, delayTicks);
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ // handle race condition where plugin is disabled asynchronously
|
|
+ ret.cancel();
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ScheduledTask runAtFixedRate(final Plugin plugin, final World world, final int chunkX, final int chunkZ,
|
|
+ final Consumer<ScheduledTask> task, final long initialDelayTicks, final long periodTicks) {
|
|
+ Validate.notNull(plugin, "Plugin may not be null");
|
|
+ Validate.notNull(world, "World may not be null");
|
|
+ Validate.notNull(task, "Task may not be null");
|
|
+ if (initialDelayTicks <= 0) {
|
|
+ throw new IllegalArgumentException("Initial delay ticks may not be <= 0");
|
|
+ }
|
|
+ if (periodTicks <= 0) {
|
|
+ throw new IllegalArgumentException("Period ticks may not be <= 0");
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled");
|
|
+ }
|
|
+
|
|
+ final LocationScheduledTask ret = new LocationScheduledTask(plugin, world, chunkX, chunkZ, periodTicks, task);
|
|
+
|
|
+ if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
|
|
+ scheduleInternalOnRegion(ret, initialDelayTicks);
|
|
+ } else {
|
|
+ scheduleInternalOffRegion(ret, initialDelayTicks);
|
|
+ }
|
|
+
|
|
+ if (!plugin.isEnabled()) {
|
|
+ // handle race condition where plugin is disabled asynchronously
|
|
+ ret.cancel();
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ public void tick() {
|
|
+ SCHEDULER_DATA.get().tick();
|
|
+ }
|
|
+
|
|
+ private static final class Scheduler {
|
|
+ private static final RegionizedData.RegioniserCallback<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 long delay) {
|
|
+ // note: must be on the thread that owns this scheduler
|
|
+ // note: delay > 0
|
|
+
|
|
+ final World world = task.world;
|
|
+ if (world == null) {
|
|
+ // cancelled
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ final int sectionX = task.chunkX >> TickRegions.getRegionChunkShift();
|
|
+ final int sectionZ = task.chunkZ >> TickRegions.getRegionChunkShift();
|
|
+
|
|
+ final Long2ObjectOpenHashMap<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 long 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 long 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 bfb1de19f53d5d7c7b65e25a606fabfa416706b3..61e51d68df3bc5be3622c32025af9cb418b4ca75 100644
|
|
--- a/src/main/java/io/papermc/paper/util/CollisionUtil.java
|
|
+++ b/src/main/java/io/papermc/paper/util/CollisionUtil.java
|
|
@@ -1648,7 +1648,7 @@ public final class CollisionUtil {
|
|
|
|
for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
|
|
for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
|
|
- final ChunkAccess chunk = loadChunks ? chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, true) : chunkSource.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ);
|
|
+ final ChunkAccess chunk = !io.papermc.paper.util.TickThread.isTickThreadFor(chunkSource.chunkMap.level, currChunkX, currChunkZ) ? null : (loadChunks ? chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, true) : chunkSource.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ)); // Folia - ignore chunk if we do not own the region
|
|
|
|
if (chunk == null) {
|
|
if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) {
|
|
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 8240bb085b619f257f8c0a25775e0b15068e440f..8f91b7f44baaf62b829a81afc0633311e6c13f19 100644
|
|
--- a/src/main/java/io/papermc/paper/util/MCUtil.java
|
|
+++ b/src/main/java/io/papermc/paper/util/MCUtil.java
|
|
@@ -336,6 +336,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());
|
|
}
|
|
@@ -476,6 +477,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 f9063e2282f89e97a378f06822cde0a64ab03f9a..c6b3c747d4c9792c3b690af4d45b13d2b05039ee 100644
|
|
--- a/src/main/java/io/papermc/paper/util/TickThread.java
|
|
+++ b/src/main/java/io/papermc/paper/util/TickThread.java
|
|
@@ -1,5 +1,11 @@
|
|
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;
|
|
@@ -114,46 +120,137 @@ public class TickThread extends Thread {
|
|
}
|
|
|
|
public static boolean isShutdownThread() {
|
|
- return false;
|
|
+ return Thread.currentThread().getClass() == RegionShutdownThread.class;
|
|
}
|
|
|
|
public static boolean isTickThreadFor(final ServerLevel world, final BlockPos pos) {
|
|
- return isTickThread();
|
|
+ return isTickThreadFor(world, pos.getX() >> 4, pos.getZ() >> 4);
|
|
}
|
|
|
|
public static boolean isTickThreadFor(final ServerLevel world, final ChunkPos pos) {
|
|
- return isTickThread();
|
|
+ return isTickThreadFor(world, pos.x, pos.z);
|
|
}
|
|
|
|
public static boolean isTickThreadFor(final ServerLevel world, final Vec3 pos) {
|
|
- return isTickThread();
|
|
+ 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 isTickThread();
|
|
+ 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 AABB aabb) {
|
|
- return isTickThread();
|
|
+ return isTickThreadFor(
|
|
+ world,
|
|
+ CoordinateUtils.getChunkCoordinate(aabb.minX), CoordinateUtils.getChunkCoordinate(aabb.minZ),
|
|
+ CoordinateUtils.getChunkCoordinate(aabb.maxX), CoordinateUtils.getChunkCoordinate(aabb.maxZ)
|
|
+ );
|
|
}
|
|
|
|
public static boolean isTickThreadFor(final ServerLevel world, final double blockX, final double blockZ) {
|
|
- return isTickThread();
|
|
+ return isTickThreadFor(world, CoordinateUtils.getChunkCoordinate(blockX), CoordinateUtils.getChunkCoordinate(blockZ));
|
|
}
|
|
|
|
public static boolean isTickThreadFor(final ServerLevel world, final Vec3 position, final Vec3 deltaMovement, final int buffer) {
|
|
- return isTickThread();
|
|
+ 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) {
|
|
- return isTickThread();
|
|
+ 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 isTickThread();
|
|
+ return isTickThreadFor(world, chunkX - radius, chunkZ - radius, chunkX + radius, chunkZ + radius);
|
|
}
|
|
|
|
public static boolean isTickThreadFor(final Entity entity) {
|
|
- return isTickThread();
|
|
+ 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) {
|
|
+ final ServerGamePacketListenerImpl possibleBad = serverPlayer.connection;
|
|
+ if (possibleBad == null) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ final net.minecraft.network.PacketListener packetListener = possibleBad.connection.getPacketListener();
|
|
+ if (packetListener instanceof ServerGamePacketListenerImpl gamePacketListener) {
|
|
+ return gamePacketListener.waitingForSwitchToConfig;
|
|
+ }
|
|
+ if (packetListener instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) {
|
|
+ return !configurationPacketListener.switchToMain;
|
|
+ }
|
|
+ return true;
|
|
+ } 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 56ae02aab93b9a698e9d2f07a0448aa4767169d9..4e196b4e7bc8ac6da8d3e61e68bdf461c7ea821f 100644
|
|
--- a/src/main/java/net/minecraft/commands/CommandSourceStack.java
|
|
+++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java
|
|
@@ -69,7 +69,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), (j) -> {
|
|
+ }, EntityAnchorArgument.Anchor.FEET, CommandSigningContext.ANONYMOUS, TaskChainer.immediate((Runnable run) -> { io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run);}), (j) -> { // Folia - region threading
|
|
});
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java
|
|
index 3eec879bf3975636739b2491cc05b8177032d16d..3435bdeaf723c64103f7c924ea42a4ec78f2ba01 100644
|
|
--- a/src/main/java/net/minecraft/commands/Commands.java
|
|
+++ b/src/main/java/net/minecraft/commands/Commands.java
|
|
@@ -146,13 +146,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);
|
|
@@ -162,47 +162,47 @@ 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);
|
|
RandomCommand.register(this.dispatcher);
|
|
- ReloadCommand.register(this.dispatcher);
|
|
+ //ReloadCommand.register(this.dispatcher); // Folia - region threading
|
|
RecipeCommand.register(this.dispatcher);
|
|
- ReturnCommand.register(this.dispatcher);
|
|
- RideCommand.register(this.dispatcher);
|
|
+ //ReturnCommand.register(this.dispatcher); // Folia - region threading - TODO later
|
|
+ //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);
|
|
}
|
|
@@ -223,8 +223,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);
|
|
@@ -454,9 +454,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 155bd3d6d9c7d3cac7fd04de8210301251d1e17a..446f590dcf8f1d30b365e71515683c9a592a608b 100644
|
|
--- a/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java
|
|
+++ b/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java
|
|
@@ -32,7 +32,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 80dbeb0a988c749feaaba26ce5ad93c181d88a5d..e92d8c8f6b208642e87cdb4080d3b38f95d84503 100644
|
|
--- a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
|
|
+++ b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
|
|
@@ -62,7 +62,7 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1);
|
|
|
|
BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d1, d2 + d4, d3));
|
|
- 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 379890ae05b2fb4bd81b2fa907413d3736ba1169..4ea33c913b005d81a600215ac35a644ee1396cb3 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 a0c7c6208314d981e8577ad69ef1c5193290a085..f0c2ee38e9101a6c593f6deb6b8101b72ad4a067 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);
|
|
}
|
|
|
|
@@ -633,7 +633,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);
|
|
|
|
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 = CraftLocation.toBukkit(blockposition, worldserver.getWorld());
|
|
- 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);
|
|
@@ -884,7 +885,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);
|
|
}
|
|
|
|
@@ -941,7 +942,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);
|
|
}
|
|
|
|
@@ -990,7 +991,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);
|
|
}
|
|
|
|
@@ -1063,7 +1064,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 e17090003988ad2c890d48666c2234b14d511345..5bce4ad0d3025def91649592ec8610b975f8ac8a 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);
|
|
|
|
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 6f2adf2334e35e8a617a4ced0c1af2abf32bbd8d..de323c881b6d54061f7fef2b56658bebc2d71604 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().booleanValue()) { // Folia - region threading
|
|
pointer.level().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 c0ea20dcee8bb293df96bc6ee019e50ad6b383fd..b84e9d5ae5efe1a8257a5f1f78a0ab66113a698e 100644
|
|
--- a/src/main/java/net/minecraft/network/Connection.java
|
|
+++ b/src/main/java/net/minecraft/network/Connection.java
|
|
@@ -84,7 +84,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<WrappedConsumer> pendingActions = Queues.newConcurrentLinkedQueue();
|
|
+ private final Queue<WrappedConsumer> pendingActions = new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>(); // Folia - region threading - connection fixes
|
|
public Channel channel;
|
|
public SocketAddress address;
|
|
// Spigot Start
|
|
@@ -99,7 +99,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
@Nullable
|
|
private Component disconnectedReason;
|
|
private boolean encrypted;
|
|
- private boolean disconnectionHandled;
|
|
+ private final java.util.concurrent.atomic.AtomicBoolean disconnectionHandled = new java.util.concurrent.atomic.AtomicBoolean(false); // Folia - region threading - may be called concurrently during configuration stage
|
|
private int receivedPackets;
|
|
private int sentPackets;
|
|
private float averageReceivedPackets;
|
|
@@ -153,6 +153,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.ServerCommonPacketListenerImpl;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
public void channelActive(ChannelHandlerContext channelhandlercontext) throws Exception {
|
|
super.channelActive(channelhandlercontext);
|
|
this.channel = channelhandlercontext.channel();
|
|
@@ -163,7 +189,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
if (this.delayedDisconnect != null) {
|
|
this.disconnect(this.delayedDisconnect);
|
|
}
|
|
-
|
|
+ this.becomeActive = true; // Folia - region threading
|
|
}
|
|
|
|
public static void setInitialProtocolAttributes(Channel channel) {
|
|
@@ -386,7 +412,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
}
|
|
|
|
packet.onPacketDispatch(this.getPlayer());
|
|
- if (connected && (InnerUtil.canSendImmediate(this, packet)
|
|
+ if (false && connected && (InnerUtil.canSendImmediate(this, packet) // Folia - region threading - connection fixes
|
|
|| (io.papermc.paper.util.MCUtil.isMainThread() && packet.isReady() && this.pendingActions.isEmpty()
|
|
&& (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())))) {
|
|
this.sendPacket(packet, callbacks, flush);
|
|
@@ -415,11 +441,12 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
}
|
|
|
|
public void runOnceConnected(Consumer<Connection> task) {
|
|
- if (this.isConnected()) {
|
|
+ if (false && this.isConnected()) { // Folia - region threading - connection fixes
|
|
this.flushQueue();
|
|
task.accept(this);
|
|
} else {
|
|
this.pendingActions.add(new WrappedConsumer(task)); // Paper - Optimize network
|
|
+ this.flushQueue(); // Folia - region threading - connection fixes
|
|
}
|
|
|
|
}
|
|
@@ -478,10 +505,11 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
}
|
|
|
|
public void flushChannel() {
|
|
- if (this.isConnected()) {
|
|
+ if (false && this.isConnected()) { // Folia - region threading - connection fixes
|
|
this.flush();
|
|
} else {
|
|
this.pendingActions.add(new WrappedConsumer(Connection::flush)); // Paper - Optimize network
|
|
+ this.flushQueue(); // Folia - region threading - connection fixes
|
|
}
|
|
|
|
}
|
|
@@ -516,53 +544,61 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
|
|
// Paper start - Optimize network: Rewrite this to be safer if ran off main thread
|
|
private boolean flushQueue() {
|
|
- if (!this.isConnected()) {
|
|
- return true;
|
|
- }
|
|
- if (io.papermc.paper.util.MCUtil.isMainThread()) {
|
|
- return this.processQueue();
|
|
- } else if (this.isPending) {
|
|
- // Should only happen during login/status stages
|
|
- synchronized (this.pendingActions) {
|
|
- return this.processQueue();
|
|
- }
|
|
- }
|
|
- return false;
|
|
+ return this.processQueue(); // Folia - region threading - connection fixes
|
|
}
|
|
|
|
+ // Folia start - region threading - connection fixes
|
|
+ // 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 static boolean canWrite(WrappedConsumer queued) {
|
|
+ return queued != null && (!(queued instanceof PacketSendAction packet) || packet.packet.isReady());
|
|
+ }
|
|
+
|
|
+ private boolean canWritePackets() {
|
|
+ return canWrite(this.pendingActions.peek());
|
|
+ }
|
|
+ // Folia end - region threading - connection fixes
|
|
+
|
|
private boolean processQueue() {
|
|
- if (this.pendingActions.isEmpty()) {
|
|
+ // Folia start - region threading - connection fixes
|
|
+ if (!this.isConnected()) {
|
|
return true;
|
|
}
|
|
|
|
- // 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
|
|
- final java.util.Iterator<WrappedConsumer> iterator = this.pendingActions.iterator();
|
|
- while (iterator.hasNext()) {
|
|
- final WrappedConsumer queued = iterator.next(); // poll -> peek
|
|
-
|
|
- // Fix NPE (Spigot bug caused by handleDisconnection())
|
|
- if (queued == null) {
|
|
- return true;
|
|
- }
|
|
+ while (this.canWritePackets()) {
|
|
+ final boolean set = this.flushingQueue.getAndSet(true);
|
|
+ try {
|
|
+ if (set) {
|
|
+ // we didn't acquire the lock, break
|
|
+ return false;
|
|
+ }
|
|
|
|
- if (queued.isConsumed()) {
|
|
- continue;
|
|
- }
|
|
+ ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<WrappedConsumer> queue =
|
|
+ (ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<WrappedConsumer>)this.pendingActions;
|
|
+ WrappedConsumer holder;
|
|
+ for (;;) {
|
|
+ // synchronise so that queue clears appear atomic
|
|
+ synchronized (queue) {
|
|
+ holder = queue.pollIf(Connection::canWrite);
|
|
+ }
|
|
+ if (holder == null) {
|
|
+ break;
|
|
+ }
|
|
|
|
- if (queued instanceof PacketSendAction packetSendAction) {
|
|
- final Packet<?> packet = packetSendAction.packet;
|
|
- if (!packet.isReady()) {
|
|
- return false;
|
|
+ holder.accept(this);
|
|
}
|
|
- }
|
|
|
|
- iterator.remove();
|
|
- if (queued.tryMarkConsumed()) {
|
|
- queued.accept(this);
|
|
+ } finally {
|
|
+ if (!set) {
|
|
+ this.flushingQueue.set(false);
|
|
+ }
|
|
}
|
|
}
|
|
+
|
|
return true;
|
|
+ // Folia end - region threading - connection fixes
|
|
}
|
|
// Paper end - Optimize network
|
|
|
|
@@ -571,21 +607,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.ServerCommonPacketListenerImpl commonPacketListener) {
|
|
+ commonPacketListener.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.VERIFYING
|
|
- || 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
|
|
@@ -597,7 +653,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
// Paper end
|
|
}
|
|
|
|
- if (!this.isConnected() && !this.disconnectionHandled) {
|
|
+ if (!this.isConnected()) {// Folia - region threading - it's fine to call if it is already handled, as it no longer logs
|
|
this.handleDisconnection();
|
|
}
|
|
|
|
@@ -643,6 +699,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
|
|
|
|
}
|
|
|
|
@@ -822,10 +879,10 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
|
|
public void handleDisconnection() {
|
|
if (this.channel != null && !this.channel.isOpen()) {
|
|
- if (this.disconnectionHandled) {
|
|
+ if (this.disconnectionHandled.getAndSet(true)) { // Folia - region threading - may be called concurrently during configuration stage
|
|
// Connection.LOGGER.warn("handleDisconnection() called twice"); // Paper - Don't log useless message
|
|
} else {
|
|
- this.disconnectionHandled = true;
|
|
+ // Folia - region threading - may be called concurrently during configuration stage - set above
|
|
PacketListener packetlistener = this.getPacketListener();
|
|
PacketListener packetlistener1 = packetlistener != null ? packetlistener : this.disconnectListener;
|
|
|
|
@@ -856,6 +913,21 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
((java.net.InetSocketAddress)address).getAddress(), false).callEvent();
|
|
}
|
|
}
|
|
+ // Folia start - region threading
|
|
+ if (packetlistener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) {
|
|
+ net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection(
|
|
+ commonPacketListener.getOwner().getName(),
|
|
+ commonPacketListener.getOwner().getId(), this
|
|
+ );
|
|
+ } else if (packetlistener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) {
|
|
+ if (loginPacketListener.state.ordinal() >= net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING.ordinal()) {
|
|
+ net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection(
|
|
+ loginPacketListener.authenticatedProfile.getName(),
|
|
+ loginPacketListener.authenticatedProfile.getId(), this
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
// Paper end
|
|
|
|
}
|
|
@@ -877,15 +949,25 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
|
// Paper start - Optimize network
|
|
public void clearPacketQueue() {
|
|
final net.minecraft.server.level.ServerPlayer player = getPlayer();
|
|
- for (final Consumer<Connection> queuedAction : this.pendingActions) {
|
|
- if (queuedAction instanceof PacketSendAction packetSendAction) {
|
|
- final Packet<?> packet = packetSendAction.packet;
|
|
- if (packet.hasFinishListener()) {
|
|
- packet.onPacketDispatchFinish(player, null);
|
|
+ // Folia start - region threading - connection fixes
|
|
+ java.util.List<Connection.PacketSendAction> queuedPackets = new java.util.ArrayList<>();
|
|
+ // synchronise so that flushQueue does not poll values while the queue is being cleared
|
|
+ synchronized (this.pendingActions) {
|
|
+ Connection.WrappedConsumer consumer;
|
|
+ while ((consumer = this.pendingActions.poll()) != null) {
|
|
+ if (consumer instanceof Connection.PacketSendAction packetHolder) {
|
|
+ queuedPackets.add(packetHolder);
|
|
}
|
|
}
|
|
}
|
|
- this.pendingActions.clear();
|
|
+
|
|
+ for (Connection.PacketSendAction queuedPacket : queuedPackets) {
|
|
+ Packet<?> packet = queuedPacket.packet;
|
|
+ if (packet.hasFinishListener()) {
|
|
+ packet.onPacketDispatchFinish(player, null);
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading - connection fixes
|
|
}
|
|
|
|
private static class InnerUtil { // Attempt to hide these methods from ProtocolLib, so it doesn't accidently pick them up.
|
|
diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java
|
|
index 9a49f5271ec1d9de17632bfffe8309cb1ba0d8b1..c06bed93f6a701b1639c1cc334802e7b802431a5 100644
|
|
--- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java
|
|
+++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java
|
|
@@ -43,7 +43,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 ServerCommonPacketListenerImpl && ((ServerCommonPacketListenerImpl) listener).processedDisconnect)) return; // CraftBukkit, MC-142590
|
|
@@ -77,7 +77,21 @@ public class PacketUtils {
|
|
}
|
|
// Paper end - detailed watchdog information
|
|
|
|
- });
|
|
+ }; // Folia start - region threading
|
|
+ // ignore retired state, if removed then we don't want the packet to be handled
|
|
+ if (listener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl gamePacketListener) {
|
|
+ gamePacketListener.player.getBukkitEntity().taskScheduler.schedule(
|
|
+ (net.minecraft.server.level.ServerPlayer player) -> {
|
|
+ run.run();
|
|
+ },
|
|
+ null, 1L
|
|
+ );
|
|
+ } else if (listener instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run);
|
|
+ } else {
|
|
+ throw new UnsupportedOperationException("Unknown listener: " + listener);
|
|
+ }
|
|
+ // Folia end - region threading
|
|
throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD;
|
|
// CraftBukkit start - SPIGOT-5477, MC-142590
|
|
} else if (MinecraftServer.getServer().hasStopped() || (listener instanceof ServerCommonPacketListenerImpl && ((ServerCommonPacketListenerImpl) listener).processedDisconnect)) {
|
|
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
|
|
index 6758e3ff1100bba2852e44cbb0b38ce4f22490e8..75d5e7f1b247e27f526a3f76fa7df7aeca4e90ac 100644
|
|
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
|
|
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
|
|
@@ -238,7 +238,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
private volatile boolean running;
|
|
private volatile boolean isRestarting = false; // Paper - flag to signify we're attempting to restart
|
|
private boolean stopped;
|
|
- private int tickCount;
|
|
+ // Folia - region threading
|
|
protected final Proxy proxy;
|
|
private boolean onlineMode;
|
|
private boolean preventProxyConnections;
|
|
@@ -287,7 +287,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
public OptionSet options;
|
|
public org.bukkit.command.ConsoleCommandSender console;
|
|
//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;
|
|
@@ -300,7 +300,7 @@ 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
|
|
@@ -309,6 +309,34 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
public static final long SERVER_INIT = System.nanoTime();
|
|
// Paper end - lag compensation
|
|
|
|
+ // 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
|
|
@@ -594,7 +622,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, this.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);
|
|
@@ -606,6 +648,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()));
|
|
}
|
|
@@ -669,7 +712,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());
|
|
@@ -690,6 +733,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);
|
|
}
|
|
|
|
@@ -770,7 +814,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();
|
|
}
|
|
|
|
@@ -793,7 +837,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
|
|
// CraftBukkit start
|
|
@@ -897,7 +941,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;
|
|
@@ -907,7 +981,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();
|
|
@@ -934,12 +1008,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
this.getConnection().stop();
|
|
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();
|
|
|
|
@@ -955,6 +1036,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 {
|
|
@@ -1010,6 +1096,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();
|
|
@@ -1091,10 +1178,23 @@ 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");
|
|
- this.server.getScheduler().mainThreadHeartbeat(this.tickCount); // run all 1 tick delay tasks during init,
|
|
+ //this.server.getScheduler().mainThreadHeartbeat(this.tickCount); // run all 1 tick delay tasks during init, // Folia - region threading
|
|
// this is going to be the first thing the tick process does anyways, so move done and run it after
|
|
// everything is init before watchdog tick.
|
|
// anything at 3+ won't be caught here but also will trip watchdog....
|
|
@@ -1126,8 +1226,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);
|
|
@@ -1145,7 +1245,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
if (this.debugCommandProfilerDelayStart) {
|
|
this.debugCommandProfilerDelayStart = false;
|
|
- this.debugCommandProfiler = new MinecraftServer.TimeProfiler(Util.getNanos(), this.tickCount);
|
|
+ //this.debugCommandProfiler = new MinecraftServer.TimeProfiler(Util.getNanos(), this.tickCount); // Folia - region threading
|
|
}
|
|
|
|
//MinecraftServer.currentTick = (int) (System.currentTimeMillis() / 50); // CraftBukkit // Paper - don't overwrite current tick time
|
|
@@ -1153,7 +1253,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);
|
|
@@ -1276,21 +1376,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
@Override
|
|
public TickTask wrapRunnable(Runnable runnable) {
|
|
- // Paper start - anything that does try to post to main during watchdog crash, run on watchdog
|
|
- if (this.hasStopped && Thread.currentThread().equals(shutdownThread)) {
|
|
- runnable.run();
|
|
- runnable = () -> {};
|
|
- }
|
|
- // Paper end
|
|
- return new TickTask(this.tickCount, runnable);
|
|
+ throw new UnsupportedOperationException(); // Folia - region threading
|
|
}
|
|
|
|
protected boolean shouldRun(TickTask ticktask) {
|
|
- return ticktask.getTick() + 3 < this.tickCount || this.haveTime();
|
|
+ throw new UnsupportedOperationException(); // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public boolean pollTask() {
|
|
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
|
|
boolean flag = this.pollTaskInternal();
|
|
|
|
this.mayHaveDelayedTasks = flag;
|
|
@@ -1298,6 +1393,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;
|
|
@@ -1320,6 +1416,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);
|
|
}
|
|
@@ -1362,22 +1459,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) {
|
|
+ // Folia - region threading
|
|
+ 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();
|
|
}
|
|
@@ -1388,15 +1527,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
playerSaveInterval = autosavePeriod;
|
|
}
|
|
this.profiler.push("save");
|
|
- final boolean fullSave = autosavePeriod > 0 && this.tickCount % autosavePeriod == 0;
|
|
+ final boolean fullSave = autosavePeriod > 0 && io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() % autosavePeriod == 0; // Folia - region threading
|
|
try {
|
|
this.isSaving = true;
|
|
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 {
|
|
@@ -1406,27 +1545,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((int)io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(), ((double)(endTime - startTime) / 1000000D), remaining).callEvent(); // Folia - region ticking
|
|
// Paper end
|
|
this.profiler.push("tallying");
|
|
- long j = this.tickTimes[this.tickCount % 100] = Util.getNanos() - i;
|
|
-
|
|
- this.averageTickTime = this.averageTickTime * 0.8F + (float) j / 1000000.0F * 0.19999999F;
|
|
- long k = Util.getNanos();
|
|
-
|
|
- // Paper start
|
|
- tickTimes5s.add(this.tickCount, j);
|
|
- tickTimes10s.add(this.tickCount, j);
|
|
- tickTimes60s.add(this.tickCount, j);
|
|
- // Paper end
|
|
- this.logTickTime(k - i);
|
|
+ // Folia - region threading
|
|
this.profiler.pop();
|
|
org.spigotmc.WatchdogThread.tick(); // Spigot
|
|
co.aikar.timings.TimingsManager.FULL_SERVER_TICK.stopTiming(); // Paper
|
|
@@ -1434,6 +1563,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
protected void logTickTime(long nanos) {}
|
|
|
|
+ // Folia start - region threading
|
|
+ public void rebuildServerStatus() {
|
|
+ this.status = this.buildServerStatus();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
private ServerStatus buildServerStatus() {
|
|
ServerStatus.Players serverping_serverpingplayersample = this.buildPlayerStatus();
|
|
|
|
@@ -1441,7 +1576,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()) {
|
|
@@ -1462,31 +1597,18 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
}
|
|
|
|
- public void tickChildren(BooleanSupplier shouldKeepTicking) {
|
|
- this.getPlayerList().getPlayers().forEach((entityplayer) -> {
|
|
+ public void tickChildren(BooleanSupplier shouldKeepTicking, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - region threading
|
|
+ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - regionised ticking
|
|
+ if (region == null) this.getPlayerList().getPlayers().forEach((entityplayer) -> { // Folia - region threading
|
|
entityplayer.connection.suspendFlushing();
|
|
});
|
|
MinecraftTimings.bukkitSchedulerTimer.startTiming(); // Spigot // Paper
|
|
- this.server.getScheduler().mainThreadHeartbeat(this.tickCount); // CraftBukkit
|
|
+ // Folia - region threading
|
|
MinecraftTimings.bukkitSchedulerTimer.stopTiming(); // Spigot // Paper
|
|
- // Paper start - Folia scheduler API
|
|
- ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) Bukkit.getGlobalRegionScheduler()).tick();
|
|
- getAllLevels().forEach(level -> {
|
|
- for (final Entity entity : level.getEntityLookup().getAllCopy()) {
|
|
- if (entity.isRemoved()) {
|
|
- continue;
|
|
- }
|
|
- final org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw();
|
|
- if (bukkit != null) {
|
|
- bukkit.taskScheduler.executeTick();
|
|
- }
|
|
- }
|
|
- });
|
|
- // Paper end - Folia scheduler API
|
|
- io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper
|
|
+ // Folia - region threading - moved to global tick - and moved entity scheduler to tickRegion
|
|
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
|
|
@@ -1494,7 +1616,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
|
|
@@ -1502,13 +1624,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 level : this.getAllLevels()) {
|
|
+ for (final ServerLevel level : (region == null ? this.getAllLevels() : Arrays.asList(region.world))) { // Folia - region threading
|
|
final boolean doDaylight = level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT);
|
|
final long dayTime = level.getDayTime();
|
|
long worldTime = level.getGameTime();
|
|
final ClientboundSetTimePacket worldPacket = new ClientboundSetTimePacket(worldTime, dayTime, doDaylight);
|
|
- for (Player entityhuman : level.players()) {
|
|
- if (!(entityhuman instanceof ServerPlayer) || (tickCount + entityhuman.getId()) % 20 != 0) {
|
|
+ for (Player entityhuman : level.getLocalPlayers()) { // Folia - region threading
|
|
+ if (!(entityhuman instanceof ServerPlayer) || (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + entityhuman.getId()) % 20 != 0) { // Folia - region threading
|
|
continue;
|
|
}
|
|
ServerPlayer entityplayer = (ServerPlayer) entityhuman;
|
|
@@ -1521,14 +1643,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.updateLagCompensationTick(); // Paper - lag compensation
|
|
- 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();
|
|
@@ -1545,7 +1664,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();
|
|
@@ -1569,17 +1688,17 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
this.profiler.pop();
|
|
this.profiler.pop();
|
|
- worldserver.explosionDensityCache.clear(); // Paper - Optimize explosions
|
|
+ regionizedWorldData.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();
|
|
@@ -1588,7 +1707,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
|
|
@@ -1596,7 +1715,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
this.profiler.popPush("send chunks");
|
|
iterator = this.playerList.getPlayers().iterator();
|
|
|
|
- while (iterator.hasNext()) {
|
|
+ if (region == null) while (iterator.hasNext()) { // Folia - region threading
|
|
ServerPlayer entityplayer = (ServerPlayer) iterator.next();
|
|
|
|
entityplayer.connection.chunkSender.sendNextChunks(entityplayer);
|
|
@@ -1939,7 +2058,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
|
|
public int getTickCount() {
|
|
- return this.tickCount;
|
|
+ throw new UnsupportedOperationException(); // Folia - region threading
|
|
}
|
|
|
|
public int getSpawnProtectionRadius() {
|
|
@@ -1994,6 +2113,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;
|
|
}
|
|
|
|
@@ -2008,6 +2136,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 {
|
|
@@ -2611,14 +2740,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
|
|
public ProfileResults stopTimeProfiler() {
|
|
- if (this.debugCommandProfiler == null) {
|
|
- return EmptyProfileResults.EMPTY;
|
|
- } else {
|
|
- ProfileResults methodprofilerresults = this.debugCommandProfiler.stop(Util.getNanos(), this.tickCount);
|
|
-
|
|
- this.debugCommandProfiler = null;
|
|
- return methodprofilerresults;
|
|
- }
|
|
+ throw new UnsupportedOperationException(); // Folia - region threading
|
|
}
|
|
|
|
public int getMaxChainedNeighborUpdates() {
|
|
@@ -2747,33 +2869,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;
|
|
@@ -2782,13 +2889,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
|
|
@@ -2801,7 +2908,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 7d116ff3c0cf5c30583c10853940e8897e1d9ca3..90c3347d650113c9b5fa9318cb1b2805295b7cf5 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/AdvancementCommands.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/AdvancementCommands.java
|
|
@@ -65,7 +65,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) {
|
|
@@ -112,9 +116,13 @@ public class AdvancementCommands {
|
|
throw new CommandRuntimeException(Component.translatable("commands.advancement.criterionNotFound", Advancement.name(advancement), 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 506c5a8369ec0b2af3605e5bdfce8b000cf56ec4..efb41b6f138fd45f6b240b1d17c3320d720a2f34 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/AttributeCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/AttributeCommand.java
|
|
@@ -92,70 +92,124 @@ 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(() -> {
|
|
- return 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);
|
|
+ double d = livingEntity.getAttributeValue(attribute);
|
|
+ source.sendSuccess(() -> {
|
|
+ return 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(() -> {
|
|
- return 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(() -> {
|
|
+ return 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(() -> {
|
|
- return 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(() -> {
|
|
+ return 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(() -> {
|
|
- return Component.translatable("commands.attribute.base_value.set.success", getAttributeDescription(attribute), target.getName(), value);
|
|
- }, false);
|
|
- return 1;
|
|
+ // Folia start - region threading
|
|
+ target.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> {
|
|
+ try {
|
|
+ getAttributeInstance(nmsEntity, attribute).setBaseValue(value);
|
|
+ source.sendSuccess(() -> {
|
|
+ return Component.translatable("commands.attribute.base_value.set.success", getAttributeDescription(attribute), nmsEntity.getName(), value);
|
|
+ }, false);
|
|
+ } catch (CommandSyntaxException ex) {
|
|
+ sendMessage(source, ex);
|
|
+ }
|
|
+ }, null, 1L);
|
|
+ return 0;
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
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(() -> {
|
|
- return 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(() -> {
|
|
+ return Component.translatable("commands.attribute.modifier.add.success", uuid, getAttributeDescription(attribute), nmsEntity.getName());
|
|
+ }, false);
|
|
+ }
|
|
+ } catch (CommandSyntaxException ex) {
|
|
+ sendMessage(source, ex);
|
|
+ }
|
|
+ }, null, 1L);
|
|
+ return 0;
|
|
+ // 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(() -> {
|
|
- return 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(() -> {
|
|
+ return 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 0;
|
|
+ // 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 90c061eaf40ed756dcd56bb877a617a219ea90e1..36f5d081fae92289e1b6bf012018671343dd92a0 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 b8250748606d0356da0e15d432784fb3ae59438a..473d3016f604a41ec3626bb1816888d7d2b5778d 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/DamageCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/DamageCommand.java
|
|
@@ -34,14 +34,30 @@ 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)) {
|
|
+ // 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(() -> {
|
|
- return Component.translatable("commands.damage.success", amount, target.getDisplayName());
|
|
- }, true);
|
|
- return 1;
|
|
+ return 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 12d4c141a1bc72e53e18f73d7ee4d5a2467e08e1..63815d962a5f966078dca36ca30467923da816b4 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 ebe50e2e69d346ce9266ed3f180d91ceb58008bd..227acbe74ff009c9275542af734852e3044b111b 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;
|
|
}
|
|
}
|
|
@@ -114,8 +122,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
|
|
}
|
|
}
|
|
|
|
@@ -144,8 +160,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 664cbce2e06fcb95d3d3d6c5302fc9119f938925..3f68902a2842f1dddc73e86ba6278e6b4b39d1dc 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/EnchantCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/EnchantCommand.java
|
|
@@ -46,47 +46,73 @@ 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()) {
|
|
throw ERROR_LEVEL_TOO_HIGH.create(level, enchantment2.getMaxLevel());
|
|
} else {
|
|
- int i = 0;
|
|
+ final java.util.concurrent.atomic.AtomicInteger changed = new java.util.concurrent.atomic.AtomicInteger(0); // Folia - region threading
|
|
+ final java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger(targets.size()); // Folia - region threading
|
|
+ final java.util.concurrent.atomic.AtomicReference<Component> possibleSingleDisplayName = new java.util.concurrent.atomic.AtomicReference<>(); // Folia - region threading
|
|
|
|
for(Entity entity : targets) {
|
|
if (entity instanceof LivingEntity) {
|
|
- LivingEntity livingEntity = (LivingEntity)entity;
|
|
- ItemStack itemStack = livingEntity.getMainHandItem();
|
|
- if (!itemStack.isEmpty()) {
|
|
- if (enchantment2.canEnchant(itemStack) && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantments(itemStack).keySet(), enchantment2)) {
|
|
- itemStack.enchant(enchantment2, level);
|
|
- ++i;
|
|
- } else if (targets.size() == 1) {
|
|
- throw ERROR_INCOMPATIBLE.create(itemStack.getItem().getName(itemStack).getString());
|
|
+ // Folia start - region threading
|
|
+ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> {
|
|
+ try {
|
|
+ LivingEntity livingEntity = (LivingEntity)nmsEntity;
|
|
+ ItemStack itemStack = livingEntity.getMainHandItem();
|
|
+ if (!itemStack.isEmpty()) {
|
|
+ if (enchantment2.canEnchant(itemStack) && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantments(itemStack).keySet(), enchantment2)) {
|
|
+ itemStack.enchant(enchantment2, level);
|
|
+ possibleSingleDisplayName.set(livingEntity.getDisplayName());
|
|
+ changed.incrementAndGet();
|
|
+ } else if (targets.size() == 1) {
|
|
+ throw ERROR_INCOMPATIBLE.create(itemStack.getItem().getName(itemStack).getString());
|
|
+ }
|
|
+ } else if (targets.size() == 1) {
|
|
+ throw ERROR_NO_ITEM.create(livingEntity.getName().getString());
|
|
+ }
|
|
+ } catch (final CommandSyntaxException exception) {
|
|
+ sendMessage(source, exception);
|
|
+ return; // don't send feedback twice
|
|
}
|
|
- } else if (targets.size() == 1) {
|
|
- throw ERROR_NO_ITEM.create(livingEntity.getName().getString());
|
|
- }
|
|
+ sendFeedback(source, enchantment2, level, possibleSingleDisplayName, count, changed);
|
|
+ }, ignored -> sendFeedback(source, enchantment2, level, possibleSingleDisplayName, count, changed), 1L);
|
|
} else if (targets.size() == 1) {
|
|
throw ERROR_NOT_LIVING_ENTITY.create(entity.getName().getString());
|
|
+ } else {
|
|
+ sendFeedback(source, enchantment2, level, possibleSingleDisplayName, count, changed);
|
|
+ // Folia end - region threading
|
|
}
|
|
}
|
|
-
|
|
+ return targets.size(); // Folia - region threading
|
|
+ }
|
|
+ }
|
|
+ // Folia start - region threading
|
|
+ private static void sendFeedback(final CommandSourceStack source, final Enchantment enchantment2, final int level, final java.util.concurrent.atomic.AtomicReference<Component> possibleSingleDisplayName, final java.util.concurrent.atomic.AtomicInteger count, final java.util.concurrent.atomic.AtomicInteger changed) {
|
|
+ if (count.decrementAndGet() == 0) {
|
|
+ final int i = changed.get();
|
|
if (i == 0) {
|
|
- throw ERROR_NOTHING_HAPPENED.create();
|
|
+ sendMessage(source, ERROR_NOTHING_HAPPENED.create());
|
|
} else {
|
|
- if (targets.size() == 1) {
|
|
+ if (i == 1) {
|
|
source.sendSuccess(() -> {
|
|
- return Component.translatable("commands.enchant.success.single", enchantment2.getFullname(level), targets.iterator().next().getDisplayName());
|
|
+ return Component.translatable("commands.enchant.success.single", enchantment2.getFullname(level), possibleSingleDisplayName.get());
|
|
}, true);
|
|
} else {
|
|
source.sendSuccess(() -> {
|
|
- return Component.translatable("commands.enchant.success.multiple", enchantment2.getFullname(level), targets.size());
|
|
+ return Component.translatable("commands.enchant.success.multiple", enchantment2.getFullname(level), i);
|
|
}, true);
|
|
}
|
|
-
|
|
- return i;
|
|
}
|
|
}
|
|
}
|
|
+ // Folia end - region threading
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/commands/ExperienceCommand.java b/src/main/java/net/minecraft/server/commands/ExperienceCommand.java
|
|
index 24dfe8e9697331f0d7e67e90b9ca537d7ead575e..30b2dafda64a2e56d53d5d90c3087774a471f950 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/ExperienceCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/ExperienceCommand.java
|
|
@@ -46,16 +46,20 @@ 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(() -> {
|
|
return 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) {
|
|
@@ -75,9 +79,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 618f524a7bba8e5c75445872538c5fd1ceff20e4..87920a58207436b328abff257369a271752df174 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);
|
|
@@ -103,7 +120,11 @@ public class FillBiomeCommand {
|
|
source.sendSuccess(() -> {
|
|
return 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 e75b08126d9c42d49058fc91d68d1906fc277e8a..cb68b211bc7e5d4e4e68aa8c7d9dd95901b98e5f 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())) {
|
|
@@ -93,8 +111,13 @@ public class FillCommand {
|
|
source.sendSuccess(() -> {
|
|
return Component.translatable("commands.fill.success", l);
|
|
}, 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 99250f40978aa3c45df821c3d83f80b541dbb14c..a3ea3fb7ac0f1464ea33e07978ba11c01803dcbe 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java
|
|
@@ -49,47 +49,67 @@ 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(() -> {
|
|
- return 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(() -> {
|
|
+ return 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(() -> {
|
|
- return 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(() -> {
|
|
+ return Component.translatable("commands.forceload.list.single", resourceKey.location(), string);
|
|
+ }, false);
|
|
+ } else {
|
|
+ source.sendSuccess(() -> {
|
|
+ return Component.translatable("commands.forceload.list.multiple", i, resourceKey.location(), string);
|
|
+ }, false);
|
|
+ }
|
|
} else {
|
|
- source.sendSuccess(() -> {
|
|
- return 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);
|
|
@@ -97,61 +117,71 @@ public class ForceLoadCommand {
|
|
source.sendSuccess(() -> {
|
|
return 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);
|
|
+ }
|
|
+ }
|
|
}
|
|
}
|
|
- }
|
|
- }
|
|
|
|
- ChunkPos chunkPos2 = chunkPos;
|
|
- if (r == 0) {
|
|
- throw (forceLoaded ? ERROR_ALL_ADDED : ERROR_NONE_REMOVED).create();
|
|
- } else {
|
|
- if (r == 1) {
|
|
- source.sendSuccess(() -> {
|
|
- return Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".single", chunkPos2, resourceKey.location());
|
|
- }, true);
|
|
- } else {
|
|
- ChunkPos chunkPos3 = new ChunkPos(m, n);
|
|
- ChunkPos chunkPos4 = new ChunkPos(o, p);
|
|
- source.sendSuccess(() -> {
|
|
- return Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".multiple", chunkPos2, resourceKey.location(), chunkPos3, chunkPos4);
|
|
- }, true);
|
|
- }
|
|
+ ChunkPos chunkPos2 = chunkPos;
|
|
+ if (r == 0) {
|
|
+ throw (forceLoaded ? ERROR_ALL_ADDED : ERROR_NONE_REMOVED).create();
|
|
+ } else {
|
|
+ if (r == 1) {
|
|
+ source.sendSuccess(() -> {
|
|
+ return Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".single", chunkPos2, resourceKey.location());
|
|
+ }, true);
|
|
+ } else {
|
|
+ ChunkPos chunkPos3 = new ChunkPos(m, n);
|
|
+ ChunkPos chunkPos4 = new ChunkPos(o, p);
|
|
+ source.sendSuccess(() -> {
|
|
+ return Component.translatable("commands.forceload." + (forceLoaded ? "added" : "removed") + ".multiple", chunkPos2, resourceKey.location(), chunkPos3, chunkPos4);
|
|
+ }, 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 5cb15e2209d7b315904a1fc6d650ce1e75584271..4d2c88f23ba2280cba95cad41c80105a18139e73 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/GameModeCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/GameModeCommand.java
|
|
@@ -48,15 +48,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 d601d287e94a59ff93b8a83a44dac02544d211df..62a03cf3b5d72765481be54362116279bda0b2f2 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/GiveCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/GiveCommand.java
|
|
@@ -56,6 +56,7 @@ public class GiveCommand {
|
|
|
|
l -= i1;
|
|
ItemStack itemstack1 = item.createItemStack(i1, false);
|
|
+ entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading
|
|
boolean flag = entityplayer.getInventory().add(itemstack1);
|
|
ItemEntity entityitem;
|
|
|
|
@@ -75,6 +76,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 0026da50714adca207b1d3970ee808c9c09d4443..ac694041183e49ab44c5a27f3aa57ad1638298fd 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 1ca63d5504999eb63bece58e697fe42732bcdb7e..02a9fb1c3d9b94cda8f60f044245d44ce90d35dc 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/PlaceCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/PlaceCommand.java
|
|
@@ -88,12 +88,25 @@ 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 worldserver = source.getLevel();
|
|
ConfiguredFeature<?, ?> worldgenfeatureconfigured = (ConfiguredFeature) feature.value();
|
|
ChunkPos chunkcoordintpair = new ChunkPos(pos);
|
|
|
|
PlaceCommand.checkLoaded(worldserver, new ChunkPos(chunkcoordintpair.x - 1, chunkcoordintpair.z - 1), new ChunkPos(chunkcoordintpair.x + 1, chunkcoordintpair.z + 1));
|
|
+ // Folia start - region threading
|
|
+ worldserver.loadChunksAsync(
|
|
+ pos, 16, net.minecraft.world.level.chunk.ChunkStatus.FULL,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL,
|
|
+ (chunks) -> {
|
|
+ try {
|
|
+ // Folia end - region threading
|
|
if (!worldgenfeatureconfigured.place(worldserver, worldserver.getChunkSource().getGenerator(), worldserver.getRandom(), pos)) {
|
|
throw PlaceCommand.ERROR_FEATURE_FAILED.create();
|
|
} else {
|
|
@@ -102,27 +115,57 @@ public class PlaceCommand {
|
|
source.sendSuccess(() -> {
|
|
return Component.translatable("commands.place.feature.success", s, pos.getX(), pos.getY(), pos.getZ());
|
|
}, true);
|
|
- return 1;
|
|
+ return; // Folia - region threading
|
|
}
|
|
+ // Folia start - region threading
|
|
+ } 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 worldserver = source.getLevel();
|
|
|
|
+ // Folia start - region threading
|
|
+ worldserver.loadChunksAsync(
|
|
+ pos, 16, net.minecraft.world.level.chunk.ChunkStatus.FULL,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL,
|
|
+ (chunks) -> {
|
|
+ try {
|
|
+ // Folia end - region threading
|
|
if (!JigsawPlacement.generateJigsaw(worldserver, structurePool, id, maxDepth, pos, false)) {
|
|
throw PlaceCommand.ERROR_JIGSAW_FAILED.create();
|
|
} else {
|
|
source.sendSuccess(() -> {
|
|
return Component.translatable("commands.place.jigsaw.success", pos.getX(), pos.getY(), pos.getZ());
|
|
}, true);
|
|
- return 1;
|
|
+ return; // Folia start - region threading
|
|
}
|
|
+ // Folia start - region threading
|
|
+ } 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 worldserver = source.getLevel();
|
|
Structure structure1 = (Structure) structure.value();
|
|
ChunkGenerator chunkgenerator = worldserver.getChunkSource().getGenerator();
|
|
+ // Folia start - region threading
|
|
+ worldserver.loadChunksAsync(
|
|
+ pos, 16, net.minecraft.world.level.chunk.ChunkStatus.FULL,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL,
|
|
+ (chunks) -> {
|
|
+ try {
|
|
+ // Folia end - region threading
|
|
StructureStart structurestart = structure1.generate(source.registryAccess(), chunkgenerator, chunkgenerator.getBiomeSource(), worldserver.getChunkSource().randomState(), worldserver.getStructureManager(), worldserver.getSeed(), new ChunkPos(pos), 0, worldserver, (holder) -> {
|
|
return true;
|
|
});
|
|
@@ -144,12 +187,27 @@ public class PlaceCommand {
|
|
source.sendSuccess(() -> {
|
|
return Component.translatable("commands.place.structure.success", s, pos.getX(), pos.getY(), pos.getZ());
|
|
}, true);
|
|
- return 1;
|
|
+ return; // Folia - region threading
|
|
}
|
|
+ // Folia start - region threading
|
|
+ } 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 worldserver = source.getLevel();
|
|
+ // Folia start - region threading
|
|
+ worldserver.loadChunksAsync(
|
|
+ pos, 16, net.minecraft.world.level.chunk.ChunkStatus.FULL,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL,
|
|
+ (chunks) -> {
|
|
+ try {
|
|
+ // Folia end - region threading
|
|
StructureTemplateManager structuretemplatemanager = worldserver.getStructureManager();
|
|
|
|
Optional optional;
|
|
@@ -180,9 +238,17 @@ public class PlaceCommand {
|
|
source.sendSuccess(() -> {
|
|
return Component.translatable("commands.place.template.success", id, pos.getX(), pos.getY(), pos.getZ());
|
|
}, true);
|
|
- return 1;
|
|
+ return; // Folia - region threading
|
|
}
|
|
}
|
|
+ // Folia start - region threading
|
|
+ } 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 c5c690b044ff799d9909fce6856b3bf133f6ef04..bf245691e3b264cbec4428f51b2ec8e8ddbb5521 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) {
|
|
@@ -60,7 +65,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 342d7c12a26c6a211aae3db03ec3029c68ef650c..eda5bd9e1e1f1e1d41295874b239f503e4474cb0 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/SetBlockCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/SetBlockCommand.java
|
|
@@ -38,31 +38,47 @@ 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(() -> {
|
|
- return 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(() -> {
|
|
+ return 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 d797637f61bdf8a424f56fbb48e28b7c9117d604..bc371ba154b9e08a5efc213b1bbda63d1bb347cc 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java
|
|
@@ -43,7 +43,11 @@ public class SetSpawnCommand {
|
|
ServerPlayer entityplayer = (ServerPlayer) iterator.next();
|
|
|
|
// Paper start - PlayerSetSpawnEvent
|
|
- if (entityplayer.setRespawnPosition(resourcekey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND)) {
|
|
+ // Folia start - region threading
|
|
+ entityplayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> {
|
|
+ player.setRespawnPosition(resourcekey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND);
|
|
+ }, null, 1L);
|
|
+ if (true) { // Folia end - region threading
|
|
actualTargets.add(entityplayer);
|
|
}
|
|
// 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 a7c89cdf20cb63792c76de81c1ff9f2cbbfcea84..12ebfcfe3aa70635bc5f8c0847977ac08376a074 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/SummonCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/SummonCommand.java
|
|
@@ -64,11 +64,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 3fec07b250a8f145e30c8c41888e47d2a3c902e1..15c4fa89e1f1dbc80055f6f92904d1cc05a24dba 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) {
|
|
@@ -161,6 +161,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 44fcd43a466fb47d31ab05e44bafbef3c4cae63f..2989750a75b3dd244cf8f9ca78769308f982374b 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/TimeCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/TimeCommand.java
|
|
@@ -58,6 +58,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);
|
|
@@ -65,6 +66,7 @@ public class TimeCommand {
|
|
worldserver.setDayTime((long) worldserver.getDayTime() + event.getSkipAmount());
|
|
}
|
|
// CraftBukkit end
|
|
+ }); // Folia - region threading
|
|
}
|
|
|
|
source.sendSuccess(() -> {
|
|
@@ -79,6 +81,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);
|
|
@@ -86,13 +89,16 @@ 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(() -> {
|
|
return 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 8f5714221bc32fb2c9201cbc8a0a35610977f574..c78a7e5e5c6b012756e6a1e50159dd970243cd5e 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/WeatherCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/WeatherCommand.java
|
|
@@ -35,26 +35,32 @@ 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(() -> {
|
|
return 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(() -> {
|
|
return 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(() -> {
|
|
return 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 cf605aa56adf7f80d3b409f60a92a5ca7ae8fd07..05d8cabd2294456e3c8df60265f8b035990dd896 100644
|
|
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
|
|
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
|
|
@@ -442,9 +442,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
|
|
@@ -765,7 +765,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
|
|
Waitable[] waitableArray = new Waitable[1]; // Paper
|
|
rconConsoleSource.prepareForCommand();
|
|
final java.util.concurrent.atomic.AtomicReference<String> command = new java.util.concurrent.atomic.AtomicReference<>(s); // Paper
|
|
- this.executeBlocking(() -> {
|
|
+ Runnable sync = () -> { // Folia - region threading
|
|
CommandSourceStack wrapper = rconConsoleSource.createCommandSourceStack();
|
|
RemoteServerCommandEvent event = new RemoteServerCommandEvent(rconConsoleSource.getBukkitSender(wrapper), s);
|
|
this.server.getPluginManager().callEvent(event);
|
|
@@ -789,7 +789,16 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
|
|
ConsoleInput serverCommand = new ConsoleInput(event.getCommand(), wrapper);
|
|
this.server.dispatchServerCommand(event.getSender(), serverCommand);
|
|
} // Paper
|
|
- });
|
|
+ }; // Folia start - region threading
|
|
+ java.util.concurrent.CompletableFuture
|
|
+ .runAsync(sync, io.papermc.paper.threadedregions.RegionizedServer.getInstance()::addTask)
|
|
+ .whenComplete((Void r, Throwable t) -> {
|
|
+ if (t != null) {
|
|
+ LOGGER.error("Error handling command for rcon: " + s, t);
|
|
+ }
|
|
+ })
|
|
+ .join();
|
|
+ // Folia end - region threading
|
|
// Paper start
|
|
if (waitableArray[0] != null) {
|
|
//noinspection unchecked
|
|
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
index 5afeb59ff25fed2d565407acacffec8383398006..047e817eae19800d146970a3ab44913ea1d17c89 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
@@ -81,7 +81,7 @@ public class ChunkHolder {
|
|
public void onChunkAdd() {
|
|
// 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
|
|
}
|
|
@@ -89,7 +89,7 @@ public class ChunkHolder {
|
|
public void onChunkRemove() {
|
|
// 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
|
|
}
|
|
@@ -284,7 +284,7 @@ public class ChunkHolder {
|
|
|
|
private void addToBroadcastMap() {
|
|
io.papermc.paper.util.TickThread.ensureTickThread(this.chunkMap.level, this.pos, "Asynchronous ChunkHolder update is not allowed");
|
|
- this.chunkMap.needsChangeBroadcasting.add(this);
|
|
+ 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 caa73632aee15583c6b6ed12a668c8f49b794708..f640a0b8742a8362401f91a9a0f8fbb31885dca0 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
|
|
@@ -136,8 +136,8 @@ 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;
|
|
@@ -146,69 +146,33 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
// Paper - rewrite chunk system
|
|
|
|
// Paper start - distance maps
|
|
- private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets<ServerPlayer> pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>();
|
|
- // Paper start - use distance map to optimise tracker
|
|
- public static boolean isLegacyTrackingEntity(Entity entity) {
|
|
- return entity.isLegacyTrackingEntity;
|
|
- }
|
|
-
|
|
- // 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];
|
|
- }
|
|
-
|
|
- private int convertSpigotRangeToVanilla(final int vanilla) {
|
|
- return net.minecraft.server.MinecraftServer.getServer().getScaledTrackingDistance(vanilla);
|
|
- }
|
|
- // Paper end - use distance map to optimise tracker
|
|
+ // Folia - region threading
|
|
|
|
void addPlayerToDistanceMaps(ServerPlayer player) {
|
|
int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX());
|
|
int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ());
|
|
// Note: players need to be explicitly added to distance maps before they can be updated
|
|
- this.nearbyPlayers.addPlayer(player);
|
|
+ // Folia - region threading
|
|
this.level.playerChunkLoader.addPlayer(player); // Paper - replace chunk loader
|
|
- // 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)));
|
|
- }
|
|
- // Paper end - use distance map to optimise entity tracker
|
|
+ // Folia - region threading
|
|
}
|
|
|
|
void removePlayerFromDistanceMaps(ServerPlayer player) {
|
|
int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX());
|
|
int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ());
|
|
// Note: players need to be explicitly added to distance maps before they can be updated
|
|
- this.nearbyPlayers.removePlayer(player);
|
|
+ // Folia - region threading
|
|
this.level.playerChunkLoader.removePlayer(player); // Paper - replace chunk loader
|
|
- // 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);
|
|
- }
|
|
- // Paper end - use distance map to optimise tracker
|
|
- this.playerMobSpawnMap.remove(player); // Paper - optimise chunk tick iteration
|
|
+ // Folia - region threading
|
|
}
|
|
|
|
void updateMaps(ServerPlayer player) {
|
|
int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX());
|
|
int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ());
|
|
// Note: players need to be explicitly added to distance maps before they can be updated
|
|
- this.nearbyPlayers.tickPlayer(player);
|
|
+ // Folia - region threading
|
|
this.level.playerChunkLoader.updatePlayer(player); // Paper - replace chunk loader
|
|
- // 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)));
|
|
- }
|
|
- // Paper end - use distance map to optimise entity tracker
|
|
+ // Folia - region threading
|
|
}
|
|
// Paper end
|
|
// Paper start
|
|
@@ -240,19 +204,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
|
|
return null; // Paper - rewrite chunk system
|
|
}
|
|
- public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers;
|
|
+ //public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers; // Folia - region threading
|
|
// Paper end
|
|
// Paper start - optimise chunk tick iteration
|
|
- public final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<ChunkHolder> needsChangeBroadcasting = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>();
|
|
- public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets);
|
|
+ //public final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<ChunkHolder> needsChangeBroadcasting = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Folia - region threading
|
|
+ //public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets); // Folia - region threading
|
|
// Paper end - optimise chunk tick iteration
|
|
|
|
public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
|
|
super(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();
|
|
@@ -297,57 +261,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
this.setServerViewDistance(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.nearbyPlayers = new io.papermc.paper.util.player.NearbyPlayers(this.level);
|
|
+ //this.regionManagers.add(this.dataRegionManager); // Folia - region threading
|
|
+ //this.nearbyPlayers = new io.papermc.paper.util.player.NearbyPlayers(this.level); // Folia - region threading
|
|
// Paper end
|
|
// 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;
|
|
- case DISPLAY:
|
|
- configuredSpigotValue = spigotWorldConfig.displayTrackingRange;
|
|
- 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
|
|
// always use accessor, so folia can override
|
|
public final io.papermc.paper.util.player.NearbyPlayers getNearbyPlayers() {
|
|
- return this.nearbyPlayers;
|
|
+ return this.level.getCurrentWorldData().getNearbyPlayers(); // Folia - region threading
|
|
}
|
|
// Paper end
|
|
|
|
@@ -675,6 +600,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
|
|
@@ -928,6 +859,38 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
}
|
|
|
|
boolean anyPlayerCloseEnoughForSpawning(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
|
|
int chunkRange = this.level.spigotConfig.mobSpawnRange;
|
|
chunkRange = (chunkRange > this.level.spigotConfig.viewDistance) ? (byte) this.level.spigotConfig.viewDistance : chunkRange;
|
|
chunkRange = (chunkRange > 8) ? 8 : chunkRange;
|
|
@@ -939,7 +902,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
if (!this.distanceManager.hasPlayersNearby(chunkcoordintpair.toLong())) {
|
|
return false;
|
|
} else {
|
|
- Iterator iterator = this.playerMap.getAllPlayers().iterator();
|
|
+ Iterator iterator = null; // Folia - region threading
|
|
|
|
ServerPlayer entityplayer;
|
|
|
|
@@ -971,7 +934,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
return List.of();
|
|
} else {
|
|
Builder<ServerPlayer> builder = ImmutableList.builder();
|
|
- Iterator iterator = this.playerMap.getAllPlayers().iterator();
|
|
+ Iterator iterator = this.level.getLocalPlayers().iterator(); // Folia - region threading
|
|
|
|
while (iterator.hasNext()) {
|
|
ServerPlayer entityplayer = (ServerPlayer) iterator.next();
|
|
@@ -1000,25 +963,19 @@ 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);
|
|
+ // Folia - region threading
|
|
|
|
if (added) {
|
|
- this.playerMap.addPlayer(player, flag1);
|
|
+ // Folia - region threading
|
|
this.updatePlayerPos(player);
|
|
- if (!flag1) {
|
|
- this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player);
|
|
- }
|
|
+ // Folia - region threading
|
|
|
|
// Paper - handled by player chunk loader
|
|
this.addPlayerToDistanceMaps(player); // Paper - distance maps
|
|
} else {
|
|
SectionPos sectionposition = player.getLastSectionPos();
|
|
|
|
- this.playerMap.removePlayer(player);
|
|
- if (!flag2) {
|
|
- this.distanceManager.removePlayer(sectionposition, player);
|
|
- }
|
|
+ // Folia - region threading
|
|
|
|
this.removePlayerFromDistanceMaps(player); // Paper - distance maps
|
|
// Paper - handled by player chunk loader
|
|
@@ -1033,31 +990,13 @@ 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
|
|
+ // Folia - region threading - entity tracker optimisations
|
|
|
|
- SectionPos sectionposition = player.getLastSectionPos();
|
|
- SectionPos sectionposition1 = SectionPos.of((EntityAccess) player);
|
|
- boolean flag = this.playerMap.ignored(player);
|
|
- boolean flag1 = this.skipPlayer(player);
|
|
- boolean flag2 = sectionposition.asLong() != sectionposition1.asLong();
|
|
+ // Folia - region threading
|
|
|
|
- if (flag2 || flag != flag1) {
|
|
+ if (true) { // Folia - region threading
|
|
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);
|
|
- }
|
|
+ // Folia - region threading
|
|
|
|
// Paper - replaced by PlayerChunkLoader
|
|
}
|
|
@@ -1088,9 +1027,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
|
|
@@ -1103,27 +1042,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()); // 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().getLoadedEntities()) {
|
|
+ if (possible.tracker != null) {
|
|
+ possible.tracker.updatePlayer(entityplayer);
|
|
}
|
|
}
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
}
|
|
@@ -1137,16 +1074,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();
|
|
@@ -1154,82 +1091,37 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
entity.tracker = null; // Paper - We're no longer tracked
|
|
}
|
|
|
|
- // 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());
|
|
+ // Folia start - region threading - replace entity tracking ticking
|
|
+ private void foliaEntityTrackerTick() {
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData();
|
|
+ io.papermc.paper.util.player.NearbyPlayers nearbyPlayers = worldData.getNearbyPlayers();
|
|
+ for (Entity entity : worldData.getLoadedEntities()) {
|
|
+ TrackedEntity tracker = entity.tracker;
|
|
+ if (tracker == null) {
|
|
+ continue;
|
|
}
|
|
- } finally {
|
|
- this.level.timings.tracker1.stopTiming();
|
|
- }
|
|
-
|
|
-
|
|
- this.level.timings.tracker2.startTiming();
|
|
- try {
|
|
- for (TrackedEntity tracker : this.entityMap.values()) {
|
|
- tracker.serverEntity.sendChanges();
|
|
+ tracker.tick(nearbyPlayers.getChunk(entity.chunkPosition()));
|
|
+ tracker.serverEntity.sendChanges();
|
|
+ }
|
|
+ // process unloads
|
|
+ for (Entity entity : worldData.takeTrackingUnloads()) {
|
|
+ TrackedEntity tracker = entity.tracker;
|
|
+ if (tracker == null) {
|
|
+ continue;
|
|
}
|
|
- } finally {
|
|
- this.level.timings.tracker2.stopTiming();
|
|
+ tracker.clearPlayers();
|
|
}
|
|
}
|
|
- // Paper end - optimised tracker
|
|
+ // Folia end - region threading - replace entity tracking ticking
|
|
|
|
- protected void tick() {
|
|
- // Paper start - optimized tracker
|
|
- if (true) {
|
|
- this.processTrackQueue();
|
|
- 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 - replace entity tracking ticking
|
|
|
|
+ protected void tick() {
|
|
+ this.foliaEntityTrackerTick(); // Folia - region threading - replace entity tracking ticking
|
|
}
|
|
|
|
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);
|
|
@@ -1238,7 +1130,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);
|
|
@@ -1415,6 +1307,78 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
}
|
|
|
|
}
|
|
+ // Folia start - region threading
|
|
+ private int lastChunkUpdate = -1;
|
|
+ private io.papermc.paper.util.player.NearbyPlayers.TrackedChunk lastTrackedChunk;
|
|
+ public void tick(io.papermc.paper.util.player.NearbyPlayers.TrackedChunk chunk) {
|
|
+ if (chunk == null) {
|
|
+ this.clearPlayers();
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ com.destroystokyo.paper.util.maplist.ReferenceList<ServerPlayer> players =
|
|
+ chunk.getPlayers(io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.VIEW_DISTANCE);
|
|
+
|
|
+ if (players == null) {
|
|
+ this.clearPlayers();
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ int lastChunkUpdate = this.lastChunkUpdate;
|
|
+ int currChunkUpdate = chunk.getUpdateCount();
|
|
+ io.papermc.paper.util.player.NearbyPlayers.TrackedChunk lastTrackedChunk = this.lastTrackedChunk;
|
|
+ this.lastChunkUpdate = currChunkUpdate;
|
|
+ this.lastTrackedChunk = chunk;
|
|
+
|
|
+ for (int i = 0, len = players.size(); i < len; ++i) {
|
|
+ ServerPlayer player = players.getUnchecked(i);
|
|
+ this.updatePlayer(player);
|
|
+ }
|
|
+
|
|
+ if (lastChunkUpdate != currChunkUpdate || lastTrackedChunk != chunk) {
|
|
+ // need to purge any players possible not in the chunk list
|
|
+ for (ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) {
|
|
+ ServerPlayer player = conn.getPlayer();
|
|
+ if (!players.contains(player)) {
|
|
+ this.removePlayer(player);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ 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);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void clearPlayers() {
|
|
+ this.lastChunkUpdate = -1;
|
|
+ this.lastTrackedChunk = null;
|
|
+ if (this.seenBy.isEmpty()) {
|
|
+ return;
|
|
+ }
|
|
+ for (ServerPlayerConnection conn : new java.util.ArrayList<>(this.seenBy)) {
|
|
+ ServerPlayer player = conn.getPlayer();
|
|
+ this.removePlayer(player);
|
|
+ }
|
|
+ }
|
|
+ // Folia end - region threading
|
|
|
|
public void updatePlayer(ServerPlayer player) {
|
|
org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot
|
|
@@ -1434,9 +1398,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
}
|
|
}
|
|
// Paper end - check Y
|
|
+ // Folia start - region threading
|
|
+ if (flag && (this.entity instanceof ServerPlayer thisEntity) && thisEntity.broadcastedDeath) {
|
|
+ flag = false;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
|
|
// CraftBukkit start - respect vanish API
|
|
- if (flag && !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Paper - only consider hits
|
|
+ if (flag && (!io.papermc.paper.util.TickThread.isTickThreadFor(player) || !player.getBukkitEntity().canSee(this.entity.getBukkitEntity()))) { // Paper - only consider hits // Folia - region threading
|
|
flag = false;
|
|
}
|
|
// CraftBukkit end
|
|
diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
|
|
index 55f96545d6db95e3e657502a7910d96fded1113e..b39dd5a11a34407244666d8b9c1e775d6ff90fff 100644
|
|
--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
|
|
+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
|
|
@@ -192,11 +192,11 @@ public abstract class DistanceManager {
|
|
}
|
|
|
|
public int getNaturalSpawnChunkCount() {
|
|
- return this.chunkMap.playerMobSpawnMap.size(); // Paper - optimise chunk tick iteration
|
|
+ return this.chunkMap.level.getCurrentWorldData().mobSpawnMap.size(); // Paper - optimise chunk tick iteration // Folia - region threading
|
|
}
|
|
|
|
public boolean hasPlayersNearby(long chunkPos) {
|
|
- return this.chunkMap.playerMobSpawnMap.getObjectsInRange(chunkPos) != null; // Paper - optimise chunk tick iteration
|
|
+ return this.chunkMap.level.getCurrentWorldData().mobSpawnMap.getObjectsInRange(chunkPos) != null; // Paper - optimise chunk tick iteration // Folia - region threading
|
|
}
|
|
|
|
public String getDebugStatus() {
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
index 8c33a12ca879c46893150d6adfb8aa4d397c6b4c..4ed40924942bc3252fb1a533190765fbfdb2ba72 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
@@ -60,73 +60,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();
|
|
- }
|
|
+ } // Folia - region threading
|
|
|
|
- // rewrite cache if we have to
|
|
- // we do this since we also cache null chunks
|
|
- int cacheKey = getChunkCacheKey(chunk.locX, chunk.locZ);
|
|
-
|
|
- this.lastLoadedChunks[cacheKey] = chunk;
|
|
+ // 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();
|
|
- }
|
|
+ } // Folia - region threading
|
|
|
|
- // rewrite cache if we have to
|
|
- // we do this since we also cache null chunks
|
|
- int cacheKey = getChunkCacheKey(chunk.locX, chunk.locZ);
|
|
-
|
|
- LevelChunk cachedChunk = this.lastLoadedChunks[cacheKey];
|
|
- if (cachedChunk != null && cachedChunk.coordinateKey == chunk.coordinateKey) {
|
|
- this.lastLoadedChunks[cacheKey] = null;
|
|
- }
|
|
+ // 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) {
|
|
@@ -163,8 +132,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) {
|
|
@@ -238,26 +206,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
|
|
|
|
@@ -331,6 +280,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;
|
|
|
|
@@ -508,10 +458,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) {
|
|
@@ -522,7 +473,7 @@ public class ServerChunkCache extends ChunkSource {
|
|
|
|
gameprofilerfiller.push("pollingChunks");
|
|
int k = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING);
|
|
- boolean flag1 = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && worlddata.getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit
|
|
+ boolean flag1 = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getRedstoneGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit // Folia - region threading
|
|
|
|
gameprofilerfiller.push("naturalSpawnCount");
|
|
this.level.timings.countNaturalMobs.startTiming(); // Paper - timings
|
|
@@ -531,7 +482,7 @@ public class ServerChunkCache extends ChunkSource {
|
|
NaturalSpawner.SpawnState spawnercreature_d; // moved down
|
|
if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled
|
|
// re-set mob counts
|
|
- for (ServerPlayer player : this.level.players) {
|
|
+ for (ServerPlayer player : regionizedWorldData.getLocalPlayers()) { // Folia - region threading
|
|
// Paper start - per player mob spawning backoff
|
|
for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) {
|
|
player.mobCounts[ii] = 0;
|
|
@@ -544,14 +495,14 @@ public class ServerChunkCache extends ChunkSource {
|
|
}
|
|
// Paper end - per player mob spawning backoff
|
|
}
|
|
- spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, null, true);
|
|
+ spawnercreature_d = NaturalSpawner.createState(l, regionizedWorldData.getLoadedEntities(), this::getFullChunk, null, true); // Folia - region threading
|
|
} else {
|
|
- spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false);
|
|
+ spawnercreature_d = NaturalSpawner.createState(l, regionizedWorldData.getLoadedEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); // Folia - region threading
|
|
}
|
|
// Paper end
|
|
this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings
|
|
|
|
- this.lastSpawnState = spawnercreature_d;
|
|
+ regionizedWorldData.lastSpawnState = spawnercreature_d; // Folia - region threading
|
|
gameprofilerfiller.popPush("filteringLoadedChunks");
|
|
// Paper - optimise chunk tick iteration
|
|
// Paper - optimise chunk tick iteration
|
|
@@ -560,13 +511,13 @@ public class ServerChunkCache extends ChunkSource {
|
|
// Paper - optimise chunk tick iteration
|
|
|
|
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 start - optimise chunk tick iteration
|
|
ChunkMap playerChunkMap = this.chunkMap;
|
|
- for (ServerPlayer player : this.level.players) {
|
|
+ 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
|
|
player.playerNaturallySpawnedEvent = null;
|
|
player.lastEntitySpawnRadiusSquared = -1.0;
|
|
continue;
|
|
@@ -582,7 +533,7 @@ 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.playerMobSpawnMap.remove(player);
|
|
+ regionizedWorldData.mobSpawnMap.remove(player); // Folia - region threading
|
|
player.playerNaturallySpawnedEvent = null;
|
|
player.lastEntitySpawnRadiusSquared = -1.0;
|
|
continue;
|
|
@@ -592,7 +543,7 @@ public class ServerChunkCache extends ChunkSource {
|
|
int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkCoordinate(player.getX());
|
|
int chunkZ = io.papermc.paper.util.CoordinateUtils.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;
|
|
}
|
|
@@ -603,10 +554,10 @@ public class ServerChunkCache extends ChunkSource {
|
|
io.papermc.paper.util.player.NearbyPlayers nearbyPlayers = this.chunkMap.getNearbyPlayers(); // Paper - optimise chunk tick iteration
|
|
Iterator<LevelChunk> iterator1;
|
|
if (this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
|
|
- iterator1 = this.tickingChunks.iterator();
|
|
+ iterator1 = regionizedWorldData.getTickingChunks().iterator(); // Folia - region threading
|
|
} else {
|
|
- iterator1 = this.tickingChunks.unsafeIterator();
|
|
- List<LevelChunk> shuffled = Lists.newArrayListWithCapacity(this.tickingChunks.size());
|
|
+ iterator1 = regionizedWorldData.getTickingChunks().unsafeIterator(); // Folia - region threading
|
|
+ List<LevelChunk> shuffled = Lists.newArrayListWithCapacity(regionizedWorldData.getTickingChunks().size()); // Folia - region threading
|
|
while (iterator1.hasNext()) {
|
|
shuffled.add(iterator1.next());
|
|
}
|
|
@@ -673,17 +624,21 @@ public class ServerChunkCache extends ChunkSource {
|
|
// Paper - optimise chunk tick iteration
|
|
this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing
|
|
// Paper start - optimise chunk tick iteration
|
|
- if (!this.chunkMap.needsChangeBroadcasting.isEmpty()) {
|
|
- it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<ChunkHolder> copy = this.chunkMap.needsChangeBroadcasting.clone();
|
|
- this.chunkMap.needsChangeBroadcasting.clear();
|
|
- for (ChunkHolder holder : copy) {
|
|
- holder.broadcastChanges(holder.getFullChunkNowUnchecked()); // LevelChunks are NEVER unloaded
|
|
- if (holder.needsBroadcastChanges()) {
|
|
- // I DON'T want to KNOW what DUMB plugins might be doing.
|
|
- this.chunkMap.needsChangeBroadcasting.add(holder);
|
|
+ // 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
|
|
+ holder.broadcastChanges(holder.getFullChunkNowUnchecked());
|
|
+ if (!holder.needsBroadcastChanges()) {
|
|
+ iterator.remove();
|
|
}
|
|
}
|
|
}
|
|
+ // Folia end - region threading
|
|
// Paper end - optimise chunk tick iteration
|
|
this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing
|
|
// Paper - optimise chunk tick iteration
|
|
@@ -747,14 +702,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) {
|
|
@@ -826,7 +786,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() {
|
|
@@ -859,8 +820,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);
|
|
}
|
|
@@ -868,10 +864,15 @@ 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
|
|
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 584a768f2ce1c98a1de7749060c47f21721f9055..10cf0ffb2cf519f0904dbe709b9042d30d5fd827 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
@@ -192,36 +192,35 @@ 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 EndDragonFight dragonFight;
|
|
final Int2ObjectMap<EnderDragonPart> dragonParts;
|
|
private final StructureManager structureManager;
|
|
private final StructureCheck structureCheck;
|
|
- private final boolean tickTime;
|
|
+ public final boolean tickTime; // Folia - region threading
|
|
private final RandomSequences randomSequences;
|
|
- public long lastMidTickExecuteFailure; // Paper - execute chunk tasks mid tick
|
|
+ // 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());
|
|
@@ -257,6 +256,36 @@ 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;
|
|
+ }
|
|
+
|
|
+ for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
|
|
+ for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
|
|
+ if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ public final boolean isAreaLoaded(final BlockPos center, final int radius) {
|
|
+ int minX = (center.getX() - radius) >> 4;
|
|
+ int minZ = (center.getZ() - radius) >> 4;
|
|
+ int maxX = (center.getX() + radius) >> 4;
|
|
+ int maxZ = (center.getZ() + radius) >> 4;
|
|
+
|
|
+ return this.isAreaLoaded(minX, minZ, maxX, maxZ);
|
|
+ }
|
|
+
|
|
+ public final boolean isAreaLoaded(final int minChunkX, final int minChunkZ, final int maxChunkX, final int maxChunkZ) {
|
|
+ // Folia end - region threading
|
|
+ ServerChunkCache chunkProvider = this.getChunkSource();
|
|
+
|
|
for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
|
|
for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
|
|
if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) {
|
|
@@ -565,14 +594,14 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
}
|
|
// Paper end
|
|
// Paper start - lag compensation
|
|
- private long lagCompensationTick = net.minecraft.server.MinecraftServer.SERVER_INIT;
|
|
+ // Folia - region threading
|
|
|
|
public long getLagCompensationTick() {
|
|
- return this.lagCompensationTick;
|
|
+ return this.getCurrentWorldData().getLagCompensationTick(); // Folia - region threading
|
|
}
|
|
|
|
public void updateLagCompensationTick() {
|
|
- this.lagCompensationTick = (System.nanoTime() - net.minecraft.server.MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L));
|
|
+ throw new UnsupportedOperationException(); // Folia - region threading
|
|
}
|
|
// Paper end - lag compensation
|
|
// Paper start - optimise nearby player retrieval
|
|
@@ -619,7 +648,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
ServerPlayer nearest = null;
|
|
double nearestDist = Double.MAX_VALUE;
|
|
|
|
- for (ServerPlayer player : this.players()) {
|
|
+ for (ServerPlayer player : this.getLocalPlayers()) { // Folia - region threading
|
|
double dist = player.distanceToSqr(x, y, z);
|
|
if (dist >= nearestDist) {
|
|
continue;
|
|
@@ -675,7 +704,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
|
|
return nearest;
|
|
} else {
|
|
- return this.getNearestEntity(this.players(), targetPredicate, entity, x, y, z);
|
|
+ return this.getNearestEntity(this.getLocalPlayers(), targetPredicate, entity, x, y, z); // Folia - region threading
|
|
}
|
|
}
|
|
|
|
@@ -684,6 +713,58 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
return this.getNearestPlayer(targetPredicate, null, x, y, z);
|
|
}
|
|
// Paper end - optimise nearby player retrieval
|
|
+ // Folia start - region threading
|
|
+ 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<>(
|
|
+ 6,
|
|
+ (1.0 / 6.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 - region threading
|
|
|
|
// 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, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
|
|
@@ -696,13 +777,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;
|
|
@@ -741,7 +822,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(Raids.factory(this), Raids.getFileId(this.dimensionTypeRegistration()));
|
|
@@ -768,7 +849,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
|
|
|
|
// Paper start
|
|
@Override
|
|
@@ -801,47 +889,30 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
return this.structureManager;
|
|
}
|
|
|
|
- public void tick(BooleanSupplier shouldKeepTicking) {
|
|
+ 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
|
|
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)) {
|
|
- this.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");
|
|
this.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();
|
|
}
|
|
this.timings.scheduledBlocks.stopTiming(); // Paper
|
|
@@ -858,7 +929,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
this.timings.doSounds.startTiming(); // Spigot
|
|
this.runBlockEvents();
|
|
this.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
|
|
|
|
@@ -870,20 +941,30 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
gameprofilerfiller.push("entities");
|
|
this.timings.tickEntities.startTiming(); // Spigot
|
|
if (this.dragonFight != null) {
|
|
+ if (io.papermc.paper.util.TickThread.isTickThreadFor(this, this.dragonFight.origin)) { // 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
|
|
this.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();
|
|
@@ -914,6 +995,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)) {
|
|
+ // 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)) {
|
|
+ this.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
|
|
@@ -924,11 +1030,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);
|
|
}
|
|
|
|
@@ -957,15 +1064,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();
|
|
@@ -973,7 +1088,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
|
|
@@ -1027,7 +1142,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;
|
|
}
|
|
@@ -1041,7 +1156,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)
|
|
}
|
|
@@ -1142,7 +1257,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
}
|
|
|
|
public boolean isHandlingTick() {
|
|
- return this.handlingTick;
|
|
+ return this.getCurrentWorldData().isHandlingTick(); // Folia - regionised ticking
|
|
}
|
|
|
|
public boolean canSleepThroughNights() {
|
|
@@ -1174,6 +1289,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();
|
|
}
|
|
@@ -1185,7 +1308,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()) {
|
|
@@ -1271,23 +1394,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
|
|
@@ -1351,7 +1475,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);
|
|
@@ -1384,7 +1508,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
|
|
@@ -1407,7 +1540,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
|
|
@@ -1424,7 +1557,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();
|
|
@@ -1512,7 +1654,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;
|
|
|
|
@@ -1520,12 +1670,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();
|
|
}
|
|
@@ -1580,6 +1725,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);
|
|
@@ -1681,8 +1839,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
|
|
@@ -1826,7 +1984,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"));
|
|
@@ -1839,7 +1997,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
|
|
@@ -1862,7 +2020,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
}
|
|
|
|
try {
|
|
- this.isUpdatingNavigations = true;
|
|
+ //this.isUpdatingNavigations = true; // Folia - region threading
|
|
iterator = list.iterator();
|
|
|
|
while (iterator.hasNext()) {
|
|
@@ -1871,7 +2029,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
navigationabstract1.recomputePath();
|
|
}
|
|
} finally {
|
|
- this.isUpdatingNavigations = false;
|
|
+ //this.isUpdatingNavigations = false; // Folia - region threading
|
|
}
|
|
|
|
}
|
|
@@ -1880,23 +2038,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
|
|
@@ -1927,7 +2085,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();
|
|
@@ -1942,25 +2100,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) {
|
|
@@ -1971,12 +2132,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
|
|
@@ -2000,7 +2161,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, @Nullable ServerPlayer sender, T t0, double d0, double d1, double d2, int i, double d3, double d4, double d5, double d6, boolean force) {
|
|
// Paper end
|
|
@@ -2053,7 +2214,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
|
|
@@ -2220,6 +2388,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
}
|
|
|
|
public boolean setChunkForced(int x, int z, boolean forced) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify force loaded chunks off of the global region"); // Folia - region threading
|
|
ForcedChunksSavedData forcedchunk = (ForcedChunksSavedData) this.getDataStorage().computeIfAbsent(ForcedChunksSavedData.factory(), "chunks");
|
|
ChunkPos chunkcoordintpair = new ChunkPos(x, z);
|
|
long k = chunkcoordintpair.toLong();
|
|
@@ -2228,7 +2397,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);
|
|
@@ -2256,13 +2425,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);
|
|
@@ -2270,7 +2444,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
|
|
});
|
|
}
|
|
}
|
|
@@ -2317,7 +2496,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) {
|
|
@@ -2331,7 +2510,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");
|
|
@@ -2477,7 +2656,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();
|
|
@@ -2490,7 +2669,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());
|
|
});
|
|
}
|
|
@@ -2499,7 +2678,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
public void blockUpdated(BlockPos pos, Block block) {
|
|
if (!this.isDebug()) {
|
|
// CraftBukkit start
|
|
- if (this.populating) {
|
|
+ if (this.getCurrentWorldData().populating) { // Folia - region threading
|
|
return;
|
|
}
|
|
// CraftBukkit end
|
|
@@ -2542,9 +2721,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) {
|
|
@@ -2577,6 +2754,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();
|
|
@@ -2589,11 +2772,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
|
|
@@ -2614,13 +2793,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
|
|
@@ -2642,7 +2822,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
|
|
@@ -2698,16 +2878,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 && !paperConfig().entities.markers.tick) return; // Paper - Configurable marker ticking
|
|
- 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;
|
|
@@ -2718,6 +2898,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
|
|
public void onTrackingStart(Entity entity) {
|
|
org.spigotmc.AsyncCatcher.catchOp("entity register"); // Spigot
|
|
+ ServerLevel.this.getCurrentWorldData().addLoadedEntity(entity); // Folia - region threading
|
|
// ServerLevel.this.getChunkSource().addEntity(entity); // Paper - moved down below valid=true
|
|
if (entity instanceof ServerPlayer) {
|
|
ServerPlayer entityplayer = (ServerPlayer) entity;
|
|
@@ -2735,7 +2916,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) {
|
|
@@ -2746,7 +2927,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
|
|
}
|
|
}
|
|
|
|
@@ -2767,16 +2950,24 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
|
|
public void onTrackingEnd(Entity entity) {
|
|
org.spigotmc.AsyncCatcher.catchOp("entity unregister"); // Spigot
|
|
+ ServerLevel.this.getCurrentWorldData().removeLoadedEntity(entity);
|
|
// Spigot start
|
|
if ( entity instanceof Player )
|
|
{
|
|
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(); )
|
|
{
|
|
@@ -2786,6 +2977,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
iter.remove();
|
|
}
|
|
}
|
|
+ } // Folia - make map data thread-safe
|
|
}
|
|
}
|
|
} );
|
|
@@ -2820,7 +3012,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) {
|
|
@@ -2831,13 +3023,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 f71a4a8307fb092d33545e12d253e0b80c884168..89f7825a8cf415f3c2e0ddcb41c159a84d2e4bd1 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
|
|
@@ -185,7 +185,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 - changed to nanoTime
|
|
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;
|
|
@@ -463,51 +463,151 @@ public class ServerPlayer extends Player {
|
|
}
|
|
// CraftBukkit end
|
|
|
|
- public void fudgeSpawnLocation(ServerLevel world) {
|
|
- BlockPos blockposition = world.getSharedSpawnPos();
|
|
+ // Folia start - region threading
|
|
+ private static final int SPAWN_RADIUS_SELECTION_SEARCH = 5;
|
|
|
|
- if (world.dimensionType().hasSkyLight() && world.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit
|
|
- int i = Math.max(0, this.server.getSpawnRadius(world));
|
|
- int j = Mth.floor(world.getWorldBorder().getDistanceToBorder((double) blockposition.getX(), (double) blockposition.getZ()));
|
|
+ private static BlockPos getRandomSpawn(ServerLevel world, RandomSource random) {
|
|
+ BlockPos spawn = world.getSharedSpawnPos();
|
|
+ double radius = (double)Math.max(0, world.getGameRules().getInt(GameRules.RULE_SPAWN_RADIUS));
|
|
|
|
- if (j < i) {
|
|
- i = j;
|
|
- }
|
|
+ double spawnX = (double)spawn.getX() + 0.5;
|
|
+ double spawnZ = (double)spawn.getZ() + 0.5;
|
|
|
|
- if (j <= 1) {
|
|
- i = 1;
|
|
+ WorldBorder worldBorder = world.getWorldBorder();
|
|
+
|
|
+ double selectMinX = Math.max(worldBorder.getMinX() + 1.0, spawnX - radius);
|
|
+ double selectMinZ = Math.max(worldBorder.getMinZ() + 1.0, spawnZ - radius);
|
|
+ double selectMaxX = Math.min(worldBorder.getMaxX() - 1.0, spawnX + radius);
|
|
+ double selectMaxZ = Math.min(worldBorder.getMaxZ() - 1.0, spawnZ + radius);
|
|
+
|
|
+ double amountX = selectMaxX - selectMinX;
|
|
+ double amountZ = selectMaxZ - selectMinZ;
|
|
+
|
|
+ int selectX = amountX < 1.0 ? Mth.floor(worldBorder.getCenterX()) : (int)Mth.floor((amountX + 1.0) * random.nextDouble() + selectMinX);
|
|
+ int selectZ = amountZ < 1.0 ? Mth.floor(worldBorder.getCenterZ()) : (int)Mth.floor((amountZ + 1.0) * random.nextDouble() + selectMinZ);
|
|
+
|
|
+ return new BlockPos(selectX, 0, selectZ);
|
|
+ }
|
|
+
|
|
+ private static void completeSpawn(ServerLevel world, BlockPos selected,
|
|
+ ca.spottedleaf.concurrentutil.completable.Completable<Location> toComplete) {
|
|
+ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(selected), world.levelData.getSpawnAngle(), 0.0f));
|
|
+ }
|
|
+
|
|
+ private static BlockPos findSpawnAround(ServerLevel world, ServerPlayer player, BlockPos selected) {
|
|
+ // try hard to find, so that we don't attempt another chunk load
|
|
+ for (int dz = -SPAWN_RADIUS_SELECTION_SEARCH; dz <= SPAWN_RADIUS_SELECTION_SEARCH; ++dz) {
|
|
+ for (int dx = -SPAWN_RADIUS_SELECTION_SEARCH; dx <= SPAWN_RADIUS_SELECTION_SEARCH; ++dx) {
|
|
+ BlockPos inChunk = PlayerRespawnLogic.getOverworldRespawnPos(world, selected.getX(), selected.getZ());
|
|
+ if (inChunk == null) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ AABB checkVolume = player.getBoundingBoxAt((double)inChunk.getX() + 0.5, (double)inChunk.getY(), (double)inChunk.getZ() + 0.5);
|
|
+
|
|
+ if (!world.noCollision(player, checkVolume, true)) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ return inChunk;
|
|
}
|
|
+ }
|
|
|
|
- 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 k1 = RandomSource.create().nextInt(i1);
|
|
+ return null;
|
|
+ }
|
|
|
|
- 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);
|
|
+ // rets false when another attempt is required
|
|
+ private static boolean trySpawnOrSchedule(ServerLevel world, ServerPlayer player, RandomSource random, int[] attemptCount, int maxAttempts,
|
|
+ ca.spottedleaf.concurrentutil.completable.Completable<Location> toComplete) {
|
|
+ ++attemptCount[0];
|
|
|
|
- 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;
|
|
+ BlockPos rough = getRandomSpawn(world, random);
|
|
+
|
|
+ int minX = (rough.getX() - SPAWN_RADIUS_SELECTION_SEARCH) >> 4;
|
|
+ int minZ = (rough.getZ() - SPAWN_RADIUS_SELECTION_SEARCH) >> 4;
|
|
+ int maxX = (rough.getX() + SPAWN_RADIUS_SELECTION_SEARCH) >> 4;
|
|
+ int maxZ = (rough.getZ() + SPAWN_RADIUS_SELECTION_SEARCH) >> 4;
|
|
+
|
|
+ // we could short circuit this check, but it would possibly recurse. Then, it could end up causing a stack overflow
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(world, minX, minZ, maxX, maxZ) || !world.isAreaLoaded(minX, minZ, maxX, maxZ)) {
|
|
+ world.loadChunksAsync(minX, maxX, minZ, maxZ,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER,
|
|
+ (unused) -> {
|
|
+ BlockPos selected = findSpawnAround(world, player, rough);
|
|
+ if (selected == null) {
|
|
+ // run more spawn attempts
|
|
+ selectSpawn(world, player, random, attemptCount, maxAttempts, toComplete);
|
|
+ return;
|
|
}
|
|
+
|
|
+ completeSpawn(world, selected, toComplete);
|
|
+ return;
|
|
}
|
|
- }
|
|
- } else {
|
|
- this.moveTo(blockposition, 0.0F, 0.0F);
|
|
+ );
|
|
+ return true;
|
|
+ }
|
|
|
|
- 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());
|
|
+ BlockPos selected = findSpawnAround(world, player, rough);
|
|
+ if (selected == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ completeSpawn(world, selected, toComplete);
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ private static void selectSpawn(ServerLevel world, ServerPlayer player, RandomSource random, int[] attemptCount, int maxAttempts,
|
|
+ ca.spottedleaf.concurrentutil.completable.Completable<Location> toComplete) {
|
|
+ do {
|
|
+ if (attemptCount[0] >= maxAttempts) {
|
|
+ BlockPos sharedSpawn = world.getSharedSpawnPos();
|
|
+ BlockPos selected = world.getWorldBorder().clampToBounds((double)sharedSpawn.getX(), (double)sharedSpawn.getY(), (double)sharedSpawn.getZ());
|
|
+
|
|
+ LOGGER.warn("Found no spawn in radius for player '" + player.getName() + "', ignoring radius");
|
|
+
|
|
+ // this call requires to return a location with loaded chunks, so we need to schedule a load here
|
|
+ io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(
|
|
+ world, selected.getX() >> 4, selected.getZ() >> 4, net.minecraft.world.level.chunk.ChunkStatus.FULL,
|
|
+ true, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER,
|
|
+ (unused) -> {
|
|
+ completeSpawn(world, selected, toComplete);
|
|
+ }
|
|
+ );
|
|
+ return;
|
|
}
|
|
+ } while (!trySpawnOrSchedule(world, player, random, attemptCount, maxAttempts, toComplete));
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
+ public static void fudgeSpawnLocation(ServerLevel world, ServerPlayer player, ca.spottedleaf.concurrentutil.completable.Completable<Location> toComplete) { // Folia - region threading
|
|
+ BlockPos blockposition = world.getSharedSpawnPos();
|
|
+
|
|
+ if (world.dimensionType().hasSkyLight() && world.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit
|
|
+ // Folia start - region threading
|
|
+ selectSpawn(world, player, player.random, new int[1], 500, toComplete);
|
|
+ // Folia end - region threading
|
|
+ } else {
|
|
+ // 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;
|
|
}
|
|
|
|
@@ -1166,6 +1266,337 @@ 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.level().noCollision(this, this.getBoundingBox(), true) && this.getY() < (double)this.level().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.serverLevel();
|
|
+ ServerLevel respawnWorld = this.server.getLevel(this.getRespawnDimension());
|
|
+
|
|
+ // modified based off PlayerList#respawn
|
|
+
|
|
+ EntityTreeNode passengerTree = this.makePassengerTree();
|
|
+
|
|
+ this.isChangingDimension = true;
|
|
+ origin.removePlayerImmediately(this, RemovalReason.CHANGED_DIMENSION);
|
|
+ // reset player if needed, only after removal from world
|
|
+ if (!alive) {
|
|
+ ServerPlayer.this.reset();
|
|
+ }
|
|
+ // must be manually removed from connections, delay until after reset() so that we do not trip any thread checks
|
|
+ this.serverLevel().getCurrentWorldData().connections.remove(this.connection.connection);
|
|
+
|
|
+ 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) -> {
|
|
+ // 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.serverLevel().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();
|
|
+ this.resetStoredPositions();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected ServerPlayer transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) {
|
|
+ // must be manually removed from connections
|
|
+ this.serverLevel().getCurrentWorldData().connections.remove(this.connection.connection);
|
|
+ this.serverLevel().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.serverLevel().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(
|
|
+ this.createCommonSpawnInfo(destination),
|
|
+ (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L ? (byte)1 : (byte)0
|
|
+ )
|
|
+ );
|
|
+ // 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.serverLevel().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.level().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) {
|
|
@@ -1175,6 +1606,11 @@ public class ServerPlayer extends Player {
|
|
|
|
@Nullable
|
|
public Entity changeDimension(ServerLevel worldserver, PlayerTeleportEvent.TeleportCause cause) {
|
|
+ // Folia start - region threading
|
|
+ if (true) {
|
|
+ throw new UnsupportedOperationException("Must use teleportAsync while in region threading");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
// CraftBukkit end
|
|
if (this.isSleeping()) return this; // CraftBukkit - SPIGOT-3154
|
|
// this.isChangingDimension = true; // CraftBukkit - Moved down and into PlayerList#changeDimension
|
|
@@ -2132,6 +2568,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
|
|
@@ -2633,7 +3075,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 106a312aba249d1e83e4b535fc6e741e04ccfd14..0806559b4f4cebc88fb1f724a6916dac0c0c95e4 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
|
|
@@ -128,7 +128,7 @@ public class ServerPlayerGameMode {
|
|
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 {
|
|
@@ -141,7 +141,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;
|
|
@@ -417,7 +417,7 @@ public class ServerPlayerGameMode {
|
|
} else {
|
|
// CraftBukkit start
|
|
org.bukkit.block.BlockState state = bblock.getState();
|
|
- this.level.captureDrops = new ArrayList<>();
|
|
+ this.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);
|
|
@@ -445,8 +445,8 @@ public class ServerPlayerGameMode {
|
|
// return true; // CraftBukkit
|
|
}
|
|
// CraftBukkit start
|
|
- java.util.List<net.minecraft.world.entity.item.ItemEntity> itemsToDrop = this.level.captureDrops; // Paper - store current list
|
|
- this.level.captureDrops = null; // Paper - Remove this earlier so that we can actually drop stuff
|
|
+ java.util.List<net.minecraft.world.entity.item.ItemEntity> itemsToDrop = this.level.getCurrentWorldData().captureDrops; // Paper - store current list // Folia - region threading
|
|
+ this.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 f382d138959b34bfc3a114bc9d96e056cccbfc89..100293099156978ff701bc6c9d8df94ba8282021 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
|
|
@@ -97,10 +97,15 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
this.chunkMap.level.chunkTaskScheduler.radiusAwareScheduler.queueInfiniteRadiusTask(() -> { // 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), 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();
|
|
@@ -108,7 +113,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<io.papermc.paper.chunk.system.light.LightQueue.ChunkTasks> runnable) { // Paper - rewrite chunk system
|
|
@@ -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;
|
|
}
|
|
|
|
@@ -151,22 +161,28 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
}
|
|
updateFuture.isTicketAdded = true;
|
|
|
|
- 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.onComplete.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.onComplete.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 658e63ebde81dc14c8ab5850fb246dc0aab25dea..7e1f15ac8d2f7c86d4aba1be5df717052ffb31f0 100644
|
|
--- a/src/main/java/net/minecraft/server/level/TicketType.java
|
|
+++ b/src/main/java/net/minecraft/server/level/TicketType.java
|
|
@@ -37,6 +37,14 @@ public class TicketType<T> {
|
|
public static final TicketType<Long> NON_FULL_SYNC_LOAD = create("non_full_sync_load", Long::compareTo);
|
|
public static final TicketType<ChunkPos> DELAY_UNLOAD = create("delay_unload", Comparator.comparingLong(ChunkPos::toLong), 1);
|
|
// 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> 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 50ed7cfe1ecef6d075ba484804827cec83ba2bf2..41c03421edd88dba669354595a2ace1277d93af6 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/ServerCommonPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
|
|
index 598f807f0d0caac98b81e0e2991f1bd497c4534e..2bb944cef9bc8c5e56023ef20921ef13509d4823 100644
|
|
--- a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
|
|
+++ b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
|
|
@@ -85,6 +85,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
|
|
ServerCommonPacketListenerImpl.LOGGER.info("Stopping singleplayer server as player logged out");
|
|
this.server.halt(false);
|
|
}
|
|
+ this.player.getBukkitEntity().taskScheduler.retire(); // Folia - region threading
|
|
|
|
}
|
|
|
|
@@ -98,9 +99,9 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
|
|
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(ServerCommonPacketListenerImpl.TIMEOUT_DISCONNECTION_MESSAGE, 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 endg
|
|
}
|
|
|
|
@@ -279,24 +280,8 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
|
|
if (this.processedDisconnect) {
|
|
return;
|
|
}
|
|
- if (!this.cserver.isPrimaryThread()) {
|
|
- Waitable waitable = new Waitable() {
|
|
- @Override
|
|
- protected Object evaluate() {
|
|
- ServerCommonPacketListenerImpl.this.disconnect(reason, cause); // Paper - adventure
|
|
- 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(io.papermc.paper.adventure.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;
|
|
}
|
|
|
|
@@ -327,7 +312,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
|
|
|
|
Objects.requireNonNull(this.connection);
|
|
// CraftBukkit - Don't wait
|
|
- minecraftserver.scheduleOnMain(networkmanager::handleDisconnection); // Paper
|
|
+ // Folia - region threading
|
|
}
|
|
|
|
protected boolean isSingleplayerOwner() {
|
|
diff --git a/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
|
|
index 8dbcc1b3a70b6bbea3bd2d15b6d66cc4f9cd53f8..d57130a8e1d28ce2458d11197911d12b9ca801ea 100644
|
|
--- a/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
|
|
+++ b/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
|
|
@@ -43,6 +43,7 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis
|
|
@Nullable
|
|
private ConfigurationTask currentTask;
|
|
private ClientInformation clientInformation;
|
|
+ public boolean switchToMain = false; // Folia - region threading - rewrite login process
|
|
|
|
public ServerConfigurationPacketListenerImpl(MinecraftServer minecraftserver, Connection networkmanager, CommonListenerCookie commonlistenercookie, ServerPlayer player) { // CraftBukkit
|
|
super(minecraftserver, networkmanager, commonlistenercookie, player); // CraftBukkit
|
|
@@ -127,7 +128,57 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis
|
|
|
|
ServerPlayer entityplayer = playerlist.getPlayerForLogin(this.gameProfile, this.clientInformation, this.player); // CraftBukkit
|
|
|
|
- playerlist.placeNewPlayer(this.connection, entityplayer, this.createCookie(this.clientInformation));
|
|
+ // Folia start - region threading - rewrite login process
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot handle player login off global tick thread");
|
|
+ CommonListenerCookie clientData = this.createCookie(this.clientInformation);
|
|
+ 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,
|
|
+ () -> {
|
|
+ // once switchToMain is set, the current ticking region now owns the connection and is responsible
|
|
+ // for cleaning it up
|
|
+ playerlist.placeNewPlayer(
|
|
+ ServerConfigurationPacketListenerImpl.this.connection,
|
|
+ entityplayer,
|
|
+ clientData,
|
|
+ data.getValue(),
|
|
+ lastKnownName.getValue(),
|
|
+ loc
|
|
+ );
|
|
+ },
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER
|
|
+ );
|
|
+ });
|
|
+ this.switchToMain = true;
|
|
+ try {
|
|
+ // now the connection responsibility is transferred on the region
|
|
+ playerlist.loadSpawnForNewPlayer(this.connection, entityplayer, clientData, data, lastKnownName, toComplete);
|
|
+ } catch (final Throwable throwable) {
|
|
+ // assume toComplete will not be invoked
|
|
+ // ensure global tick thread owns the connection again, to properly disconnect it
|
|
+ this.switchToMain = false;
|
|
+ }
|
|
+ // Folia end - region threading - rewrite login process
|
|
this.connection.resumeInboundAfterProtocolChange();
|
|
} catch (Exception exception) {
|
|
ServerConfigurationPacketListenerImpl.LOGGER.error("Couldn't place player in world", exception);
|
|
diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
|
|
index 79326308f6126f84a3cbb3d5a33302de048d8a50..81090d1b5d67506268a41c6387a1d45302e88a5c 100644
|
|
--- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
|
|
+++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java
|
|
@@ -158,10 +158,13 @@ public class ServerConnectionListener {
|
|
});
|
|
}
|
|
// Paper end
|
|
- pending.add(object); // Paper
|
|
+ // Folia - connection fixes - move down
|
|
((Connection) object).configurePacketHandler(channelpipeline);
|
|
((Connection) object).setListenerForServerboundHandshake(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(object);
|
|
+ // Folia end - regionised threading
|
|
}
|
|
}).group(eventloopgroup).localAddress(address)).option(ChannelOption.AUTO_READ, false).bind().syncUninterruptibly()); // CraftBukkit // Paper
|
|
}
|
|
@@ -224,7 +227,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 65bb221993147a558995b36fb835f7b82e0eb4bd..159d3a27c1686fd2b0025cab5b7e7775679c4ce9 100644
|
|
--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
|
|
+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
|
|
@@ -289,7 +289,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
private final LastSeenMessagesValidator lastSeenMessages;
|
|
private final MessageSignatureCache messageSignatureCache;
|
|
private final FutureChain chatMessageChain;
|
|
- private boolean waitingForSwitchToConfig;
|
|
+ public volatile boolean waitingForSwitchToConfig; // Folia - rewrite login process - fix bad ordering of this field write + public
|
|
private static final int MAX_SIGN_LINE_LENGTH = Integer.getInteger("Paper.maxSignLength", 80); // Paper
|
|
|
|
public ServerGamePacketListenerImpl(MinecraftServer server, Connection connection, ServerPlayer player, CommonListenerCookie clientData) {
|
|
@@ -307,10 +307,10 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
}
|
|
|
|
// CraftBukkit start - add fields
|
|
- 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
|
|
@@ -323,8 +323,21 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
private boolean hasMoved; // Spigot
|
|
// 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());
|
|
+ // Folia end - region threading
|
|
+
|
|
@Override
|
|
public void tick() {
|
|
+ // Folia start - region threading
|
|
+ this.keepConnectionAlive();
|
|
+ if (this.player.wonGame) {
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
if (this.ackBlockChangesUpTo > -1) {
|
|
this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo));
|
|
this.ackBlockChangesUpTo = -1;
|
|
@@ -373,7 +386,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
this.aboveGroundVehicleTickCount = 0;
|
|
}
|
|
|
|
- this.keepConnectionAlive();
|
|
+ // Folia - region threading - moved to beginning of method
|
|
// CraftBukkit start
|
|
for (int spam; (spam = this.chatSpamTickCount.get()) > 0 && !this.chatSpamTickCount.compareAndSet(spam, spam - 1); ) ;
|
|
if (tabSpamLimiter.get() > 0) tabSpamLimiter.getAndDecrement(); // Paper - split to seperate variable
|
|
@@ -410,6 +423,19 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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
|
|
@@ -506,9 +532,10 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
// 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;
|
|
@@ -583,7 +610,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
Location curPos = this.getCraftPlayer().getLocation(); // Spigot
|
|
|
|
entity.absMoveTo(d3, d4, d5, f, f1);
|
|
- this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
|
|
+ //this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit // Folia - move to repositionAllPassengers
|
|
|
|
// Paper start - optimise out extra getCubes
|
|
boolean teleportBack = flag2; // violating this is always a fail
|
|
@@ -596,11 +623,19 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
}
|
|
if (teleportBack) { // Paper end - optimise out extra getCubes
|
|
entity.absMoveTo(d0, d1, d2, f, f1);
|
|
- this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
|
|
+ //this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit // Folia - not needed, the player is no longer updated
|
|
this.send(new ClientboundMoveVehiclePacket(entity));
|
|
return;
|
|
}
|
|
|
|
+ // Folia start - move to positionRider
|
|
+ // this correction is required on folia since we move the connection tick to the beginning of the server
|
|
+ // tick, which would make any desync here visible
|
|
+ // this will correctly update the passenger positions for all mounted entities
|
|
+ // this prevents desync and ensures that all passengers have the correct rider-adjusted position
|
|
+ entity.repositionAllPassengers(false);
|
|
+ // Folia end - move to positionRider
|
|
+
|
|
// CraftBukkit start - fire PlayerMoveEvent
|
|
Player player = this.getCraftPlayer();
|
|
// Spigot Start
|
|
@@ -646,7 +681,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
|
|
// If the event is cancelled we move the player back to their old location.
|
|
if (event.isCancelled()) {
|
|
- this.teleport(from);
|
|
+ this.player.getBukkitEntity().teleportAsync(from, PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading
|
|
return;
|
|
}
|
|
|
|
@@ -654,7 +689,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
// there to avoid any 'Moved wrongly' or 'Moved too quickly' errors.
|
|
// We only do this if the Event was not cancelled.
|
|
if (!oldTo.equals(event.getTo()) && !event.isCancelled()) {
|
|
- this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN);
|
|
+ this.player.getBukkitEntity().teleportAsync(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading
|
|
return;
|
|
}
|
|
|
|
@@ -771,13 +806,13 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
// PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); // 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]))); // Paper
|
|
+ this.disconnect(Component.translatable("disconnect.spam", new Object[0])); // Paper // Folia - region threading
|
|
return;
|
|
}
|
|
// Paper end
|
|
@@ -802,7 +837,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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) -> {
|
|
@@ -818,7 +853,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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());
|
|
@@ -1130,7 +1165,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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;
|
|
@@ -1153,17 +1188,17 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
|
|
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();
|
|
|
|
@@ -1183,7 +1218,19 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
this.updateBookContents(list1, i);
|
|
};
|
|
|
|
- this.filterTextPacket((List) list).thenAcceptAsync(consumer, this.server);
|
|
+ this.filterTextPacket(list).thenAcceptAsync(consumer, // Folia start - region threading
|
|
+ (Runnable run) -> {
|
|
+ this.player.getBukkitEntity().taskScheduler.schedule(
|
|
+ (player) -> {
|
|
+ run.run();
|
|
+ },
|
|
+ null, 1L);
|
|
+ }).whenComplete((Object res, Throwable thr) -> {
|
|
+ if (thr != null) {
|
|
+ LOGGER.error("Failed to handle book update packet", thr);
|
|
+ }
|
|
+ });
|
|
+ // Folia end - region threading
|
|
}
|
|
}
|
|
|
|
@@ -1349,9 +1396,10 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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);
|
|
@@ -1537,7 +1585,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
|
|
// If the event is cancelled we move the player back to their old location.
|
|
if (event.isCancelled()) {
|
|
- this.teleport(from);
|
|
+ this.player.getBukkitEntity().teleportAsync(from, PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading
|
|
return;
|
|
}
|
|
|
|
@@ -1545,7 +1593,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
// there to avoid any 'Moved wrongly' or 'Moved too quickly' errors.
|
|
// We only do this if the Event was not cancelled.
|
|
if (!oldTo.equals(event.getTo()) && !event.isCancelled()) {
|
|
- this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN);
|
|
+ this.player.getBukkitEntity().teleportAsync(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading
|
|
return;
|
|
}
|
|
|
|
@@ -1778,9 +1826,9 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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++;
|
|
@@ -1808,7 +1856,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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.serverLevel(), 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;
|
|
}
|
|
@@ -1892,7 +1940,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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.serverLevel(), 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;
|
|
|
|
@@ -2006,7 +2054,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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;
|
|
}
|
|
}
|
|
@@ -2043,7 +2091,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
}
|
|
// CraftBukkit end
|
|
ServerGamePacketListenerImpl.LOGGER.info("{} lost connection: {}", this.player.getName().getString(), reason.getString());
|
|
- this.removePlayerFromWorld(quitMessage); // Paper
|
|
+ if (!this.waitingForSwitchToConfig) this.removePlayerFromWorld(quitMessage); // Paper // Folia - region threading
|
|
super.onDisconnect(reason, quitMessage); // Paper
|
|
}
|
|
|
|
@@ -2052,6 +2100,8 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
this.removePlayerFromWorld(null);
|
|
}
|
|
|
|
+ public boolean hackSwitchingConfig; // Folia - rewrite login process
|
|
+
|
|
private void removePlayerFromWorld(@Nullable net.kyori.adventure.text.Component quitMessage) {
|
|
// Paper end
|
|
this.chatMessageChain.close();
|
|
@@ -2064,6 +2114,8 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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
|
|
+ if (!this.hackSwitchingConfig) this.disconnectPos = this.player.chunkPosition(); // Folia - region threading - note: only set after removing, since it can tick the player
|
|
+ if (!this.hackSwitchingConfig) this.player.serverLevel().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
|
|
@@ -2116,9 +2168,9 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
}
|
|
// 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());
|
|
|
|
@@ -2151,23 +2203,22 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
@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
|
|
// CraftBukkit start - SPIGOT-7346: Prevent disconnected players from executing commands
|
|
if (this.player.hasDisconnected()) {
|
|
return;
|
|
}
|
|
// CraftBukkit end
|
|
-
|
|
this.performChatCommand(packet, (LastSeenMessages) optional.get());
|
|
this.detectRateSpam("/" + packet.command()); // Spigot
|
|
- });
|
|
+ }, null, 1L); // Folia - region threading
|
|
}
|
|
|
|
}
|
|
@@ -2241,9 +2292,9 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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 causes
|
|
- }); // Paper - push to main
|
|
+ // Folia - region threading
|
|
return Optional.empty();
|
|
} else {
|
|
Optional<LastSeenMessages> optional = this.unpackAndApplyLastSeen(acknowledgment);
|
|
@@ -2318,7 +2369,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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());
|
|
@@ -2429,6 +2480,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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
|
|
@@ -2665,8 +2717,25 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
// Spigot End
|
|
|
|
public void switchToConfig() {
|
|
- this.waitingForSwitchToConfig = true;
|
|
+ // Folia start - rewrite login process
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this.player, "Cannot switch config off-main");
|
|
+ if (io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) {
|
|
+ throw new IllegalStateException("Cannot switch config while on global tick thread");
|
|
+ }
|
|
+ // Folia end - rewrite login process
|
|
+ // Folia start - rewrite login process - fix bad ordering of this field write - move after removed from world
|
|
+ // the field write ordering is bad as it allows the client to send the response packet before the player is
|
|
+ // removed from the world
|
|
+ // Folia end - rewrite login process - fix bad ordering of this field write - move after removed from world
|
|
+ try { // Folia - rewrite login process - move connection ownership to global region
|
|
+ this.hackSwitchingConfig = true; // Folia - rewrite login process - avoid adding logout ticket here and retiring scheduler
|
|
this.removePlayerFromWorld();
|
|
+ } finally { // Folia start - rewrite login process - move connection ownership to global region
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.player.serverLevel().getCurrentWorldData();
|
|
+ worldData.connections.remove(this.connection);
|
|
+ // once waitingForSwitchToConfig is set, the global tick thread will own the connection
|
|
+ } // Folia end - rewrite login process - move connection ownership to global region
|
|
+ this.waitingForSwitchToConfig = true; // Folia - rewrite login process - fix bad ordering of this field write - moved down
|
|
this.send(new ClientboundStartConfigurationPacket());
|
|
}
|
|
|
|
@@ -2691,7 +2760,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
|
|
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;
|
|
}
|
|
@@ -2832,6 +2901,12 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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, RespawnReason.END_PORTAL, org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag.END_PORTAL); // Paper - add isEndCreditsRespawn argument
|
|
CriteriaTriggers.CHANGED_DIMENSION.trigger(this.player, Level.END, Level.OVERWORLD);
|
|
@@ -2840,6 +2915,18 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
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.level().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS)).set(false, player.serverLevel()); // Paper
|
|
+ }
|
|
+ });
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
this.player = this.server.getPlayerList().respawn(this.player, false, RespawnReason.DEATH);
|
|
if (this.server.isHardcore()) {
|
|
this.player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper
|
|
@@ -3198,7 +3285,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
// Paper start
|
|
if (!org.bukkit.Bukkit.isPrimaryThread()) {
|
|
if (this.recipeSpamPackets.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamLimit) {
|
|
- this.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;
|
|
}
|
|
}
|
|
@@ -3367,7 +3454,18 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
|
|
this.filterTextPacket(list).thenAcceptAsync((list1) -> {
|
|
this.updateSignText(packet, list1);
|
|
- }, this.server);
|
|
+ }, (Runnable run) -> { // Folia start - region threading
|
|
+ this.player.getBukkitEntity().taskScheduler.schedule(
|
|
+ (player) -> {
|
|
+ run.run();
|
|
+ },
|
|
+ null, 1L);
|
|
+ }).whenComplete((Object res, Throwable thr) -> {
|
|
+ if (thr != null) {
|
|
+ LOGGER.error("Failed to handle sign update packet", thr);
|
|
+ }
|
|
+ });
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
private void updateSignText(ServerboundSignUpdatePacket packet, List<FilteredText> signText) {
|
|
diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
|
|
index 1c4f272219e68373eaae93fc5ea9af7d8f3fd6f9..6f483c6581daeaf68f85e3b730d31c5b7a6ae001 100644
|
|
--- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
|
|
+++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
|
|
@@ -82,9 +82,13 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
|
|
}
|
|
// Paper end
|
|
if (this.state == ServerLoginPacketListenerImpl.State.VERIFYING) {
|
|
- if (this.connection.isConnected()) { // Paper - prevent logins to be processed even though disconnect was called
|
|
+ // Folia start - region threading - rewrite login process
|
|
+ String name = this.authenticatedProfile.getName();
|
|
+ UUID uniqueId = this.authenticatedProfile.getId();
|
|
+ if (this.server.getPlayerList().pushPendingJoin(name, uniqueId, this.connection)) {
|
|
+ // Folia end - region threading - rewrite login process
|
|
this.verifyLoginAndFinishConnectionSetup((GameProfile) Objects.requireNonNull(this.authenticatedProfile));
|
|
- } // Paper
|
|
+ } // Folia - region threading - rewrite login process
|
|
}
|
|
|
|
if (this.state == ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT && !this.isPlayerAlreadyInWorld((GameProfile) Objects.requireNonNull(this.authenticatedProfile))) {
|
|
@@ -227,7 +231,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
|
|
}));
|
|
}
|
|
|
|
- boolean flag = playerlist.disconnectAllPlayersWithProfile(profile, this.player); // CraftBukkit - add player reference
|
|
+ boolean flag = false && playerlist.disconnectAllPlayersWithProfile(profile, this.player); // CraftBukkit - add player reference // Folia - rewrite login process - always false here
|
|
|
|
if (flag) {
|
|
this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT;
|
|
@@ -353,7 +357,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
|
|
diff --git a/src/main/java/net/minecraft/server/players/BanListEntry.java b/src/main/java/net/minecraft/server/players/BanListEntry.java
|
|
index 8b1da1fb5ca27432a39aff6dbc452b793268dab5..e83f3676d5a194fa8d3d1567edcb4b6f7847a4c1 100644
|
|
--- a/src/main/java/net/minecraft/server/players/BanListEntry.java
|
|
+++ b/src/main/java/net/minecraft/server/players/BanListEntry.java
|
|
@@ -10,7 +10,7 @@ import net.minecraft.network.chat.Component;
|
|
|
|
public abstract class BanListEntry<T> extends StoredUserEntry<T> {
|
|
|
|
- public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT);
|
|
+ public static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT)); // Folia - region threading - SDF is not thread-safe
|
|
public static final String EXPIRES_NEVER = "forever";
|
|
protected final Date created;
|
|
protected final String source;
|
|
@@ -32,7 +32,7 @@ public abstract class BanListEntry<T> extends StoredUserEntry<T> {
|
|
Date date;
|
|
|
|
try {
|
|
- date = json.has("created") ? BanListEntry.DATE_FORMAT.parse(json.get("created").getAsString()) : new Date();
|
|
+ date = json.has("created") ? BanListEntry.DATE_FORMAT.get().parse(json.get("created").getAsString()) : new Date(); // Folia - region threading - SDF is not thread-safe
|
|
} catch (ParseException parseexception) {
|
|
date = new Date();
|
|
}
|
|
@@ -43,7 +43,7 @@ public abstract class BanListEntry<T> extends StoredUserEntry<T> {
|
|
Date date1;
|
|
|
|
try {
|
|
- date1 = json.has("expires") ? BanListEntry.DATE_FORMAT.parse(json.get("expires").getAsString()) : null;
|
|
+ date1 = json.has("expires") ? BanListEntry.DATE_FORMAT.get().parse(json.get("expires").getAsString()) : null; // Folia - region threading - SDF is not thread-safe
|
|
} catch (ParseException parseexception1) {
|
|
date1 = null;
|
|
}
|
|
@@ -78,9 +78,9 @@ public abstract class BanListEntry<T> extends StoredUserEntry<T> {
|
|
|
|
@Override
|
|
protected void serialize(JsonObject json) {
|
|
- json.addProperty("created", BanListEntry.DATE_FORMAT.format(this.created));
|
|
+ json.addProperty("created", BanListEntry.DATE_FORMAT.get().format(this.created)); // Folia - region threading - SDF is not thread-safe
|
|
json.addProperty("source", this.source);
|
|
- json.addProperty("expires", this.expires == null ? "forever" : BanListEntry.DATE_FORMAT.format(this.expires));
|
|
+ json.addProperty("expires", this.expires == null ? "forever" : BanListEntry.DATE_FORMAT.get().format(this.expires)); // Folia - region threading - SDF is not thread-safe
|
|
json.addProperty("reason", this.reason);
|
|
}
|
|
|
|
@@ -89,7 +89,7 @@ public abstract class BanListEntry<T> extends StoredUserEntry<T> {
|
|
Date expires = null;
|
|
|
|
try {
|
|
- expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.parse(jsonobject.get("expires").getAsString()) : null;
|
|
+ expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.get().parse(jsonobject.get("expires").getAsString()) : null; // Folia - region threading - SDF is not thread-safe
|
|
} catch (ParseException ex) {
|
|
// Guess we don't have a date
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/players/OldUsersConverter.java b/src/main/java/net/minecraft/server/players/OldUsersConverter.java
|
|
index ce43cb0152ba07c6c21e08142d65813d47c3b63b..8bdb1d10648965e2011b4a22a1dfc575d3bbe4e0 100644
|
|
--- a/src/main/java/net/minecraft/server/players/OldUsersConverter.java
|
|
+++ b/src/main/java/net/minecraft/server/players/OldUsersConverter.java
|
|
@@ -512,7 +512,7 @@ public class OldUsersConverter {
|
|
Date date1;
|
|
|
|
try {
|
|
- date1 = BanListEntry.DATE_FORMAT.parse(dateString);
|
|
+ date1 = BanListEntry.DATE_FORMAT.get().parse(dateString); // Folia - region threading - SDF is not thread-safe
|
|
} catch (ParseException parseexception) {
|
|
date1 = fallback;
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
index 33abcf12b4426572b74ca4c813e4392c823494bc..6cce2a9bc9beeeef25b7bacd2ad307ed3ae44432 100644
|
|
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
@@ -133,10 +133,10 @@ public abstract class PlayerList {
|
|
public static final Component DUPLICATE_LOGIN_DISCONNECT_MESSAGE = Component.translatable("multiplayer.disconnect.duplicate_login");
|
|
private static final Logger LOGGER = LogUtils.getLogger();
|
|
private static final int SEND_PLAYER_INFO_INTERVAL = 600;
|
|
- private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z");
|
|
+ private static final ThreadLocal<SimpleDateFormat> BAN_DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z")); // Folia - region threading - SDF is not thread-safe
|
|
private final MinecraftServer server;
|
|
public final List<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;
|
|
@@ -157,9 +157,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
|
|
@@ -180,7 +227,7 @@ public abstract class PlayerList {
|
|
}
|
|
abstract public void loadAndSaveFiles(); // Paper - moved from DedicatedPlayerList constructor
|
|
|
|
- public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData) {
|
|
+ public void loadSpawnForNewPlayer(final Connection connection, final ServerPlayer player, final CommonListenerCookie clientData, org.apache.commons.lang3.mutable.MutableObject<CompoundTag> data, org.apache.commons.lang3.mutable.MutableObject<String> lastKnownName, ca.spottedleaf.concurrentutil.completable.Completable<Location> toComplete) { // Folia - region threading - rewrite login process
|
|
player.isRealPlayer = true; // Paper
|
|
player.loginTime = System.currentTimeMillis(); // Paper
|
|
GameProfile gameprofile = player.getGameProfile();
|
|
@@ -259,18 +306,42 @@ public abstract class PlayerList {
|
|
player.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT; // set Player SpawnReason to DEFAULT on first login
|
|
// Paper start - reset to main world spawn if first spawn or invalid world
|
|
}
|
|
+ // Folia start - region threading - rewrite login process
|
|
+ // must write to these before toComplete is invoked
|
|
+ data.setValue(nbttagcompound);
|
|
+ lastKnownName.setValue(s);
|
|
+ // Folia end - region threading - rewrite login process
|
|
if (nbttagcompound == null || invalidPlayerWorld) {
|
|
// Paper end
|
|
- player.fudgeSpawnLocation(worldserver1); // only move to spawn on first login, otherwise, stay where you are....
|
|
+ ServerPlayer.fudgeSpawnLocation(worldserver1, player, toComplete); // only move to spawn on first login, otherwise, stay where you are.... // Folia - region threading
|
|
+ } else { // Folia start - region threading - rewrite login process
|
|
+ worldserver1.loadChunksForMoveAsync(
|
|
+ player.getBoundingBox(),
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER,
|
|
+ (c) -> {
|
|
+ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(worldserver1, player.position()));
|
|
+ }
|
|
+ );
|
|
}
|
|
+ // Folia end - region threading - rewrite login process
|
|
// Paper end
|
|
+ // Folia start - region threading - rewrite login process
|
|
+ return;
|
|
+ }
|
|
+ // nbttagcomound -> player data
|
|
+ // s -> last known name
|
|
+ public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData, CompoundTag nbttagcompound, String s, Location selectedSpawn) {
|
|
+ ServerLevel worldserver1 = ((CraftWorld)selectedSpawn.getWorld()).getHandle();
|
|
+ player.setPosRaw(selectedSpawn.getX(), selectedSpawn.getY(), selectedSpawn.getZ());
|
|
+ player.lastSave = System.nanoTime(); // changed to nanoTime
|
|
+ // Folia end - region threading - rewrite login process
|
|
player.setServerLevel(worldserver1);
|
|
String s1 = connection.getLoggableAddress(this.server.logIPs());
|
|
|
|
// 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();
|
|
@@ -289,6 +360,10 @@ public abstract class PlayerList {
|
|
|
|
player.loadGameTypes(nbttagcompound);
|
|
ServerGamePacketListenerImpl playerconnection = new ServerGamePacketListenerImpl(this.server, connection, player, clientData);
|
|
+ // Folia start - rewrite login process
|
|
+ // only after setting the connection listener to game type, add the connection to this regions list
|
|
+ worldserver1.getCurrentWorldData().connections.add(connection);
|
|
+ // Folia end - rewrite login process
|
|
GameRules gamerules = worldserver1.getGameRules();
|
|
boolean flag = gamerules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN);
|
|
boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO);
|
|
@@ -304,7 +379,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;
|
|
|
|
@@ -346,7 +421,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();
|
|
@@ -361,8 +436,7 @@ public abstract class PlayerList {
|
|
ClientboundPlayerInfoUpdatePacket packet = ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)); // Paper
|
|
|
|
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)) {
|
|
// Paper start
|
|
@@ -484,7 +558,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
|
|
@@ -579,7 +653,7 @@ public abstract class PlayerList {
|
|
|
|
protected void save(ServerPlayer player) {
|
|
if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit
|
|
- player.lastSave = MinecraftServer.currentTick; // Paper
|
|
+ player.lastSave = System.nanoTime(); // Folia - region threading - changed to nanoTime tracking
|
|
this.playerIo.save(player);
|
|
ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit
|
|
|
|
@@ -619,7 +693,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) {
|
|
@@ -659,7 +733,7 @@ public abstract class PlayerList {
|
|
|
|
entityplayer.unRide();
|
|
worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER);
|
|
- entityplayer.retireScheduler(); // Paper - Folia schedulers
|
|
+ // Folia - region threading - move to onDisconnect of common packet listener
|
|
entityplayer.getAdvancements().stopListening();
|
|
this.players.remove(entityplayer);
|
|
this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
|
|
@@ -678,8 +752,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);
|
|
@@ -704,19 +777,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
|
|
@@ -735,7 +802,7 @@ public abstract class PlayerList {
|
|
|
|
ichatmutablecomponent = Component.translatable("multiplayer.disconnect.banned.reason", gameprofilebanentry.getReason());
|
|
if (gameprofilebanentry.getExpires() != null) {
|
|
- ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned.expiration", PlayerList.BAN_DATE_FORMAT.format(gameprofilebanentry.getExpires())));
|
|
+ ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned.expiration", PlayerList.BAN_DATE_FORMAT.get().format(gameprofilebanentry.getExpires()))); // Folia - region threading - SDF is not thread-safe
|
|
}
|
|
|
|
// return chatmessage;
|
|
@@ -748,14 +815,14 @@ public abstract class PlayerList {
|
|
|
|
ichatmutablecomponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipbanentry.getReason());
|
|
if (ipbanentry.getExpires() != null) {
|
|
- ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned_ip.expiration", PlayerList.BAN_DATE_FORMAT.format(ipbanentry.getExpires())));
|
|
+ ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned_ip.expiration", PlayerList.BAN_DATE_FORMAT.get().format(ipbanentry.getExpires()))); // Folia - region threading - SDF is not thread-safe
|
|
}
|
|
|
|
// return chatmessage;
|
|
event.disallow(PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.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
|
|
}
|
|
}
|
|
@@ -821,6 +888,11 @@ public abstract class PlayerList {
|
|
|
|
public ServerPlayer respawn(ServerPlayer entityplayer, ServerLevel worldserver, boolean flag, Location location, boolean avoidSuffocation, RespawnReason reason, org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag...respawnFlags) {
|
|
// Paper end
|
|
+ // Folia start - region threading
|
|
+ if (true) {
|
|
+ throw new UnsupportedOperationException("Must use teleportAsync while in region threading");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
entityplayer.stopRiding(); // CraftBukkit
|
|
this.players.remove(entityplayer);
|
|
this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
|
|
@@ -1014,10 +1086,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());
|
|
@@ -1043,18 +1115,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
|
|
}
|
|
|
|
}
|
|
@@ -1098,8 +1169,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);
|
|
@@ -1110,10 +1180,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;
|
|
@@ -1132,7 +1204,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
|
|
}
|
|
|
|
}
|
|
@@ -1142,7 +1216,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
|
|
+
|
|
}
|
|
|
|
}
|
|
@@ -1203,8 +1280,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())) {
|
|
@@ -1234,12 +1310,21 @@ 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);
|
|
- if (interval == -1 || now - entityplayer.lastSave >= interval) {
|
|
+ long now = System.nanoTime(); // Folia - region threading
|
|
+ int max = io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick(); // Folia - region threading
|
|
+ long timeInterval = (long)interval * io.papermc.paper.threadedregions.TickRegionScheduler.TIME_BETWEEN_TICKS; // 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 >= timeInterval) { // Folia - region threading
|
|
this.save(entityplayer);
|
|
- if (interval != -1 && ++numSaved >= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { break; }
|
|
+ // Folia start - region threading
|
|
+ if (interval != -1 && max != -1 && ++numSaved >= max) {
|
|
+ break;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
}
|
|
// Paper end
|
|
}
|
|
@@ -1356,6 +1441,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 = io.papermc.paper.adventure.PaperAdventure.asVanilla(this.server.server.shutdownMessage()); // Paper - Adventure
|
|
+ // CraftBukkit end
|
|
+
|
|
+ player.connection.send(new net.minecraft.network.protocol.common.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) {
|
|
@@ -1365,7 +1464,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 09fc086548b9d0f97849f56f41e3a5be87f5091a..4dfb52f7fde8603b9fc236ea57e9b768a9d04fc6 100644
|
|
--- a/src/main/java/net/minecraft/server/players/StoredUserList.java
|
|
+++ b/src/main/java/net/minecraft/server/players/StoredUserList.java
|
|
@@ -143,6 +143,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
|
|
@@ -173,10 +174,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);
|
|
|
|
@@ -233,5 +236,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 a6ac76707da39cf86113003b1f326433fdc86c86..531ccc48154b25378e9039478ce7f89be4740bce 100644
|
|
--- a/src/main/java/net/minecraft/util/SortedArraySet.java
|
|
+++ b/src/main/java/net/minecraft/util/SortedArraySet.java
|
|
@@ -90,7 +90,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;
|
|
}
|
|
|
|
@@ -177,6 +177,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 028d69907a988e191213a17e072ef22710b5bc83..ffa081156313247882747ea6da182ee54a1cf8a0 100644
|
|
--- a/src/main/java/net/minecraft/util/SpawnUtil.java
|
|
+++ b/src/main/java/net/minecraft/util/SpawnUtil.java
|
|
@@ -63,7 +63,7 @@ public class SpawnUtil {
|
|
return Optional.of(t0);
|
|
}
|
|
|
|
- t0.discard();
|
|
+ //t0.discard(); // Folia - region threading
|
|
}
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/RandomSequences.java b/src/main/java/net/minecraft/world/RandomSequences.java
|
|
index b8adc775270a4cc179ce009d41f48c8c898a5b68..281f41edbb08ec2872e42f4fca0082a48e26d8a5 100644
|
|
--- a/src/main/java/net/minecraft/world/RandomSequences.java
|
|
+++ b/src/main/java/net/minecraft/world/RandomSequences.java
|
|
@@ -20,7 +20,7 @@ public class RandomSequences extends SavedData {
|
|
private int salt;
|
|
private boolean includeWorldSeed = true;
|
|
private boolean includeSequenceId = true;
|
|
- private final Map<ResourceLocation, RandomSequence> sequences = new Object2ObjectOpenHashMap<>();
|
|
+ private final Map<ResourceLocation, RandomSequence> sequences = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading
|
|
|
|
public static SavedData.Factory<RandomSequences> factory(long seed) {
|
|
return new SavedData.Factory<>(() -> {
|
|
@@ -116,61 +116,61 @@ public class RandomSequences extends SavedData {
|
|
@Override
|
|
public RandomSource fork() {
|
|
RandomSequences.this.setDirty();
|
|
- return this.random.fork();
|
|
+ synchronized (this.random) { return this.random.fork(); } // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public PositionalRandomFactory forkPositional() {
|
|
RandomSequences.this.setDirty();
|
|
- return this.random.forkPositional();
|
|
+ synchronized (this.random) { return this.random.forkPositional(); } // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public void setSeed(long seed) {
|
|
RandomSequences.this.setDirty();
|
|
- this.random.setSeed(seed);
|
|
+ synchronized (this.random) { this.random.setSeed(seed); } // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public int nextInt() {
|
|
RandomSequences.this.setDirty();
|
|
- return this.random.nextInt();
|
|
+ synchronized (this.random) { return this.random.nextInt(); } // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public int nextInt(int bound) {
|
|
RandomSequences.this.setDirty();
|
|
- return this.random.nextInt(bound);
|
|
+ synchronized (this.random) { return this.random.nextInt(bound); } // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public long nextLong() {
|
|
RandomSequences.this.setDirty();
|
|
- return this.random.nextLong();
|
|
+ synchronized (this.random) { return this.random.nextLong(); } // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public boolean nextBoolean() {
|
|
RandomSequences.this.setDirty();
|
|
- return this.random.nextBoolean();
|
|
+ synchronized (this.random) { return this.random.nextBoolean(); } // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public float nextFloat() {
|
|
RandomSequences.this.setDirty();
|
|
- return this.random.nextFloat();
|
|
+ synchronized (this.random) { return this.random.nextFloat(); } // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public double nextDouble() {
|
|
RandomSequences.this.setDirty();
|
|
- return this.random.nextDouble();
|
|
+ synchronized (this.random) { return this.random.nextDouble(); } // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
public double nextGaussian() {
|
|
RandomSequences.this.setDirty();
|
|
- return this.random.nextGaussian();
|
|
+ synchronized (this.random) { return this.random.nextGaussian(); } // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
diff --git a/src/main/java/net/minecraft/world/damagesource/CombatTracker.java b/src/main/java/net/minecraft/world/damagesource/CombatTracker.java
|
|
index 9c99b2e365aacb8309f29acb9025faccd2c676b3..d02bc26812321745795d2f0bc3705addd0be912d 100644
|
|
--- a/src/main/java/net/minecraft/world/damagesource/CombatTracker.java
|
|
+++ b/src/main/java/net/minecraft/world/damagesource/CombatTracker.java
|
|
@@ -52,7 +52,7 @@ public class CombatTracker {
|
|
|
|
private Component getMessageForAssistedFall(Entity attacker, Component attackerDisplayName, String itemDeathTranslationKey, String deathTranslationKey) {
|
|
ItemStack var10000;
|
|
- if (attacker instanceof LivingEntity livingEntity) {
|
|
+ if (attacker instanceof LivingEntity livingEntity && io.papermc.paper.util.TickThread.isTickThreadFor(livingEntity)) { // Folia - region threading
|
|
var10000 = livingEntity.getMainHandItem();
|
|
} else {
|
|
var10000 = ItemStack.EMPTY;
|
|
@@ -81,7 +81,7 @@ public class CombatTracker {
|
|
|
|
@Nullable
|
|
private static Component getDisplayName(@Nullable Entity entity) {
|
|
- return entity == null ? null : entity.getDisplayName();
|
|
+ return entity == null || !io.papermc.paper.util.TickThread.isTickThreadFor(entity) ? null : entity.getDisplayName(); // Folia - region threading
|
|
}
|
|
|
|
public Component getDeathMessage() {
|
|
diff --git a/src/main/java/net/minecraft/world/damagesource/DamageSource.java b/src/main/java/net/minecraft/world/damagesource/DamageSource.java
|
|
index 25a5a3b949a0eb632611355e74ccd4865be108ca..1df8d601e41c2ab35921b6a1534fdec6e6353954 100644
|
|
--- a/src/main/java/net/minecraft/world/damagesource/DamageSource.java
|
|
+++ b/src/main/java/net/minecraft/world/damagesource/DamageSource.java
|
|
@@ -106,13 +106,13 @@ public class DamageSource {
|
|
LivingEntity entityliving1 = killed.getKillCredit();
|
|
String s1 = s + ".player";
|
|
|
|
- return entityliving1 != null ? Component.translatable(s1, killed.getDisplayName(), entityliving1.getDisplayName()) : Component.translatable(s, killed.getDisplayName());
|
|
+ return entityliving1 != null && io.papermc.paper.util.TickThread.isTickThreadFor(entityliving1) ? Component.translatable(s1, killed.getDisplayName(), entityliving1.getDisplayName()) : Component.translatable(s, killed.getDisplayName()); // Folia - region threading
|
|
} else {
|
|
Component ichatbasecomponent = this.causingEntity == null ? this.directEntity.getDisplayName() : this.causingEntity.getDisplayName();
|
|
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/damagesource/FallLocation.java b/src/main/java/net/minecraft/world/damagesource/FallLocation.java
|
|
index ea27b46eec01bda427653335f922ccd068cffcb5..e551d3b875eab6851b75041f418c9a085a8903b6 100644
|
|
--- a/src/main/java/net/minecraft/world/damagesource/FallLocation.java
|
|
+++ b/src/main/java/net/minecraft/world/damagesource/FallLocation.java
|
|
@@ -39,7 +39,7 @@ public record FallLocation(String id) {
|
|
@Nullable
|
|
public static FallLocation getCurrentFallLocation(LivingEntity entity) {
|
|
Optional<BlockPos> optional = entity.getLastClimbablePos();
|
|
- if (optional.isPresent()) {
|
|
+ if (optional.isPresent() && io.papermc.paper.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)entity.level(), optional.get())) { // Folia - region threading
|
|
BlockState blockState = entity.level().getBlockState(optional.get());
|
|
return blockToFallLocation(blockState);
|
|
} else {
|
|
diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
|
|
index c655c6fee393c62ba79301f76baa72f9b1154a9a..dd18591d7928ab04f6f7c09aa89a26ccbe20c323 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/Entity.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
|
|
@@ -166,7 +166,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
|
|
@@ -240,7 +240,7 @@ 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
|
|
@@ -531,6 +531,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 boolean isLegacyTrackingEntity = false;
|
|
|
|
@@ -538,28 +557,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 ticking
|
|
// Paper end - optimise entity tracking
|
|
|
|
public Entity(EntityType<?> type, Level world) {
|
|
@@ -803,6 +801,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();
|
|
@@ -825,7 +829,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();
|
|
}
|
|
@@ -934,11 +938,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(this.level, this.lastLavaContact);
|
|
+ CraftEventFactory.blockDamageRT.set((this.lastLavaContact) == null ? null : org.bukkit.craftbukkit.block.CraftBlock.at(this.level, this.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
|
|
|
|
}
|
|
@@ -1083,8 +1087,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;
|
|
@@ -2946,7 +2950,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
this.passengers = ImmutableList.copyOf(list);
|
|
}
|
|
|
|
- this.gameEvent(GameEvent.ENTITY_MOUNT, passenger);
|
|
+ if (!passenger.hasNullCallback()) this.gameEvent(GameEvent.ENTITY_MOUNT, passenger); // Folia - region threading - do not fire game events for entities not added
|
|
}
|
|
}
|
|
|
|
@@ -2995,7 +2999,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
}
|
|
|
|
entity.boardingCooldown = 60;
|
|
- this.gameEvent(GameEvent.ENTITY_DISMOUNT, entity);
|
|
+ if (!entity.hasNullCallback()) this.gameEvent(GameEvent.ENTITY_DISMOUNT, entity); // Folia - region threading - do not fire game events for entities not added
|
|
}
|
|
return true; // CraftBukkit
|
|
}
|
|
@@ -3275,6 +3279,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());
|
|
}
|
|
@@ -3390,9 +3399,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
|
|
@@ -3565,6 +3574,775 @@ 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(boolean teleport) {
|
|
+ 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, teleport ? Entity::moveTo : Entity::setPos);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void repositionAllPassengers(boolean teleport) {
|
|
+ this.makePassengerTree().adjustRiders(teleport);
|
|
+ }
|
|
+
|
|
+ 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;
|
|
+ /**
|
|
+ * The flag will dismount any passengers and dismout from the current vehicle
|
|
+ * to teleport if and only if dismounting would result in the teleport being allowed.
|
|
+ */
|
|
+ public static final long TELEPORT_FLAG_UNMOUNT = 1L << 2;
|
|
+
|
|
+ protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) {
|
|
+ destination.addDuringTeleport(this);
|
|
+ }
|
|
+
|
|
+ protected final void placeInAsync(ServerLevel originWorld, ServerLevel destination, long teleportFlags,
|
|
+ EntityTreeNode passengerTree, java.util.function.Consumer<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(true);
|
|
+
|
|
+ // invoke post dimension change now
|
|
+ for (EntityTreeNode node : fullTree) {
|
|
+ node.root.postChangeDimension();
|
|
+ }
|
|
+
|
|
+ if (teleportComplete != null) {
|
|
+ teleportComplete.accept(Entity.this);
|
|
+ }
|
|
+ }
|
|
+ );
|
|
+ };
|
|
+
|
|
+ if ((teleportFlags & TELEPORT_FLAG_LOAD_CHUNK) != 0L) {
|
|
+ destination.loadChunksForMoveAsync(
|
|
+ Entity.this.getBoundingBox(), ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER,
|
|
+ (chunkList) -> {
|
|
+ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunkList) {
|
|
+ destination.chunkSource.addTicketAtLevel(
|
|
+ TicketType.POST_TELEPORT, chunk.getPos(),
|
|
+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL,
|
|
+ Integer.valueOf(Entity.this.getId())
|
|
+ );
|
|
+ }
|
|
+ scheduleEntityJoin.run();
|
|
+ }
|
|
+ );
|
|
+ } else {
|
|
+ scheduleEntityJoin.run();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected boolean canTeleportAsync() {
|
|
+ return !this.hasNullCallback() && !this.isRemoved() && this.isAlive() && (!(this instanceof net.minecraft.world.entity.LivingEntity livingEntity) || !livingEntity.isSleeping());
|
|
+ }
|
|
+
|
|
+ // Mojang for whatever reason has started storing positions to cache certain physics properties that entities collide with
|
|
+ // As usual though, they don't properly do anything to prevent serious desync with respect to the current entity position
|
|
+ // We add additional logic to reset these before teleporting to prevent issues with them possibly tripping thread checks.
|
|
+ protected void resetStoredPositions() {
|
|
+ this.mainSupportingBlockPos = Optional.empty();
|
|
+ }
|
|
+
|
|
+ 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);
|
|
+ this.resetStoredPositions();
|
|
+ }
|
|
+
|
|
+ protected void transform(Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) {
|
|
+ if (yaw != null) {
|
|
+ this.setYRot(yaw.floatValue());
|
|
+ this.setYHeadRot(yaw.floatValue());
|
|
+ }
|
|
+ if (pitch != null) {
|
|
+ this.setXRot(pitch.floatValue());
|
|
+ }
|
|
+ if (speedDirectionUpdate != null) {
|
|
+ this.setDeltaMovement(speedDirectionUpdate);
|
|
+ }
|
|
+ if (pos != null) {
|
|
+ this.setPosRaw(pos.x, pos.y, pos.z);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected Entity transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate) {
|
|
+ this.removeAfterChangingDimensions(); // remove before so that any CBEntity#getHandle call affects this entity before copying
|
|
+
|
|
+ Entity copy = this.getType().create(destination);
|
|
+ copy.restoreFrom(this);
|
|
+ copy.transform(pos, yaw, pitch, speedDirectionUpdate);
|
|
+ // vanilla code used to call remove _after_ copying, and some stuff is required to be after copy - so add hook here
|
|
+ // for example, clearing of inventory after switching dimensions
|
|
+ this.postRemoveAfterChangingDimensions();
|
|
+
|
|
+ return copy;
|
|
+ }
|
|
+
|
|
+ public final boolean teleportAsync(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 speedDirectionUpdate,
|
|
+ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause, long teleportFlags,
|
|
+ java.util.function.Consumer<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 (!this.canTeleportAsync()) {
|
|
+ return false;
|
|
+ }
|
|
+ this.getBukkitEntity(); // force bukkit entity to be created before TPing
|
|
+ if ((teleportFlags & TELEPORT_FLAG_UNMOUNT) == 0L) {
|
|
+ for (Entity entity : this.getIndirectPassengers()) {
|
|
+ if (!entity.canTeleportAsync()) {
|
|
+ return false;
|
|
+ }
|
|
+ entity.getBukkitEntity(); // force bukkit entity to be created before TPing
|
|
+ }
|
|
+ } else {
|
|
+ this.unRide();
|
|
+ }
|
|
+
|
|
+ if ((teleportFlags & TELEPORT_FLAG_TELEPORT_PASSENGERS) != 0L) {
|
|
+ if (this.isPassenger()) {
|
|
+ return false;
|
|
+ }
|
|
+ } else {
|
|
+ if (this.isVehicle() || this.isPassenger()) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // TODO any events that can modify go HERE
|
|
+
|
|
+ // check for same region
|
|
+ if (destination == this.level()) {
|
|
+ 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(true);
|
|
+
|
|
+ // 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() {
|
|
+ this.resetStoredPositions();
|
|
+ }
|
|
+
|
|
+ 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.level().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.level().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
|
|
@@ -3573,6 +4351,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
|
|
@Nullable
|
|
public Entity teleportTo(ServerLevel worldserver, Vec3 location) {
|
|
+ // Folia start - region threading
|
|
+ if (true) {
|
|
+ throw new UnsupportedOperationException("Must use teleportAsync while in region threading");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
// CraftBukkit end
|
|
// Paper start - fix bad state entities causing dupes
|
|
if (!this.isAlive() || !this.valid) {
|
|
@@ -3661,6 +4444,12 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
}
|
|
}
|
|
|
|
+ // Folia start - region threading - move inventory clearing until after the dimension change
|
|
+ protected void postRemoveAfterChangingDimensions() {
|
|
+
|
|
+ }
|
|
+ // Folia end - region threading - move inventory clearing until after the dimension change
|
|
+
|
|
protected void removeAfterChangingDimensions() {
|
|
this.setRemoved(Entity.RemovalReason.CHANGED_DIMENSION);
|
|
}
|
|
@@ -4589,7 +5378,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
|
|
@@ -4610,7 +5400,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
|
|
@@ -4697,6 +5487,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;
|
|
@@ -4712,6 +5508,9 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
}
|
|
// Paper end - rewrite chunk system
|
|
final boolean alreadyRemoved = this.removalReason != null;
|
|
+ // Folia start - region threading
|
|
+ this.preRemove(reason);
|
|
+ // Folia end - region threading
|
|
if (this.removalReason == null) {
|
|
this.removalReason = reason;
|
|
}
|
|
@@ -4734,6 +5533,10 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
|
|
this.removalReason = null;
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ protected void preRemove(Entity.RemovalReason reason) {}
|
|
+ // Folia end - region threading
|
|
+
|
|
// Paper start - Folia schedulers
|
|
/**
|
|
* Invoked only when the entity is truly removed from the server, never to be added to any world.
|
|
diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
|
|
index a76eb3d051db0229ed088b71c92ff3f131449007..db4f220bc9767ced5a98addc9e8b440b4f4f5b03 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
|
|
@@ -276,6 +276,13 @@ public abstract class LivingEntity extends Entity implements Attackable {
|
|
++this.noActionTime; // Above all the floats
|
|
}
|
|
// Spigot end
|
|
+ // Folia start - region threading
|
|
+ @Override
|
|
+ protected void resetStoredPositions() {
|
|
+ super.resetStoredPositions();
|
|
+ this.lastClimbablePos = Optional.empty();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
|
|
protected LivingEntity(EntityType<? extends LivingEntity> type, Level world) {
|
|
super(type, world);
|
|
@@ -481,7 +488,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;
|
|
@@ -627,11 +634,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.level().broadcastEntityEvent(this, (byte) 60);
|
|
- this.remove(Entity.RemovalReason.KILLED);
|
|
+ this.broadcastedDeath = true; // Folia - region threading - death has been broadcasted
|
|
+ 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
|
|
}
|
|
|
|
}
|
|
@@ -852,9 +862,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 (!this.level().paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof net.minecraft.world.entity.player.Player)) { scoreboardteam = null; } // Paper
|
|
@@ -1134,7 +1144,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
|
|
}
|
|
|
|
public boolean addEffect(MobEffectInstance mobeffect, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause) {
|
|
- // org.spigotmc.AsyncCatcher.catchOp("effect add"); // Spigot // Paper - move to API
|
|
+ if (!this.hasNullCallback()) io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot add effects to entities asynchronously"); // Folia - region threading
|
|
if (this.isTickingEffects) {
|
|
this.effectsToProcess.add(new ProcessableEffect(mobeffect, cause));
|
|
return true;
|
|
@@ -2331,7 +2341,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
|
|
|
|
@Nullable
|
|
public LivingEntity getKillCredit() {
|
|
- return (LivingEntity) (this.lastHurtByPlayer != null ? this.lastHurtByPlayer : (this.lastHurtByMob != null ? this.lastHurtByMob : null));
|
|
+ return (LivingEntity) (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() {
|
|
@@ -3469,7 +3479,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)) { // Folia - region threading
|
|
if (this.xo != this.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());
|
|
@@ -4163,7 +4173,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 956d05e2ae59978ea9623ca0e167c0afe0b87306..4e9d510646abbc2d2b6f2d935f7416b6872eb234 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/Mob.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/Mob.java
|
|
@@ -136,6 +136,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);
|
|
@@ -287,9 +295,21 @@ public abstract class Mob extends LivingEntity implements Targeting {
|
|
@Nullable
|
|
@Override
|
|
public LivingEntity getTarget() {
|
|
+ // Folia start - region threading
|
|
+ if (this.target != null && (!io.papermc.paper.util.TickThread.isTickThreadFor(this.target) || this.target.isRemoved())) {
|
|
+ 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
|
|
@@ -297,7 +317,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;
|
|
@@ -907,7 +927,7 @@ public abstract class Mob extends LivingEntity implements Targeting {
|
|
this.level().getProfiler().push("sensing");
|
|
this.sensing.tick();
|
|
this.level().getProfiler().pop();
|
|
- int i = this.level().getServer().getTickCount() + this.getId();
|
|
+ int i = this.tickCount + this.getId(); // Folia - region threading
|
|
|
|
if (i % 2 != 0 && this.tickCount > 1) {
|
|
this.level().getProfiler().push("targetSelector");
|
|
@@ -1744,6 +1764,15 @@ public abstract class Mob extends LivingEntity implements Targeting {
|
|
this.goalSelector.removeAllGoals(predicate);
|
|
}
|
|
|
|
+ // Folia start - region threading - move inventory clearing until after the dimension change
|
|
+ @Override
|
|
+ protected void postRemoveAfterChangingDimensions() {
|
|
+ this.getAllSlots().forEach((itemstack) -> {
|
|
+ if (!itemstack.isEmpty()) itemstack.setCount(0); // CraftBukkit
|
|
+ });
|
|
+ }
|
|
+ // Folia end - region threading - move inventory clearing until after the dimension change
|
|
+
|
|
@Override
|
|
protected void removeAfterChangingDimensions() {
|
|
super.removeAfterChangingDimensions();
|
|
@@ -1752,12 +1781,7 @@ public abstract class Mob extends LivingEntity implements Targeting {
|
|
this.level().getCraftServer().getPluginManager().callEvent(event); // CraftBukkit
|
|
this.dropLeash(true, event.isDropLeash());
|
|
// Paper end
|
|
- this.getAllSlots().forEach((itemstack) -> {
|
|
- if (!itemstack.isEmpty()) {
|
|
- itemstack.setCount(0);
|
|
- }
|
|
-
|
|
- });
|
|
+ // Folia - region threading - move inventory clearing until after the dimension change - move into postRemoveAfterChangingDimensions
|
|
}
|
|
|
|
@Nullable
|
|
diff --git a/src/main/java/net/minecraft/world/entity/ai/Brain.java b/src/main/java/net/minecraft/world/entity/ai/Brain.java
|
|
index 21e5d1451f90194aa415cf0a183d1a731854f605..c83e9f99f8f3e1b147b1a5122651540dc86abdff 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/ai/Brain.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/ai/Brain.java
|
|
@@ -412,9 +412,17 @@ public class Brain<E extends LivingEntity> {
|
|
}
|
|
|
|
public void stopAll(ServerLevel world, E entity) {
|
|
+ // Folia start - region threading
|
|
+ List<BehaviorControl<? super E>> behaviors = this.getRunningBehaviors();
|
|
+ if (behaviors.isEmpty()) {
|
|
+ // avoid calling getGameTime, as this may be called while portalling an entity - which will cause
|
|
+ // the world data retrieval to fail
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
long l = entity.level().getGameTime();
|
|
|
|
- for(BehaviorControl<? super E> behaviorControl : this.getRunningBehaviors()) {
|
|
+ for(BehaviorControl<? super E> behaviorControl : behaviors) { // Folia - region threading
|
|
behaviorControl.doStop(world, entity, l);
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java b/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java
|
|
index 8ec07578c1e41997a2e5ef158885ad3f4c2a31b6..003d85261bc0df871a8247c193e2b45c822b66b3 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,11 @@ 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())) {
|
|
+ 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 689bbc0feb700cfd6b10601d2c5a237ec40ed756..4fa58570d9124ffd7dd94499f03a844e30ead864 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);
|
|
@@ -133,7 +138,21 @@ public class FollowOwnerGoal extends Goal {
|
|
return false;
|
|
}
|
|
Location 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/GroundPathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java
|
|
index ac996b066415e461af1fcb71b19807401179c7f8..c065d372ac5f1663db3c521e996ee03669d0bc31 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java
|
|
@@ -42,6 +42,11 @@ public class GroundPathNavigation extends PathNavigation {
|
|
|
|
@Override
|
|
public Path createPath(BlockPos target, @javax.annotation.Nullable Entity entity, int distance) { // Paper
|
|
+ // Folia start - region threading
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level, target)) {
|
|
+ return null;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
LevelChunk levelChunk = this.level.getChunkSource().getChunkNow(SectionPos.blockToSectionCoord(target.getX()), SectionPos.blockToSectionCoord(target.getZ()));
|
|
if (levelChunk == null) {
|
|
return null;
|
|
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 b37415d45dda8e658c8995a4519e552dc378bb41..147057bfd67d56857b5d3ab1ebd0f0a0c1793a9c 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..37a86fe9585ebf004876a48808e19eb272e2372d 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,13 +112,15 @@ 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;
|
|
|
|
try {
|
|
entityzombie = new Zombie(world);
|
|
+ entityzombie.moveTo(vec3d.x, vec3d.y, vec3d.z, world.random.nextFloat() * 360.0F, 0.0F); // Folia - region threading - move up
|
|
entityzombie.finalizeSpawn(world, world.getCurrentDifficultyAt(entityzombie.blockPosition()), MobSpawnType.EVENT, (SpawnGroupData) null, (CompoundTag) null);
|
|
} catch (Exception exception) {
|
|
VillageSiege.LOGGER.warn("Failed to create zombie for village siege at {}", vec3d, exception);
|
|
@@ -122,7 +128,7 @@ public class VillageSiege implements CustomSpawner {
|
|
return;
|
|
}
|
|
|
|
- entityzombie.moveTo(vec3d.x, vec3d.y, vec3d.z, world.random.nextFloat() * 360.0F, 0.0F);
|
|
+ // Folia - region threading - move up
|
|
world.addFreshEntityWithPassengers(entityzombie, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.VILLAGE_INVASION); // CraftBukkit
|
|
}
|
|
}
|
|
@@ -143,7 +149,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 12a7aaeaa8b4b788b620b1985591c3b93253ccd5..5150d447c9dc2f539446749c8bee102050bab4ed 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/Bee.java b/src/main/java/net/minecraft/world/entity/animal/Bee.java
|
|
index 9a7956befc346e1b58f064213800fd099a052fc6..25fd3aea2d66503018637781ccb8b5690c02e57b 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/animal/Bee.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/animal/Bee.java
|
|
@@ -1036,6 +1036,11 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal {
|
|
|
|
@Override
|
|
public boolean canBeeUse() {
|
|
+ // Folia start - region threading
|
|
+ if (Bee.this.hivePos != null && Bee.this.isTooFarAway(Bee.this.hivePos)) {
|
|
+ Bee.this.hivePos = null;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
return Bee.this.hivePos != null && !Bee.this.hasRestriction() && Bee.this.wantsToEnterHive() && !this.hasReachedTarget(Bee.this.hivePos) && Bee.this.level().getBlockState(Bee.this.hivePos).is(BlockTags.BEEHIVES);
|
|
}
|
|
|
|
@@ -1152,6 +1157,11 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal {
|
|
|
|
@Override
|
|
public boolean canBeeUse() {
|
|
+ // Folia start - region threading
|
|
+ if (Bee.this.savedFlowerPos != null && Bee.this.isTooFarAway(Bee.this.savedFlowerPos)) {
|
|
+ Bee.this.savedFlowerPos = null;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
return Bee.this.savedFlowerPos != null && !Bee.this.hasRestriction() && this.wantsToGoToKnownFlower() && Bee.this.isFlowerValid(Bee.this.savedFlowerPos) && !Bee.this.closerThan(Bee.this.savedFlowerPos, 2);
|
|
}
|
|
|
|
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 de51ce9875e12961e6e549e87d76f492d2f19787..8c9470bc0dd2d1a5bfbdc4921f4063bdfccd6924 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/animal/Cat.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/animal/Cat.java
|
|
@@ -364,7 +364,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 490472bb618e9ac07da5883a71dff8920525b1e2..28b4be002978f26a3ff9901e9d3dbe860a7d7665 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java
|
|
@@ -341,9 +341,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
|
|
}
|
|
|
|
@Override
|
|
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 759ecd79534a7706f7d4a63eb9dacbefcfe54674..0344b1f77f23274c2932b5dce01b0ea6887078cf 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java
|
|
@@ -303,8 +303,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 e6f75a9cac46c8e3ddba664a9d5b27b665a94cb4..8e348099d6b3eb4510405d76453d70e7cadeebf6 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java
|
|
@@ -295,9 +295,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(damagesource2, 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 eb0351aa12eebcefab1d1d14641fc3c60cbbcab8..ff4d2676a0833977dd8406d49dc0b5035c468b2d 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java
|
|
@@ -52,7 +52,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
|
|
@@ -126,13 +126,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();
|
|
@@ -186,11 +184,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) {
|
|
@@ -217,13 +215,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
|
|
@@ -529,14 +528,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 4ce3e69970dd9eb251d0538a2d233ca30e9e5e47..acb7545a3346758c7a598b104ea7ae43ce4263d2 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java
|
|
@@ -63,7 +63,7 @@ public class PrimedTnt extends Entity implements TraceableEntity {
|
|
|
|
@Override
|
|
public void tick() {
|
|
- if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().spigotConfig.currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot
|
|
+ if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().getCurrentWorldData().currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot // Folia - region threading
|
|
if (!this.isNoGravity()) {
|
|
this.setDeltaMovement(this.getDeltaMovement().add(0.0D, -0.04D, 0.0D));
|
|
}
|
|
@@ -105,7 +105,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/Vex.java b/src/main/java/net/minecraft/world/entity/monster/Vex.java
|
|
index 90e577b1a89b02c38daff2845a63dafe5ed929e1..150fb1fc4d70330a4c56ff5847792a1eb84af633 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/monster/Vex.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/monster/Vex.java
|
|
@@ -359,7 +359,7 @@ public class Vex extends Monster implements TraceableEntity {
|
|
public void tick() {
|
|
BlockPos blockposition = Vex.this.getBoundOrigin();
|
|
|
|
- if (blockposition == null) {
|
|
+ if (blockposition == null || !io.papermc.paper.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)Vex.this.level(), blockposition)) { // Folia - region threading
|
|
blockposition = Vex.this.blockPosition();
|
|
}
|
|
|
|
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 5fdad1600cc7a7c22d1d9a58b6b2dda605521b97..e2b9a18fd4a573aa2b3299a2e19afc07cc747366 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/monster/Zombie.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/monster/Zombie.java
|
|
@@ -95,7 +95,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) {
|
|
@@ -218,10 +218,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();
|
|
}
|
|
@@ -238,7 +235,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
|
|
@@ -277,7 +274,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 94396ad1a3c280787d36c6c18256d10340ace488..8fb13d9ed74e254522e6ee7196a6c72ccc8092b7 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java
|
|
@@ -73,7 +73,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);
|
|
@@ -148,10 +148,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) {
|
|
@@ -160,7 +157,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 5a591c439c5cef6b7e7e6f836ab813cb4f29b08c..ce728f062794e239d1dfdf842d7d0c725f77fba7 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java
|
|
@@ -212,10 +212,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 cbe2a37f74f4fb2abd0b3297699e54335aaed64f..4e9ccc518f37755e86687653f7724240db754682 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/npc/Villager.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java
|
|
@@ -203,7 +203,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
|
|
@@ -729,6 +729,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);
|
|
@@ -737,6 +739,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 8385eb1d60f377da94e3178ab506feefb43563fd..2f57e5025d0a0e720f49da1e5231a7d98495d13e 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java
|
|
@@ -32,16 +32,14 @@ public class WanderingTraderSpawner implements CustomSpawner {
|
|
private static final int SPAWN_CHANCE_INCREASE = 25;
|
|
private static final int SPAWN_ONE_IN_X_CHANCE = 10;
|
|
private static final int NUMBER_OF_SPAWN_ATTEMPTS = 10;
|
|
- private final RandomSource random = RandomSource.create();
|
|
+ private final RandomSource random = new net.minecraft.world.entity.Entity.RandomRandomSource(); // Folia - region threading
|
|
private final ServerLevelData serverLevelData;
|
|
- private int tickDelay;
|
|
- private int spawnDelay;
|
|
- private int spawnChance;
|
|
+ // Folia - region threading
|
|
|
|
public WanderingTraderSpawner(ServerLevelData properties) {
|
|
this.serverLevelData = properties;
|
|
// Paper start
|
|
- this.tickDelay = Integer.MIN_VALUE;
|
|
+ //this.tickDelay = Integer.MIN_VALUE; // Folia - region threading - moved to regionisedworlddata
|
|
//this.spawnDelay = properties.getWanderingTraderSpawnDelay(); // Paper - This value is read from the world file only for the first spawn, after which vanilla uses a hardcoded value
|
|
//this.spawnChance = properties.getWanderingTraderSpawnChance(); // Paper - This value is read from the world file only for the first spawn, after which vanilla uses a hardcoded value
|
|
//if (this.spawnDelay == 0 && this.spawnChance == 0) {
|
|
@@ -56,36 +54,37 @@ public class WanderingTraderSpawner implements CustomSpawner {
|
|
|
|
@Override
|
|
public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) {
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
// Paper start
|
|
- if (this.tickDelay == Integer.MIN_VALUE) {
|
|
- this.tickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
|
|
- this.spawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength;
|
|
- this.spawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin;
|
|
+ if (worldData.wanderingTraderTickDelay == Integer.MIN_VALUE) { // Folia - region threading
|
|
+ worldData.wanderingTraderTickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading
|
|
+ worldData.wanderingTraderSpawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading
|
|
+ worldData.wanderingTraderSpawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading
|
|
}
|
|
if (!world.getGameRules().getBoolean(GameRules.RULE_DO_TRADER_SPAWNING)) {
|
|
return 0;
|
|
- } else if (this.tickDelay - 1 > 0) {
|
|
- this.tickDelay = this.tickDelay - 1;
|
|
+ } else if (worldData.wanderingTraderTickDelay - 1 > 0) { // Folia - region threading
|
|
+ worldData.wanderingTraderTickDelay = worldData.wanderingTraderTickDelay - 1; // Folia - region threading
|
|
return 0;
|
|
} else {
|
|
- this.tickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
|
|
- this.spawnDelay = this.spawnDelay - world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
|
|
+ worldData.wanderingTraderTickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading
|
|
+ worldData.wanderingTraderSpawnDelay = worldData.wanderingTraderSpawnDelay - world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading
|
|
//this.serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways
|
|
- if (this.spawnDelay > 0) {
|
|
+ if (worldData.wanderingTraderSpawnDelay > 0) { // Folia - region threading
|
|
return 0;
|
|
} else {
|
|
- this.spawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength;
|
|
+ worldData.wanderingTraderSpawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading
|
|
if (!world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) {
|
|
return 0;
|
|
} else {
|
|
- int i = this.spawnChance;
|
|
+ int i = worldData.wanderingTraderSpawnChance; // Folia - region threading
|
|
|
|
// this.serverLevelData.setWanderingTraderSpawnChance(this.spawnChance); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways
|
|
- this.spawnChance = Mth.clamp(i + world.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax);
|
|
+ worldData.wanderingTraderSpawnChance = Mth.clamp(i + world.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); // Folia - region threading
|
|
if (this.random.nextInt(100) > i) {
|
|
return 0;
|
|
} else if (this.spawn(world)) {
|
|
- this.spawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin;
|
|
+ worldData.wanderingTraderSpawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading
|
|
// Paper end
|
|
return 1;
|
|
} else {
|
|
@@ -97,7 +96,7 @@ public class WanderingTraderSpawner implements CustomSpawner {
|
|
}
|
|
|
|
private boolean spawn(ServerLevel world) {
|
|
- ServerPlayer entityplayer = world.getRandomPlayer();
|
|
+ ServerPlayer entityplayer = world.getRandomLocalPlayer(); // Folia - region threading
|
|
|
|
if (entityplayer == null) {
|
|
return true;
|
|
@@ -127,7 +126,7 @@ public class WanderingTraderSpawner implements CustomSpawner {
|
|
this.tryToSpawnLlamaFor(world, entityvillagertrader, 4);
|
|
}
|
|
|
|
- this.serverLevelData.setWanderingTraderId(entityvillagertrader.getUUID());
|
|
+ //this.serverLevelData.setWanderingTraderId(entityvillagertrader.getUUID()); // Folia - region threading - doesn't appear to be used anywhere, so avoid the race condition here...
|
|
// entityvillagertrader.setDespawnDelay(48000); // CraftBukkit - moved to EntityVillagerTrader constructor. This lets the value be modified by plugins on CreatureSpawnEvent
|
|
entityvillagertrader.setWanderTarget(blockposition1);
|
|
entityvillagertrader.restrictTo(blockposition1, 16);
|
|
diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java
|
|
index 481c3e321cfc0f20bb1c4c6942b8bdbd23c06339..51c31ac3d54996a6e2d870d8215546fdcf5ee506 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/player/Player.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/player/Player.java
|
|
@@ -1508,6 +1508,14 @@ public abstract class Player extends LivingEntity {
|
|
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ @Override
|
|
+ protected void preRemove(RemovalReason reason) {
|
|
+ super.preRemove(reason);
|
|
+ this.fishing = null;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
public boolean isLocalPlayer() {
|
|
return false;
|
|
}
|
|
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 6c176933967f6ee98da3026f16a10efe4c3842fe..1b919bda12fb6c4bbad8cd477dbcbe520655b346 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.level(), 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 6c9a8f062f989db022154155e8a05b334a0510da..77f55bcb120295fa11140681f63175d37d8a78e2 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.level(), 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 bbdb82b319480b103df463cce3c1b8e3dd5857ec..d345e3163694588da5318e7ecd5c3d7cbbc24b32 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 b2f08889139dc447f7071f1c81456035bf8de31e..619bedc55d61f83dbc5adcf76a739cf9e6a1c55c 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 a2093158e57d5f43c4afa66386481b82b3c4c3c4..762376c2ab61200681fd5e5732337530285e03fb 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java
|
|
@@ -104,7 +104,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);
|
|
@@ -116,6 +116,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();
|
|
|
|
@@ -266,6 +267,11 @@ public class FishingHook extends Projectile {
|
|
}
|
|
|
|
private boolean shouldStopFishing(net.minecraft.world.entity.player.Player player) {
|
|
+ // Folia start - region threading
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(player)) {
|
|
+ return true;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
ItemStack itemstack = player.getMainHandItem();
|
|
ItemStack itemstack1 = player.getOffhandItem();
|
|
boolean flag = itemstack.is(Items.FISHING_ROD);
|
|
@@ -605,10 +611,18 @@ public class FishingHook extends Projectile {
|
|
|
|
@Override
|
|
public void remove(Entity.RemovalReason reason) {
|
|
- this.updateOwnerInfo((FishingHook) null);
|
|
+ // Folia - region threading - move into preRemove
|
|
super.remove(reason);
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ @Override
|
|
+ protected void preRemove(RemovalReason reason) {
|
|
+ super.preRemove(reason);
|
|
+ this.updateOwnerInfo((FishingHook) null);
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
@Override
|
|
public void onClientRemoval() {
|
|
this.updateOwnerInfo((FishingHook) null);
|
|
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 0bbe853f7df93f9dcd2b21d762939f8b6be069aa..4f4d6dedb99feac995e9b5bdf1d5c9f1332d61be 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java
|
|
@@ -29,6 +29,11 @@ public class LlamaSpit 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.level(), this.position(), this.getDeltaMovement(), 1)) {
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading - make sure entities do not move into regions they do not own
|
|
Vec3 vec3d = this.getDeltaMovement();
|
|
HitResult movingobjectposition = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity);
|
|
|
|
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 a90317100d32974e481e14476843f66997a2cf3a..0c8f90a904c01105ba5fa6a8037150697bc2621e 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
|
|
@@ -69,9 +69,20 @@ public abstract class Projectile extends Entity implements TraceableEntity {
|
|
}
|
|
// Paper end
|
|
|
|
+ // 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()) {
|
|
this.refreshProjectileSource(false); // Paper
|
|
return this.cachedOwner;
|
|
@@ -296,6 +307,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 9d43c8520953d6fe0d0948f9dbe14e0650ee01c2..c7713583930240512cd94f2a6bd1ff819ca6cd48 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
|
|
this.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 ab777952bda1651796ed41e8a7fc6621f27db9aa..6b9365eba3339578ee2984605240b74db2b8a6c0 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.level(), 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.getHitResultOnMoveVector(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 f5db60cbecbe69941873e064315931089fe0e48a..39bb4ec91e096cf84221a3bddbf7425023f0f609 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java
|
|
@@ -42,6 +42,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.level() != 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.serverLevel();
|
|
+
|
|
+ // 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);
|
|
@@ -51,6 +107,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.level(), this.position());
|
|
+ }
|
|
+ this.discard();
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
Entity entity = this.getOwner();
|
|
|
|
if (entity instanceof ServerPlayer) {
|
|
@@ -82,9 +152,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
|
|
}
|
|
@@ -110,6 +180,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 eaa2943b667967f93f28d9d794d702fdaeb670ec..8fc22de1aa17cd8cb52d3804533d56cbb0e6bfeb 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/raid/Raid.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/raid/Raid.java
|
|
@@ -107,6 +107,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();
|
|
@@ -215,7 +222,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
|
|
};
|
|
}
|
|
|
|
@@ -531,7 +538,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 cdbc925ef61b8b439415f0a89368227890bcecb2..6ad770b8ea068638c31a3a04fbbd84ddd294bb3c 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 31831811ce16265e9828fa34d9e67d8ac195d723..287024eaee26390a469162d63045a7cb1bfc20cf 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/raid/Raids.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/raid/Raids.java
|
|
@@ -29,9 +29,9 @@ 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 static SavedData.Factory<Raids> factory(ServerLevel world) {
|
|
@@ -44,7 +44,7 @@ public class Raids extends SavedData {
|
|
|
|
public Raids(ServerLevel world) {
|
|
this.level = world;
|
|
- this.nextAvailableID = 1;
|
|
+ this.nextAvailableID.set(1); // Folia - make raids thread-safe
|
|
this.setDirty();
|
|
}
|
|
|
|
@@ -52,12 +52,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();
|
|
@@ -71,14 +85,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;
|
|
}
|
|
|
|
@@ -91,7 +108,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();
|
|
@@ -171,7 +188,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);
|
|
|
|
@@ -187,7 +204,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();
|
|
@@ -209,7 +226,7 @@ public class Raids extends SavedData {
|
|
}
|
|
|
|
private int getUniqueId() {
|
|
- return ++this.nextAvailableID;
|
|
+ return this.nextAvailableID.incrementAndGet(); // Folia - make raids thread-safe
|
|
}
|
|
|
|
@Nullable
|
|
@@ -220,6 +237,11 @@ public class Raids extends SavedData {
|
|
|
|
while (iterator.hasNext()) {
|
|
Raid raid1 = (Raid) iterator.next();
|
|
+ // Folia start - make raids thread-safe
|
|
+ if (!raid1.ownsRaid()) {
|
|
+ continue;
|
|
+ }
|
|
+ // Folia end - make raids thread-safe
|
|
double d1 = raid1.getCenter().distSqr(pos);
|
|
|
|
if (raid1.isActive() && d1 < d0) {
|
|
diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java
|
|
index 48b4fe75a7f881e7713885d79d4ef5ec7b574a2d..1e1adbb68596bd8351ec77da67e6fd8139734b61 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java
|
|
@@ -150,5 +150,11 @@ public class MinecartCommandBlock extends AbstractMinecart {
|
|
return (org.bukkit.craftbukkit.entity.CraftMinecartCommand) MinecartCommandBlock.this.getBukkitEntity();
|
|
}
|
|
// CraftBukkit end
|
|
+ // Folia start
|
|
+ @Override
|
|
+ public void threadCheck() {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(MinecartCommandBlock.this, "Asynchronous sendSystemMessage to a command block");
|
|
+ }
|
|
+ // Folia end
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java
|
|
index fc35cfc9d045f3e5b6a50af1d0ba83b6e322091f..c87765a28f31673d547653b293c48856511bc693 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 42d87800a328f71c5127ce5599ca4c71cc9bb1cd..4b08175b71e7f92e9bb578f9f6c2c0efb2ce898f 100644
|
|
--- a/src/main/java/net/minecraft/world/item/ArmorItem.java
|
|
+++ b/src/main/java/net/minecraft/world/item/ArmorItem.java
|
|
@@ -69,7 +69,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 4697df75fdee2023c41260bed211e3e3d90d2b9b..1a0618422675cbe4ab55a03c85c7bc383efa5cee 100644
|
|
--- a/src/main/java/net/minecraft/world/item/ItemStack.java
|
|
+++ b/src/main/java/net/minecraft/world/item/ItemStack.java
|
|
@@ -357,31 +357,32 @@ public final class ItemStack {
|
|
CompoundTag oldData = this.getTagClone();
|
|
int oldCount = this.getCount();
|
|
ServerLevel world = (ServerLevel) context.getLevel();
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
|
|
if (!(item instanceof BucketItem/* || item 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 (item == Items.BONE_MEAL) {
|
|
- world.captureTreeGeneration = true;
|
|
+ worldData.captureTreeGeneration = true; // Folia - region threading
|
|
}
|
|
}
|
|
InteractionResult enuminteractionresult;
|
|
try {
|
|
enuminteractionresult = item.useOn(context);
|
|
} finally {
|
|
- world.captureBlockStates = false;
|
|
+ worldData.captureBlockStates = false; // Folia - region threading
|
|
}
|
|
CompoundTag newData = this.getTagClone();
|
|
int newCount = this.getCount();
|
|
this.setCount(oldCount);
|
|
this.setTagClone(oldData);
|
|
- if (enuminteractionresult.consumesAction() && world.captureTreeGeneration && world.capturedBlockStates.size() > 0) {
|
|
- world.captureTreeGeneration = false;
|
|
+ if (enuminteractionresult.consumesAction() && worldData.captureTreeGeneration && worldData.capturedBlockStates.size() > 0) { // Folia - region threading
|
|
+ world.getCurrentWorldData().captureTreeGeneration = false; // Folia - region threading
|
|
Location location = CraftLocation.toBukkit(blockposition, world.getWorld());
|
|
- 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;
|
|
@@ -410,13 +411,13 @@ 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()) {
|
|
InteractionHand enumhand = context.getHand();
|
|
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
|
|
@@ -427,13 +428,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();
|
|
@@ -448,7 +449,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());
|
|
}
|
|
|
|
@@ -535,8 +536,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 c368b437597edf7e165326727ae778a69c3fcc83..efa947cbd9b5f98ceb6a782a6fd39884e2f0fa8c 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<MapColor> 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 that it uses getChunkIfLoaded
|
|
|
|
- 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 a33395dc5a94d89b5ab273c7832813b6ff9ea3b7..310c8ebcf05cf5755db1baca8c585eb9a07c7f32 100644
|
|
--- a/src/main/java/net/minecraft/world/item/MinecartItem.java
|
|
+++ b/src/main/java/net/minecraft/world/item/MinecartItem.java
|
|
@@ -69,7 +69,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 ceeedbd88c56c08ec8b047c9ca2f14cc581e12ad..19cb22df8eb29d1708e3da2124de3b43378b575a 100644
|
|
--- a/src/main/java/net/minecraft/world/level/BaseCommandBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/BaseCommandBlock.java
|
|
@@ -20,7 +20,7 @@ import net.minecraft.world.phys.Vec3;
|
|
|
|
public abstract class BaseCommandBlock implements CommandSource {
|
|
|
|
- private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss");
|
|
+ private static final ThreadLocal<SimpleDateFormat> TIME_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("HH:mm:ss")); // Folia - region threading - SDF is not thread-safe
|
|
private static final Component DEFAULT_NAME = Component.literal("@");
|
|
private long lastExecution = -1L;
|
|
private boolean updateLastExecution = true;
|
|
@@ -111,6 +111,7 @@ public abstract class BaseCommandBlock implements CommandSource {
|
|
}
|
|
|
|
public boolean performCommand(Level world) {
|
|
+ if (true) return false; // Folia - region threading
|
|
if (!world.isClientSide && world.getGameTime() != this.lastExecution) {
|
|
if ("Searge".equalsIgnoreCase(this.command)) {
|
|
this.lastOutput = Component.literal("#itzlipofutzli");
|
|
@@ -169,11 +170,13 @@ public abstract class BaseCommandBlock implements CommandSource {
|
|
|
|
}
|
|
|
|
+ public void threadCheck() {} // Folia
|
|
+
|
|
@Override
|
|
public void sendSystemMessage(Component message) {
|
|
if (this.trackOutput) {
|
|
- org.spigotmc.AsyncCatcher.catchOp("sendSystemMessage to a command block"); // Paper
|
|
- SimpleDateFormat simpledateformat = BaseCommandBlock.TIME_FORMAT;
|
|
+ this.threadCheck(); // Folia
|
|
+ SimpleDateFormat simpledateformat = BaseCommandBlock.TIME_FORMAT.get(); // Folia - region threading - SDF is not thread-safe
|
|
Date date = new Date();
|
|
|
|
this.lastOutput = Component.literal("[" + simpledateformat.format(date) + "] ").append(message);
|
|
diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java
|
|
index aaa07fcd4b32fe0de88142ab30378327a01f1729..08884cc7d787887b470e90897838969d50dc4ad9 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) {
|
|
@@ -129,7 +135,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)) {
|
|
@@ -150,7 +156,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);
|
|
|
|
@@ -177,7 +183,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) {
|
|
@@ -190,7 +196,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) {
|
|
@@ -204,17 +210,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
|
|
@@ -249,7 +255,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);
|
|
}
|
|
@@ -275,8 +281,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 45243249a561440512ef2a620c60b02e159c80e2..f875a15db0d437a32062c626fb7e42fdcc6cbfde 100644
|
|
--- a/src/main/java/net/minecraft/world/level/Explosion.java
|
|
+++ b/src/main/java/net/minecraft/world/level/Explosion.java
|
|
@@ -566,7 +566,7 @@ public class Explosion {
|
|
continue;
|
|
}
|
|
|
|
- CraftEventFactory.entityDamage = this.source;
|
|
+ CraftEventFactory.entityDamageRT.set(this.source); // Folia - region threading
|
|
entity.lastDamageCancelled = false;
|
|
|
|
if (entity instanceof EnderDragon) {
|
|
@@ -582,7 +582,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;
|
|
}
|
|
@@ -852,17 +852,18 @@ public class Explosion {
|
|
if (!this.level.paperConfig().environment.optimizeExplosions) {
|
|
return this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise explosions
|
|
}
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading
|
|
CacheKey key = new CacheKey(this, entity.getBoundingBox());
|
|
- Float blockDensity = this.level.explosionDensityCache.get(key);
|
|
+ Float blockDensity = worldData.explosionDensityCache.get(key); // Folia - region threading
|
|
if (blockDensity == null) {
|
|
blockDensity = this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise explosions;
|
|
- this.level.explosionDensityCache.put(key, blockDensity);
|
|
+ worldData.explosionDensityCache.put(key, blockDensity); // Folia - region threading
|
|
}
|
|
|
|
return blockDensity;
|
|
}
|
|
|
|
- static class CacheKey {
|
|
+ public static class CacheKey { // Folia - region threading - public
|
|
private final Level world;
|
|
private final double posX, posY, posZ;
|
|
private final double minX, minY, minZ;
|
|
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
|
|
index 2354a0e5d15e9be633d9fe3a1a9feefe7b9b7782..2329d08c736ab04f640b0baef3a0d3dce6840a31 100644
|
|
--- a/src/main/java/net/minecraft/world/level/Level.java
|
|
+++ b/src/main/java/net/minecraft/world/level/Level.java
|
|
@@ -117,10 +117,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;
|
|
@@ -130,7 +130,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();
|
|
@@ -144,7 +144,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;
|
|
@@ -153,20 +153,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;
|
|
@@ -180,9 +170,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
|
|
@@ -210,6 +200,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<net.minecraft.server.level.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
|
|
@@ -253,7 +270,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
|
|
@@ -846,8 +863,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();
|
|
}
|
|
@@ -909,16 +926,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
|
|
@Override
|
|
public boolean setBlock(BlockPos pos, BlockState state, int flags, int maxUpdateDepth) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread((ServerLevel)this, pos, "Updating block asynchronously"); // Folia - region threading
|
|
+ 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);
|
|
@@ -935,10 +954,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
|
|
@@ -948,8 +967,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;
|
|
@@ -986,7 +1005,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 {
|
|
@@ -1036,7 +1055,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
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();
|
|
boolean cancelledUpdates = false; // Paper
|
|
- 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);
|
|
|
|
@@ -1050,7 +1069,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
|
|
@@ -1129,7 +1148,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
|
|
@@ -1154,11 +1173,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();
|
|
}
|
|
@@ -1249,7 +1291,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() {
|
|
@@ -1257,11 +1299,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
|
|
gameprofilerfiller.push("blockEntities");
|
|
this.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 = this.getCurrentWorldData(); // Folia - regionised ticking
|
|
+ regionizedWorldData.seTtickingBlockEntities(true); // Folia - regionised ticking
|
|
+ regionizedWorldData.pushPendingTickingBlockEntities(); // Folia - regionised ticking
|
|
+ List<TickingBlockEntity> blockEntityTickers = regionizedWorldData.getBlockEntityTickers(); // Folia - regionised ticking
|
|
this.timings.tileEntityPending.stopTiming(); // Spigot
|
|
|
|
this.timings.tileEntityTick.startTiming(); // Spigot
|
|
@@ -1270,9 +1311,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(this.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");
|
|
@@ -1289,19 +1329,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
|
|
|
|
this.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();
|
|
- this.spigotConfig.currentPrimedTnt = 0; // Spigot
|
|
+ regionizedWorldData.currentPrimedTnt = 0; // Spigot // Folia - region threading
|
|
}
|
|
|
|
public <T extends Entity> void guardEntityTick(Consumer<T> tickConsumer, T entity) {
|
|
@@ -1314,7 +1354,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ());
|
|
MinecraftServer.LOGGER.error(msg, throwable);
|
|
getCraftServer().getPluginManager().callEvent(new ServerExceptionEvent(new ServerInternalException(msg, throwable)));
|
|
- entity.discard();
|
|
+ if (!(entity instanceof net.minecraft.server.level.ServerPlayer)) entity.discard(); // Folia - properly disconnect players
|
|
+ if (entity instanceof net.minecraft.server.level.ServerPlayer player) player.connection.disconnect(net.minecraft.network.chat.Component.translatable("multiplayer.disconnect.generic"), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); // Folia - properly disconnect players
|
|
// Paper end
|
|
}
|
|
}
|
|
@@ -1407,9 +1448,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
|
|
@@ -1422,8 +1468,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
|
|
@@ -1503,6 +1549,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
|
|
@Override
|
|
public List<Entity> getEntities(@Nullable Entity except, AABB box, Predicate<? super Entity> predicate) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread((ServerLevel)this, box, "Cannot getEntities asynchronously"); // Folia - region threading
|
|
this.getProfiler().incrementCounter("getEntities");
|
|
List<Entity> list = Lists.newArrayList();
|
|
((ServerLevel)this).getEntityLookup().getEntities(except, box, list, predicate); // Paper - optimise this call
|
|
@@ -1522,6 +1569,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
}
|
|
|
|
public <T extends Entity> void getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate, List<? super T> result, int limit) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread((ServerLevel)this, box, "Cannot getEntities asynchronously"); // Folia - region threading
|
|
this.getProfiler().incrementCounter("getEntities");
|
|
// Paper start - optimise this call
|
|
//TODO use limit
|
|
@@ -1559,13 +1607,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;
|
|
@@ -1767,8 +1832,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
|
|
@@ -1799,7 +1863,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 12eaafdbd324fa36b3f46c3b644bc8117a4123ad..c8c358a2ce567567159039ed6a1ba804b5d3ee85 100644
|
|
--- a/src/main/java/net/minecraft/world/level/LevelReader.java
|
|
+++ b/src/main/java/net/minecraft/world/level/LevelReader.java
|
|
@@ -210,6 +210,25 @@ public interface LevelReader extends BlockAndTintGetter, CollisionGetter, Signal
|
|
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 3cdddda9c0618e95288b81b975d499c8dd30c05f..5caca2a34849189ea42d2699f6d8672e0d7251cb 100644
|
|
--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java
|
|
+++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
|
|
@@ -146,7 +146,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);
|
|
}
|
|
|
|
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/BedBlock.java b/src/main/java/net/minecraft/world/level/block/BedBlock.java
|
|
index d40500f9a807cab0b2fb6fa9032f33f4fb74c895..824d89c2c70c64f6b37155dcc2aa0f7bd34d0303 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/BedBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/BedBlock.java
|
|
@@ -357,7 +357,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock
|
|
|
|
world.setBlock(blockposition1, (BlockState) state.setValue(BedBlock.PART, BedPart.HEAD), 3);
|
|
// CraftBukkit start - SPIGOT-7315: Don't updated if we capture block states
|
|
- if (world.captureBlockStates) {
|
|
+ if (world.getCurrentWorldData().captureBlockStates) { // Folia - region threading
|
|
return;
|
|
}
|
|
// CraftBukkit end
|
|
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 d4cbff18adb62073a1dceb189043789620af6877..4ddc3484311f9c83a6f8aecb89188195ab281742 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/Block.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/Block.java
|
|
@@ -382,8 +382,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 0003fb51ae3a6575575e10b4c86719f3061e2577..362aab7977f636fa8a8548a01ab333da066fbc76 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/CactusBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/CactusBlock.java
|
|
@@ -115,9 +115,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 7700461b8cd0bde1bf6c0d5e4b73184bed1adc4e..479f571152ebb17315b294469658665b6a4e0c12 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java
|
|
@@ -95,9 +95,9 @@ public class CampfireBlock extends BaseEntityBlock implements SimpleWaterloggedB
|
|
public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) {
|
|
if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper
|
|
if ((Boolean) state.getValue(CampfireBlock.LIT) && entity instanceof LivingEntity && !EnchantmentHelper.hasFrostWalker((LivingEntity) entity)) {
|
|
- org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = CraftBlock.at(world, pos); // CraftBukkit
|
|
+ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // CraftBukkit // Folia - region threading
|
|
entity.hurt(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 c98a7d0cc2fcf993ab05dc9347bfb34a7e1bdde5..db3060021f9e024a6dba93498422d41e9662f68a 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 9b1e51c1d95da885c80c6d05000d83436b7bcfb4..5ff51a42415306907045452c9feff62afe02898e 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/DispenserBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/DispenserBlock.java
|
|
@@ -48,7 +48,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);
|
|
@@ -99,7 +99,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(Boolean.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 030b38d5d5d2578d6ef482a239ef58787efa3b08..c93a125445107d97db2a647d7b57b5f871d7e97a 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 17e9e2efc78cfe8577dbf4e1d6096543ad8b8ac4..37b0f51eb93a331f2d3b33deb4467f65c474b92c 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 745f33ce496a7ce8c788f24c093b37933a74148a..b81b43635dcf516492ec5edfa385efcdea4f7534 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 1b766045687e4dcded5cbcc50b746c55b9a34e22..a8270d65c22fa979ce53ffeeeadce8d61db397e1 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java
|
|
@@ -23,9 +23,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 302c5a6401facf192677b89cc0e9190bb35b1229..63a51676162d8e658cf10f63ff91d450850a4b86 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/MushroomBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/MushroomBlock.java
|
|
@@ -93,7 +93,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 cd943997f11f5ea5c600fdc6db96043fb0fa713c..7039b27f74b7898e7028cbd67184c6e9ba213e39 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java
|
|
@@ -141,9 +141,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 2b054439b7a763d5a3fbb5dbfe197cb9a9a3525c..f695d81a646307846abb490b484f166be2993382 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 c91535f6c0bbc870fad7e04b9d341783cfcbbd63..660ade166edf1750247e6a02134581de4225f338 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java
|
|
@@ -74,10 +74,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();
|
|
}
|
|
}
|
|
@@ -158,14 +158,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;
|
|
@@ -187,12 +187,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 53ac4e618fec3fe384d8a106c521f3eace0b5b35..2724793d0e2e455f81393775adc6ddceb2319fbc 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/SaplingBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/SaplingBlock.java
|
|
@@ -27,7 +27,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);
|
|
@@ -53,18 +53,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 = CraftLocation.toBukkit(pos, world.getWorld());
|
|
- 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 9bbb9f8e917288bb0d11661a1399a05631ebcce0..8d57dc0aa214e14690880f9a58234fff134cf1f1 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 34eb7ba1adb51e394bf46a6f643db3529626d9ec..c9c544f6db4a24d66e81d9ba2929a41507c5d4af 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 1aa0e921890d600c9274deb923da04e72b12bcc6..daca759793b4a22406fb9c75b5ac9814920d3ac0 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java
|
|
@@ -51,7 +51,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 f13943db6f2fb923c52dcf9e8bf7000041d0a362..9c3a271f98e723f1d8bf3badd3fca7a19fdc6d13 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
|
|
@@ -211,7 +211,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 370a25d2deb54f10a35ee24d9e7e92fbfde60edf..fb16bbfd8db86495eced3e9ff9f9c4267e2230cc 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
|
|
@@ -41,6 +41,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();
|
|
@@ -162,7 +168,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/CommandBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java
|
|
index 167f334eec90417eba05fcecec21435415771df7..3698bae85063baec031a61d24ef3286703a9d04c 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java
|
|
@@ -57,6 +57,13 @@ public class CommandBlockEntity extends BlockEntity {
|
|
return new CommandSourceStack(this, Vec3.atCenterOf(CommandBlockEntity.this.worldPosition), new Vec2(0.0F, enumdirection.toYRot()), this.getLevel(), 2, this.getName().getString(), this.getName(), this.getLevel().getServer(), (Entity) null);
|
|
}
|
|
|
|
+ // Folia start
|
|
+ @Override
|
|
+ public void threadCheck() {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread((ServerLevel) CommandBlockEntity.this.level, CommandBlockEntity.this.worldPosition, "Asynchronous sendSystemMessage to a command block");
|
|
+ }
|
|
+ // Folia end
|
|
+
|
|
@Override
|
|
public boolean isValid() {
|
|
return !CommandBlockEntity.this.isRemoved();
|
|
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 d4dcf7fe26474ae07374e7761d823bc5c8b54f97..72779080969fe9f058c19533ffc9dad5f9c47086 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
|
|
@@ -233,12 +233,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);
|
|
@@ -253,7 +252,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);
|
|
@@ -283,13 +282,14 @@ 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);
|
|
container.setChanged(); // original logic always marks source inv as changed even if no move happens.
|
|
movedItem.setCount(movedItemCount);
|
|
|
|
- if (!skipPullModeEventFire) {
|
|
+ if (!worldData.skipPullModeEventFire) { // Folia - region threading
|
|
movedItem = callPullMoveEvent(hopper, container, movedItem);
|
|
if (movedItem == null) { // cancelled
|
|
origItemStack.setCount(originalItemCount);
|
|
@@ -309,9 +309,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;
|
|
}
|
|
@@ -326,12 +326,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 io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(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);
|
|
@@ -347,6 +348,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);
|
|
|
|
@@ -354,7 +356,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
|
|
final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(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);
|
|
@@ -517,13 +519,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
|
|
// merge container isEmpty check and move logic into one loop
|
|
if (iinventory instanceof WorldlyContainer worldlyContainer) {
|
|
for (final int slot : worldlyContainer.getSlotsForFace(enumdirection)) {
|
|
@@ -723,9 +726,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
|
|
stack = stack.split(to.getMaxStackSize());
|
|
}
|
|
// Spigot end
|
|
- ignoreTileUpdates = true; // Paper
|
|
+ IGNORE_TILE_UPDATES.set(Boolean.TRUE); // Paper // Folia - region threading
|
|
to.setItem(slot, stack);
|
|
- ignoreTileUpdates = false; // Paper
|
|
+ IGNORE_TILE_UPDATES.set(Boolean.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 65112ec3a6ea1c27f032477720ae74395523012b..7f9022a271e29d0b47bef15359f1b2dc79de6402 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
|
|
@@ -43,9 +43,9 @@ public class SculkCatalystBlockEntity extends BlockEntity implements GameEventLi
|
|
// Paper end
|
|
|
|
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.catalystListener.getSculkSpreader().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 1ec80f9c901dff1c9f29befa5a8e3c3f6f37aaf7..53f70401fbed7f60507559a8e3d6dcd2400a1179 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
|
|
@@ -50,9 +50,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);
|
|
}
|
|
@@ -127,7 +130,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() {
|
|
@@ -175,8 +178,136 @@ 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;
|
|
+ }
|
|
+
|
|
+ Entity chosenEntity;
|
|
+ if (entity instanceof ThrownEnderpearl pearl) {
|
|
+ Entity owner = pearl.getOwner();
|
|
+
|
|
+ if (owner instanceof ServerPlayer player) {
|
|
+ CriteriaTriggers.ENTER_BLOCK.trigger(player, state);
|
|
+ }
|
|
+
|
|
+ if (owner != null) {
|
|
+ // vanilla behavior is to just break if the owner is riding anything
|
|
+ // it's not likely intentional that throwing a pearl while riding something is intended
|
|
+ // to teleport the vehicle, rather just the owner given the lack of getRootVehicle
|
|
+ owner.unRide();
|
|
+ chosenEntity = owner;
|
|
+ pearl.discard();
|
|
+ } else {
|
|
+ // see above for unRide()
|
|
+ pearl.unRide();
|
|
+ chosenEntity = pearl;
|
|
+ }
|
|
+ } else {
|
|
+ chosenEntity = entity.getRootVehicle();
|
|
+ }
|
|
+
|
|
+ // 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
|
|
+ chosenEntity.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
|
|
+ chosenEntity.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, blockEntity);
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
ServerLevel worldserver = (ServerLevel) world;
|
|
|
|
blockEntity.teleportCooldown = 100;
|
|
@@ -280,6 +411,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 1ef87580574919796dbba707f44a413ee5c5781b..a595abb43853cd4c3f5886a83527c6cbe4a3e8f7 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 d5c2a608e1b4c8099c96b33d9d758e968350a46d..1a4da589142c515e713d879095b105de4b913bd3 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java
|
|
@@ -320,7 +320,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 fa170cc1ce7011d201295b89718292d696c7fc24..744fa3a3a62ffb4b3d1e7831c93fd76dee29fdd0 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
|
|
@@ -59,6 +59,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;
|
|
@@ -412,6 +419,7 @@ public class LevelChunk extends ChunkAccess {
|
|
|
|
@Nullable
|
|
public BlockState setBlockState(BlockPos blockposition, BlockState iblockdata, boolean flag, boolean doPlace) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this.level, blockposition, "Updating block asynchronously"); // Folia - region threading
|
|
// CraftBukkit end
|
|
int i = blockposition.getY();
|
|
LevelChunkSection chunksection = this.getSection(this.getSectionIndex(i));
|
|
@@ -462,7 +470,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);
|
|
}
|
|
|
|
@@ -509,7 +517,7 @@ public class LevelChunk extends ChunkAccess {
|
|
@Nullable
|
|
public BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) {
|
|
// CraftBukkit start
|
|
- BlockEntity tileentity = this.level.capturedTileEntities.get(pos);
|
|
+ BlockEntity tileentity = this.level.getCurrentWorldData().capturedTileEntities.get(pos); // Folia - region threading
|
|
if (tileentity == null) {
|
|
tileentity = (BlockEntity) this.blockEntities.get(pos);
|
|
}
|
|
@@ -796,13 +804,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(bukkitChunk));
|
|
@@ -852,7 +860,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;
|
|
}
|
|
@@ -1118,6 +1126,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();
|
|
@@ -1154,6 +1169,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 1379084a80ce25644f13736b4a5ee5fabbd9ec1f..97533c4a31ab6137072b79dc4377fd602fef9ea8 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
|
|
@@ -646,7 +646,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 c1ff2e15bc5da1a642872ac0fdcdc457e8abb063..f73552b09913b290a7c79db3d6f064ab28553f05 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
|
|
@@ -76,7 +76,7 @@ public class EndDragonFight {
|
|
private static final Component DEFAULT_BOSS_EVENT_NAME = Component.translatable("entity.minecraft.ender_dragon"); // Paper
|
|
public final ServerBossEvent dragonEvent;
|
|
public final ServerLevel level;
|
|
- private final BlockPos origin;
|
|
+ public final BlockPos origin; // Folia - region threading
|
|
public final ObjectArrayList<Integer> gateways;
|
|
private final BlockPattern exitPortalPattern;
|
|
private int ticksSinceDragonSeen;
|
|
@@ -152,7 +152,7 @@ public class EndDragonFight {
|
|
|
|
if (!this.dragonEvent.getPlayers().isEmpty()) {
|
|
this.level.getChunkSource().addRegionTicket(TicketType.DRAGON, new ChunkPos(0, 0), 9, Unit.INSTANCE);
|
|
- boolean flag = this.isArenaLoaded();
|
|
+ boolean flag = this.isArenaLoaded(); if (!flag) { return; } // Folia - region threading - don't tick if we don't own the entire region
|
|
|
|
if (this.needsStateScanning && flag) {
|
|
this.scanState();
|
|
@@ -201,6 +201,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;
|
|
@@ -347,9 +353,8 @@ public class EndDragonFight {
|
|
|
|
for (int i = -8 + chunkcoordintpair.x; i <= 8 + chunkcoordintpair.x; ++i) {
|
|
for (int j = 8 + chunkcoordintpair.z; j <= 8 + chunkcoordintpair.z; ++j) {
|
|
- ChunkAccess ichunkaccess = this.level.getChunk(i, j, ChunkStatus.FULL, false);
|
|
-
|
|
- if (!(ichunkaccess instanceof LevelChunk)) {
|
|
+ ChunkAccess ichunkaccess = this.level.getChunkIfLoaded(i, j); // Folia - region threading
|
|
+ if (!(ichunkaccess instanceof LevelChunk) || !io.papermc.paper.util.TickThread.isTickThreadFor(this.level, i, j, this.level.regioniser.regionSectionChunkSize)) { // Folia - region threading
|
|
return false;
|
|
}
|
|
|
|
@@ -536,6 +541,11 @@ public class EndDragonFight {
|
|
}
|
|
|
|
public void onCrystalDestroyed(EndCrystal enderCrystal, DamageSource source) {
|
|
+ // Folia start - region threading
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(this.level, this.origin)) {
|
|
+ return;
|
|
+ }
|
|
+ // Folia end - region threading
|
|
if (this.respawnStage != null && this.respawnCrystals.contains(enderCrystal)) {
|
|
EndDragonFight.LOGGER.debug("Aborting respawn sequence");
|
|
this.respawnStage = null;
|
|
@@ -564,7 +574,7 @@ public class EndDragonFight {
|
|
|
|
public boolean tryRespawn(@Nullable BlockPos placedEndCrystalPos) { // placedEndCrystalPos is null if the tryRespawn() call was not caused by a placed end crystal
|
|
// Paper end
|
|
- if (this.dragonKilled && this.respawnStage == null) {
|
|
+ if (this.dragonKilled && this.respawnStage == null && io.papermc.paper.util.TickThread.isTickThreadFor(this.level, this.origin)) { // Folia - region threading
|
|
BlockPos blockposition = this.portalLocation;
|
|
|
|
if (blockposition == null) {
|
|
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 dfeb3e336e06ef01f5401a362755030db942bb07..a127b6cbaab211d324d42b3bddcd6ebd84c250e3 100644
|
|
--- a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java
|
|
+++ b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java
|
|
@@ -22,7 +22,7 @@ import net.minecraft.world.level.material.FluidState;
|
|
|
|
public class PhantomSpawner implements CustomSpawner {
|
|
|
|
- private int nextTick;
|
|
+ //private int nextTick; // Folia - region threading
|
|
|
|
public PhantomSpawner() {}
|
|
|
|
@@ -40,20 +40,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()) {
|
|
ServerPlayer entityplayer = (ServerPlayer) iterator.next();
|
|
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 c322827a910b6244804893784617784002758aee..22ceea2d8ff11dabd0406fb7ab50e5d02bc207f5 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
|
|
@@ -28,7 +28,7 @@ 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 org.bukkit.event.world.AsyncStructureGenerateEvent.Cause generationEventCause = org.bukkit.event.world.AsyncStructureGenerateEvent.Cause.WORLD_GENERATION; // CraftBukkit
|
|
@@ -36,7 +36,7 @@ public final class StructureStart {
|
|
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;
|
|
}
|
|
|
|
@@ -127,7 +127,7 @@ public final class StructureStart {
|
|
nbttagcompound.putString("id", context.registryAccess().registryOrThrow(Registries.STRUCTURE).getKey(this.structure).toString());
|
|
nbttagcompound.putInt("ChunkX", chunkPos.x);
|
|
nbttagcompound.putInt("ChunkZ", chunkPos.z);
|
|
- nbttagcompound.putInt("references", this.references);
|
|
+ nbttagcompound.putInt("references", this.references.get()); // Folia - region threading
|
|
nbttagcompound.put("Children", this.pieceContainer.save(context));
|
|
return nbttagcompound;
|
|
} else {
|
|
@@ -145,15 +145,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 42212d4533ce25d1cfcf4c58f1fc88791d546cff..fa33c8e40bad9411e1a03899d2c370806f6eba24 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 598dc0d3a2b9387e76d7e4e19e54c4573a24bc54..8a139bcfc9c3de5793b1b590be6cafaae01c86e1 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 f9e3a10f91b7b22604cf13d9e7ebdca460aa39ca..92fe26539027ee57b134c1cf3c0686d7182b6fea 100644
|
|
--- a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java
|
|
+++ b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java
|
|
@@ -13,7 +13,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);
|
|
|
|
@@ -31,6 +31,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);
|
|
@@ -41,7 +42,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 3eca0b1d1407770d3986e0f09c49bc86804b604e..d76df0d600d7f17ac01ce131b85ce9289989995a 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
|
|
@@ -32,17 +32,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 e4c4948e076cd64686dfd16ae0568fafc1437140..ba531fcd90df266d8d0c2de907fefe994949ae0c 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
|
|
@@ -192,7 +192,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;
|
|
|
|
@@ -249,7 +249,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);
|
|
@@ -260,11 +260,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);
|
|
|
|
@@ -373,7 +374,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);
|
|
}
|
|
@@ -432,14 +433,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()) {
|
|
@@ -447,15 +448,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) {
|
|
@@ -467,7 +469,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;
|
|
@@ -476,7 +478,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;
|
|
@@ -497,7 +499,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()) {
|
|
@@ -519,12 +521,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) {
|
|
@@ -535,12 +537,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;
|
|
@@ -575,7 +577,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;
|
|
}
|
|
|
|
@@ -701,11 +703,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 f921f55e815a4da01828e025881a7a03591c3978..844226e119a9277741972cddf340bceb59b6cda7 100644
|
|
--- a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java
|
|
+++ b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java
|
|
@@ -35,6 +35,7 @@ public class DimensionDataStorage {
|
|
}
|
|
|
|
public <T extends SavedData> T computeIfAbsent(SavedData.Factory<T> type, String id) {
|
|
+ synchronized (this.cache) { // Folia - make map data thread-safe
|
|
T savedData = this.get(type, id);
|
|
if (savedData != null) {
|
|
return savedData;
|
|
@@ -43,10 +44,12 @@ public class DimensionDataStorage {
|
|
this.set(id, savedData2);
|
|
return savedData2;
|
|
}
|
|
+ } // Folia - make map data thread-safe
|
|
}
|
|
|
|
@Nullable
|
|
public <T extends SavedData> T get(SavedData.Factory type, 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(type.deserializer(), type.type(), id);
|
|
@@ -54,6 +57,7 @@ public class DimensionDataStorage {
|
|
}
|
|
|
|
return (T)savedData;
|
|
+ } // Folia - make map data thread-safe
|
|
}
|
|
|
|
@Nullable
|
|
@@ -72,7 +76,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, DataFixTypes dataFixTypes, int currentSaveVersion) 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 bbc51ebf0dc0801ace9d5e7875fb2fe100b286bc..541915675dd3ad832992360283e3ba1b3cc62e27 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
|
|
@@ -311,7 +311,7 @@ public final class CraftServer implements Server {
|
|
private final CraftPotionBrewer potionBrewer = new CraftPotionBrewer(); // Paper
|
|
|
|
// Paper start - Folia region threading API
|
|
- private final io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler();
|
|
+ private final io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler(); // Folia - region threading
|
|
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();
|
|
|
|
@@ -379,6 +379,12 @@ public final class CraftServer implements Server {
|
|
return io.papermc.paper.util.TickThread.isTickThreadFor(((org.bukkit.craftbukkit.entity.CraftEntity) entity).getHandleRaw());
|
|
}
|
|
// Paper end - Folia reagion threading API
|
|
+ // Folia start - region threading API
|
|
+ @Override
|
|
+ public boolean isGlobalTickThread() {
|
|
+ return io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread();
|
|
+ }
|
|
+ // Folia end - region threading API
|
|
|
|
static {
|
|
ConfigurationSerialization.registerClass(CraftOfflinePlayer.class);
|
|
@@ -963,6 +969,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;
|
|
|
|
@@ -982,12 +991,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 || sender instanceof io.papermc.paper.commands.FeedbackForwardingSender) {
|
|
+ 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) {
|
|
Preconditions.checkArgument(sender != null, "sender cannot be null");
|
|
Preconditions.checkArgument(commandLine != null, "commandLine cannot be null");
|
|
org.spigotmc.AsyncCatcher.catchOp("Command Dispatched Async: " + commandLine); // Spigot // Paper - Include command in error message
|
|
|
|
+ // 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 || sender instanceof io.papermc.paper.commands.FeedbackForwardingSender) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Dispatching command async");
|
|
+ } else {
|
|
+ // huh?
|
|
+ throw new UnsupportedOperationException("Dispatching command for " + sender);
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
if (this.commandMap.dispatch(sender, commandLine)) {
|
|
return true;
|
|
}
|
|
@@ -3171,7 +3212,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 c3060d1d4d0caf369c6ab516cb424f45eb851019..bcbf7ba7c687ecaa530d4de3af45ebeb80dd15c5 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
|
|
@@ -184,7 +184,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
|
|
@@ -240,7 +240,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
// Paper start - per world spawn limits
|
|
for (SpawnCategory spawnCategory : SpawnCategory.values()) {
|
|
if (CraftSpawnCategory.isValidForLimits(spawnCategory)) {
|
|
- setSpawnLimit(spawnCategory, this.world.paperConfig().entities.spawning.spawnLimits.getInt(CraftSpawnCategory.toNMS(spawnCategory)));
|
|
+ this.spawnCategoryLimit.put(spawnCategory, this.world.paperConfig().entities.spawning.spawnLimits.getInt(CraftSpawnCategory.toNMS(spawnCategory))); // Folia - region threading
|
|
}
|
|
}
|
|
// Paper end
|
|
@@ -327,6 +327,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 = this.world.getChunkSource().getChunkAtIfLoadedImmediately(x, z);
|
|
@@ -350,7 +351,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(() -> this.world.getChunkSource().addRegionTicket(TicketType.PLUGIN, new ChunkPos(x, z), 0, Unit.INSTANCE)); // Paper
|
|
+ this.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
|
|
|
|
@@ -369,10 +370,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) {
|
|
@@ -426,7 +427,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
}
|
|
|
|
private boolean unloadChunk0(int x, int z, boolean save) {
|
|
- org.spigotmc.AsyncCatcher.catchOp("chunk unload"); // Spigot
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, x, z, "Cannot unload chunk asynchronously"); // Folia - region threading
|
|
if (!this.isChunkLoaded(x, z)) {
|
|
return true;
|
|
}
|
|
@@ -441,7 +442,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public boolean regenerateChunk(int x, int z) {
|
|
- org.spigotmc.AsyncCatcher.catchOp("chunk regenerate"); // Spigot
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, x, z, "Cannot regenerate chunk asynchronously"); // Folia - region threading
|
|
warnUnsafeChunk("regenerating a faraway chunk", x, z); // Paper
|
|
// Paper start - implement regenerateChunk method
|
|
final ServerLevel serverLevel = this.world;
|
|
@@ -502,6 +503,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public boolean refreshChunk(int x, int z) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, x, z, "Cannot refresh chunk asynchronously"); // Folia - region threading
|
|
ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
|
|
if (playerChunk == null) return false;
|
|
|
|
@@ -537,7 +539,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public boolean loadChunk(int x, int z, boolean generate) {
|
|
- org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this.getHandle(), x, z, "May not sync load chunks asynchronously"); // Folia - region threading
|
|
warnUnsafeChunk("loading a faraway chunk", x, z); // Paper
|
|
// Paper start - Optimize this method
|
|
ChunkPos chunkPos = new ChunkPos(x, z);
|
|
@@ -609,7 +611,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;
|
|
}
|
|
|
|
@@ -800,13 +802,15 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public boolean generateTree(Location loc, TreeType type, BlockChangeDelegate delegate) {
|
|
- this.world.captureTreeGeneration = true;
|
|
- this.world.captureBlockStates = true;
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, loc.getX(), loc.getZ(), "Cannot generate tree asynchronously"); // Folia - region threading
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
+ worldData.captureTreeGeneration = true; // Folia - region threading
|
|
+ worldData.captureBlockStates = true; // Folia - region threading
|
|
boolean grownTree = this.generateTree(loc, type);
|
|
- this.world.captureBlockStates = false;
|
|
- this.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 : this.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();
|
|
@@ -814,10 +818,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);
|
|
}
|
|
- this.world.capturedBlockStates.clear();
|
|
+ worldData.capturedBlockStates.clear(); // Folia - region threading
|
|
return true;
|
|
} else {
|
|
- this.world.capturedBlockStates.clear();
|
|
+ worldData.capturedBlockStates.clear(); // Folia - region threading
|
|
return false;
|
|
}
|
|
}
|
|
@@ -851,6 +855,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public void setTime(long time) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify time off of the global region"); // Folia - region threading
|
|
long margin = (time - this.getFullTime()) % 24000;
|
|
if (margin < 0) margin += 24000;
|
|
this.setFullTime(this.getFullTime() + margin);
|
|
@@ -863,6 +868,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public void setFullTime(long time) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify time off of the global region"); // Folia - region threading
|
|
// Notify anyone who's listening
|
|
TimeSkipEvent event = new TimeSkipEvent(this, TimeSkipEvent.SkipReason.CUSTOM, time - this.world.getDayTime());
|
|
this.server.getPluginManager().callEvent(event);
|
|
@@ -890,7 +896,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public long getGameTime() {
|
|
- return this.world.levelData.getGameTime();
|
|
+ return this.getHandle().getGameTime(); // Folia - region threading
|
|
}
|
|
|
|
@Override
|
|
@@ -910,11 +916,13 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public boolean createExplosion(double x, double y, double z, float power, boolean setFire, boolean breakBlocks, Entity source) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, x, z, "Cannot create explosion asynchronously");
|
|
return !this.world.explode(source == null ? null : ((CraftEntity) source).getHandle(), x, y, z, power, setFire, breakBlocks ? net.minecraft.world.level.Level.ExplosionInteraction.MOB : net.minecraft.world.level.Level.ExplosionInteraction.NONE).wasCanceled;
|
|
}
|
|
// Paper start
|
|
@Override
|
|
public boolean createExplosion(Entity source, Location loc, float power, boolean setFire, boolean breakBlocks) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, loc.getX(), loc.getZ(), "Cannot create explosion asynchronously");
|
|
return !world.explode(source != null ? ((org.bukkit.craftbukkit.entity.CraftEntity) source).getHandle() : null, loc.getX(), loc.getY(), loc.getZ(), power, setFire, breakBlocks ? net.minecraft.world.level.Level.ExplosionInteraction.MOB : net.minecraft.world.level.Level.ExplosionInteraction.NONE).wasCanceled;
|
|
}
|
|
// Paper end
|
|
@@ -984,6 +992,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public int getHighestBlockYAt(int x, int z, org.bukkit.HeightMap heightMap) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, x >> 4, z >> 4, "Cannot retrieve chunk asynchronously"); // Folia - region threading
|
|
warnUnsafeChunk("getting a faraway chunk", x >> 4, z >> 4); // Paper
|
|
// Transient load for this tick
|
|
return this.world.getChunk(x >> 4, z >> 4).getHeight(CraftHeightMap.toNMS(heightMap), x, z);
|
|
@@ -1014,6 +1023,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
@Override
|
|
public void setBiome(int x, int y, int z, Holder<net.minecraft.world.level.biome.Biome> bb) {
|
|
BlockPos pos = new BlockPos(x, 0, z);
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, pos, "Cannot retrieve chunk asynchronously"); // Folia - region threading
|
|
if (this.world.hasChunkAt(pos)) {
|
|
net.minecraft.world.level.chunk.LevelChunk chunk = this.world.getChunkAt(pos);
|
|
|
|
@@ -1309,6 +1319,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public void setStorm(boolean hasStorm) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading
|
|
this.world.serverLevelData.setRaining(hasStorm, org.bukkit.event.weather.WeatherChangeEvent.Cause.PLUGIN); // Paper
|
|
this.setWeatherDuration(0); // Reset weather duration (legacy behaviour)
|
|
this.setClearWeatherDuration(0); // Reset clear weather duration (reset "/weather clear" commands)
|
|
@@ -1321,6 +1332,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public void setWeatherDuration(int duration) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading
|
|
this.world.serverLevelData.setRainTime(duration);
|
|
}
|
|
|
|
@@ -1331,6 +1343,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public void setThundering(boolean thundering) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading
|
|
this.world.serverLevelData.setThundering(thundering, org.bukkit.event.weather.ThunderChangeEvent.Cause.PLUGIN); // Paper
|
|
this.setThunderDuration(0); // Reset weather duration (legacy behaviour)
|
|
this.setClearWeatherDuration(0); // Reset clear weather duration (reset "/weather clear" commands)
|
|
@@ -1343,6 +1356,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public void setThunderDuration(int duration) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading
|
|
this.world.serverLevelData.setThunderTime(duration);
|
|
}
|
|
|
|
@@ -1353,6 +1367,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public void setClearWeatherDuration(int duration) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading
|
|
this.world.serverLevelData.setClearWeatherTime(duration);
|
|
}
|
|
|
|
@@ -1547,6 +1562,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public void setKeepSpawnInMemory(boolean keepLoaded) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify keep spawn in memory off of the global region"); // Folia - region threading
|
|
// Paper start - Configurable spawn radius
|
|
if (keepLoaded == this.world.keepSpawnInMemory) {
|
|
// do nothing, nothing has changed
|
|
@@ -1625,6 +1641,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public void setHardcore(boolean hardcore) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
this.world.serverLevelData.settings.hardcore = hardcore;
|
|
}
|
|
|
|
@@ -1637,6 +1654,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
@Override
|
|
@Deprecated
|
|
public void setTicksPerAnimalSpawns(int ticksPerAnimalSpawns) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
this.setTicksPerSpawns(SpawnCategory.ANIMAL, ticksPerAnimalSpawns);
|
|
}
|
|
|
|
@@ -1649,6 +1667,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
@Override
|
|
@Deprecated
|
|
public void setTicksPerMonsterSpawns(int ticksPerMonsterSpawns) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
this.setTicksPerSpawns(SpawnCategory.MONSTER, ticksPerMonsterSpawns);
|
|
}
|
|
|
|
@@ -1661,6 +1680,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
@Override
|
|
@Deprecated
|
|
public void setTicksPerWaterSpawns(int ticksPerWaterSpawns) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
this.setTicksPerSpawns(SpawnCategory.WATER_ANIMAL, ticksPerWaterSpawns);
|
|
}
|
|
|
|
@@ -1673,6 +1693,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
@Override
|
|
@Deprecated
|
|
public void setTicksPerWaterAmbientSpawns(int ticksPerWaterAmbientSpawns) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
this.setTicksPerSpawns(SpawnCategory.WATER_AMBIENT, ticksPerWaterAmbientSpawns);
|
|
}
|
|
|
|
@@ -1685,6 +1706,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
@Override
|
|
@Deprecated
|
|
public void setTicksPerWaterUndergroundCreatureSpawns(int ticksPerWaterUndergroundCreatureSpawns) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
this.setTicksPerSpawns(SpawnCategory.WATER_UNDERGROUND_CREATURE, ticksPerWaterUndergroundCreatureSpawns);
|
|
}
|
|
|
|
@@ -1697,11 +1719,13 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
@Override
|
|
@Deprecated
|
|
public void setTicksPerAmbientSpawns(int ticksPerAmbientSpawns) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
this.setTicksPerSpawns(SpawnCategory.AMBIENT, ticksPerAmbientSpawns);
|
|
}
|
|
|
|
@Override
|
|
public void setTicksPerSpawns(SpawnCategory spawnCategory, int ticksPerCategorySpawn) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
Preconditions.checkArgument(spawnCategory != null, "SpawnCategory cannot be null");
|
|
Preconditions.checkArgument(CraftSpawnCategory.isValidForLimits(spawnCategory), "SpawnCategory.%s are not supported", spawnCategory);
|
|
|
|
@@ -1718,21 +1742,25 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public void setMetadata(String metadataKey, MetadataValue newMetadataValue) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify metadata off of the global region"); // Folia - region threading
|
|
this.server.getWorldMetadata().setMetadata(this, metadataKey, newMetadataValue);
|
|
}
|
|
|
|
@Override
|
|
public List<MetadataValue> getMetadata(String metadataKey) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot retrieve metadata off of the global region"); // Folia - region threading
|
|
return this.server.getWorldMetadata().getMetadata(this, metadataKey);
|
|
}
|
|
|
|
@Override
|
|
public boolean hasMetadata(String metadataKey) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot retrieve metadata off of the global region"); // Folia - region threading
|
|
return this.server.getWorldMetadata().hasMetadata(this, metadataKey);
|
|
}
|
|
|
|
@Override
|
|
public void removeMetadata(String metadataKey, Plugin owningPlugin) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify metadata off of the global region"); // Folia - region threading
|
|
this.server.getWorldMetadata().removeMetadata(this, metadataKey, owningPlugin);
|
|
}
|
|
|
|
@@ -1745,6 +1773,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
@Override
|
|
@Deprecated
|
|
public void setMonsterSpawnLimit(int limit) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
this.setSpawnLimit(SpawnCategory.MONSTER, limit);
|
|
}
|
|
|
|
@@ -1757,6 +1786,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
@Override
|
|
@Deprecated
|
|
public void setAnimalSpawnLimit(int limit) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
this.setSpawnLimit(SpawnCategory.ANIMAL, limit);
|
|
}
|
|
|
|
@@ -1769,6 +1799,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
@Override
|
|
@Deprecated
|
|
public void setWaterAnimalSpawnLimit(int limit) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
this.setSpawnLimit(SpawnCategory.WATER_ANIMAL, limit);
|
|
}
|
|
|
|
@@ -1781,6 +1812,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
@Override
|
|
@Deprecated
|
|
public void setWaterAmbientSpawnLimit(int limit) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
this.setSpawnLimit(SpawnCategory.WATER_AMBIENT, limit);
|
|
}
|
|
|
|
@@ -1793,6 +1825,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
@Override
|
|
@Deprecated
|
|
public void setWaterUndergroundCreatureSpawnLimit(int limit) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
this.setSpawnLimit(SpawnCategory.WATER_UNDERGROUND_CREATURE, limit);
|
|
}
|
|
|
|
@@ -1805,6 +1838,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
@Override
|
|
@Deprecated
|
|
public void setAmbientSpawnLimit(int limit) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
this.setSpawnLimit(SpawnCategory.AMBIENT, limit);
|
|
}
|
|
|
|
@@ -1827,6 +1861,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public void setSpawnLimit(SpawnCategory spawnCategory, int limit) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
Preconditions.checkArgument(spawnCategory != null, "SpawnCategory cannot be null");
|
|
Preconditions.checkArgument(CraftSpawnCategory.isValidForLimits(spawnCategory), "SpawnCategory.%s are not supported", spawnCategory);
|
|
|
|
@@ -1881,7 +1916,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(CraftSound.bukkitToMinecraftHolder(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);
|
|
}
|
|
@@ -1892,7 +1927,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);
|
|
}
|
|
@@ -1978,6 +2013,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public boolean setGameRuleValue(String rule, String value) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
// No null values allowed
|
|
if (rule == null || value == null) return false;
|
|
|
|
@@ -2020,6 +2056,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public <T> boolean setGameRule(GameRule<T> rule, T newValue) {
|
|
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
|
|
Preconditions.checkArgument(rule != null, "GameRule cannot be null");
|
|
Preconditions.checkArgument(newValue != null, "GameRule value cannot be null");
|
|
|
|
@@ -2272,6 +2309,12 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|
|
|
@Override
|
|
public void sendGameEvent(Entity sourceEntity, org.bukkit.GameEvent gameEvent, Vector position) {
|
|
+ // Folia start - region threading
|
|
+ if (sourceEntity != null && !Bukkit.isOwnedByCurrentRegion(sourceEntity)) {
|
|
+ throw new IllegalStateException("Cannot send game event asynchronously");
|
|
+ }
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, position.getX(), position.getZ(), "Cannot send game event asynchronously");
|
|
+ // Folia end - region threading
|
|
getHandle().gameEvent(sourceEntity != null ? ((CraftEntity) sourceEntity).getHandle(): null, net.minecraft.core.registries.BuiltInRegistries.GAME_EVENT.get(org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(gameEvent.getKey())), org.bukkit.craftbukkit.util.CraftVector.toBlockPos(position));
|
|
}
|
|
// Paper end
|
|
@@ -2426,7 +2469,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(new CraftChunk(immediate));
|
|
@@ -2443,7 +2486,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) this.addTicket(x, z); // Paper
|
|
ret.complete(chunk == null ? null : new CraftChunk(chunk));
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java b/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java
|
|
index a8760f015b9ee3ee408c3b9220266eb76b313ba0..fadf10c6a7fabde2c13f3aff00739a21e9f05ced 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/block/CapturedBlockState.java
|
|
@@ -52,6 +52,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 bec8e6b62dba2bd0e4e85a7d1fb51287384f1290..6c91ee8b61cca34bd9e251076d8e9881729526da 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java
|
|
@@ -74,6 +74,11 @@ public class CraftBlock implements Block {
|
|
}
|
|
|
|
public net.minecraft.world.level.block.state.BlockState getNMS() {
|
|
+ // Folia start - region threading
|
|
+ if (this.world instanceof ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
return this.world.getBlockState(this.position);
|
|
}
|
|
|
|
@@ -150,6 +155,11 @@ public class CraftBlock implements Block {
|
|
}
|
|
|
|
private void setData(final byte data, int flag) {
|
|
+ // Folia start - region threading
|
|
+ if (this.world instanceof ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
this.world.setBlock(this.position, CraftMagicNumbers.getBlock(this.getType(), data), flag);
|
|
}
|
|
|
|
@@ -191,6 +201,11 @@ public class CraftBlock implements Block {
|
|
}
|
|
|
|
public static boolean setTypeAndData(LevelAccessor world, BlockPos position, net.minecraft.world.level.block.state.BlockState old, net.minecraft.world.level.block.state.BlockState blockData, boolean applyPhysics) {
|
|
+ // Folia start - region threading
|
|
+ if (world instanceof ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
// SPIGOT-611: need to do this to prevent glitchiness. Easier to handle this here (like /setblock) than to fix weirdness in tile entity cleanup
|
|
if (old.hasBlockEntity() && blockData.getBlock() != old.getBlock()) { // SPIGOT-3725 remove old tile entity if block changes
|
|
// SPIGOT-4612: faster - just clear tile
|
|
@@ -336,18 +351,33 @@ public class CraftBlock implements Block {
|
|
|
|
@Override
|
|
public Biome getBiome() {
|
|
+ // Folia start - region threading
|
|
+ if (this.world instanceof ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
return this.getWorld().getBiome(this.getX(), this.getY(), this.getZ());
|
|
}
|
|
|
|
// Paper start
|
|
@Override
|
|
public Biome getComputedBiome() {
|
|
+ // Folia start - region threading
|
|
+ if (this.world instanceof ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
return this.getWorld().getComputedBiome(this.getX(), this.getY(), this.getZ());
|
|
}
|
|
// Paper end
|
|
|
|
@Override
|
|
public void setBiome(Biome bio) {
|
|
+ // Folia start - region threading
|
|
+ if (this.world instanceof ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
this.getWorld().setBiome(this.getX(), this.getY(), this.getZ(), bio);
|
|
}
|
|
|
|
@@ -395,6 +425,11 @@ public class CraftBlock implements Block {
|
|
|
|
@Override
|
|
public boolean isBlockFaceIndirectlyPowered(BlockFace face) {
|
|
+ // Folia start - region threading
|
|
+ if (this.world instanceof ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
int power = this.world.getMinecraftWorld().getSignal(this.position, CraftBlock.blockFaceToNotch(face));
|
|
|
|
Block relative = this.getRelative(face);
|
|
@@ -407,6 +442,11 @@ public class CraftBlock implements Block {
|
|
|
|
@Override
|
|
public int getBlockPower(BlockFace face) {
|
|
+ // Folia start - region threading
|
|
+ if (this.world instanceof ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
int power = 0;
|
|
net.minecraft.world.level.Level world = this.world.getMinecraftWorld();
|
|
int x = this.getX();
|
|
@@ -493,6 +533,11 @@ public class CraftBlock implements Block {
|
|
|
|
@Override
|
|
public boolean breakNaturally(ItemStack item, boolean triggerEffect, boolean dropExperience) {
|
|
+ // Folia start - region threading
|
|
+ if (this.world instanceof ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
// Paper end
|
|
// Order matters here, need to drop before setting to air so skulls can get their data
|
|
net.minecraft.world.level.block.state.BlockState iblockdata = this.getNMS();
|
|
@@ -536,21 +581,27 @@ public class CraftBlock implements Block {
|
|
|
|
@Override
|
|
public boolean applyBoneMeal(BlockFace face) {
|
|
+ // Folia start - region threading
|
|
+ if (this.world instanceof ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
Direction direction = CraftBlock.blockFaceToNotch(face);
|
|
BlockFertilizeEvent event = null;
|
|
ServerLevel world = this.getCraftWorld().getHandle();
|
|
UseOnContext context = new UseOnContext(world, null, InteractionHand.MAIN_HAND, Items.BONE_MEAL.getDefaultInstance(), new BlockHitResult(Vec3.ZERO, direction, this.getPosition(), false));
|
|
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
|
|
// SPIGOT-6895: Call StructureGrowEvent and BlockFertilizeEvent
|
|
- world.captureTreeGeneration = true;
|
|
+ worldData.captureTreeGeneration = true; // Folia - region threading
|
|
InteractionResult result = BoneMealItem.applyBonemeal(context);
|
|
- world.captureTreeGeneration = false;
|
|
+ worldData.captureTreeGeneration = false; // Folia - region threading
|
|
|
|
- if (world.capturedBlockStates.size() > 0) {
|
|
- TreeType treeType = SaplingBlock.treeType;
|
|
- SaplingBlock.treeType = null;
|
|
- List<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) {
|
|
@@ -637,6 +688,11 @@ public class CraftBlock implements Block {
|
|
|
|
@Override
|
|
public RayTraceResult rayTrace(Location start, Vector direction, double maxDistance, FluidCollisionMode fluidCollisionMode) {
|
|
+ // Folia start - region threading
|
|
+ if (this.world instanceof ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
Preconditions.checkArgument(start != null, "Location start cannot be null");
|
|
Preconditions.checkArgument(this.getWorld().equals(start.getWorld()), "Location start cannot be a different world");
|
|
start.checkFinite();
|
|
@@ -678,6 +734,11 @@ public class CraftBlock implements Block {
|
|
|
|
@Override
|
|
public boolean canPlace(BlockData data) {
|
|
+ // Folia start - region threading
|
|
+ if (this.world instanceof ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
Preconditions.checkArgument(data != null, "BlockData cannot be null");
|
|
net.minecraft.world.level.block.state.BlockState iblockdata = ((CraftBlockData) data).getState();
|
|
net.minecraft.world.level.Level world = this.world.getMinecraftWorld();
|
|
@@ -712,6 +773,11 @@ public class CraftBlock implements Block {
|
|
|
|
@Override
|
|
public void tick() {
|
|
+ // Folia start - region threading
|
|
+ if (this.world instanceof ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
net.minecraft.world.level.block.state.BlockState blockData = this.getNMS();
|
|
net.minecraft.server.level.ServerLevel level = this.world.getMinecraftWorld();
|
|
|
|
@@ -720,6 +786,11 @@ public class CraftBlock implements Block {
|
|
|
|
@Override
|
|
public void randomTick() {
|
|
+ // Folia start - region threading
|
|
+ if (this.world instanceof ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
net.minecraft.world.level.block.state.BlockState blockData = this.getNMS();
|
|
net.minecraft.server.level.ServerLevel level = this.world.getMinecraftWorld();
|
|
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java
|
|
index 390e1b7fd2721b99cb3ce268c6bc1bf0a38e08a3..9bf02fde730932d356803d2ab6d3c193e0437c8a 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java
|
|
@@ -210,6 +210,12 @@ public class CraftBlockState implements BlockState {
|
|
LevelAccessor access = this.getWorldHandle();
|
|
CraftBlock block = this.getBlock();
|
|
|
|
+ // Folia start - region threading
|
|
+ if (access instanceof net.minecraft.server.level.ServerLevel serverWorld) {
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
if (block.getType() != this.getType()) {
|
|
if (!force) {
|
|
return false;
|
|
@@ -350,6 +356,9 @@ public class CraftBlockState implements BlockState {
|
|
|
|
@Override
|
|
public java.util.Collection<org.bukkit.inventory.ItemStack> getDrops(org.bukkit.inventory.ItemStack item, org.bukkit.entity.Entity entity) {
|
|
+ // Folia start - region threading
|
|
+ io.papermc.paper.util.TickThread.ensureTickThread(world.getHandle(), position, "Cannot modify world asynchronously");
|
|
+ // Folia end - region threading
|
|
net.minecraft.world.item.ItemStack nms = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(item);
|
|
|
|
// Modelled off EntityHuman#hasBlock
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java
|
|
index 12eeabafbad9da8796dc6fc383b732cf75bb7ddb..6ede423639c8e2012da897f517e8f7c9de72095f 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 0e6c7284b9aee6c5f2454a3a095ebf349f887740..690e57b55adb53d56d874cbddec6226a515f43a2 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
|
|
@@ -581,6 +581,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();
|
|
@@ -1035,7 +1040,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
|
|
ImmutableSet.Builder<Player> players = ImmutableSet.builder();
|
|
|
|
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) {
|
|
for (ServerPlayerConnection connection : entityTracker.seenBy) {
|
|
@@ -1293,7 +1298,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;
|
|
@@ -1361,30 +1366,43 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
|
|
Preconditions.checkArgument(location != null, "location");
|
|
location.checkFinite();
|
|
Location locationClone = location.clone(); // clone so we don't need to worry about mutations after this call.
|
|
-
|
|
- net.minecraft.server.level.ServerLevel world = ((CraftWorld)locationClone.getWorld()).getHandle();
|
|
+ // Folia start - region threading
|
|
java.util.concurrent.CompletableFuture<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;
|
|
- }
|
|
- net.minecraft.server.MinecraftServer.LOGGER.error("Failed to teleport entity " + CraftEntity.this, throwable);
|
|
- ret.completeExceptionally(throwable);
|
|
+ java.util.function.Consumer<Entity> run = (Entity nmsEntity) -> {
|
|
+ boolean success = nmsEntity.teleportAsync(
|
|
+ ((CraftWorld)locationClone.getWorld()).getHandle(),
|
|
+ new net.minecraft.world.phys.Vec3(locationClone.getX(), locationClone.getY(), locationClone.getZ()),
|
|
+ locationClone.getYaw(), locationClone.getPitch(), net.minecraft.world.phys.Vec3.ZERO,
|
|
+ cause == null ? TeleportCause.UNKNOWN : cause,
|
|
+ Entity.TELEPORT_FLAG_LOAD_CHUNK | Entity.TELEPORT_FLAG_UNMOUNT, // preserve behavior with old API: dismount the entity so it can teleport
|
|
+ (Entity entityTp) -> {
|
|
+ ret.complete(Boolean.TRUE);
|
|
}
|
|
- });
|
|
- });
|
|
+ );
|
|
+ if (!success) {
|
|
+ ret.complete(Boolean.FALSE);
|
|
+ }
|
|
+ };
|
|
+ if (org.bukkit.Bukkit.isOwnedByCurrentRegion(this)) {
|
|
+ run.accept(this.getHandle());
|
|
+ return ret;
|
|
+ }
|
|
+ boolean scheduled = this.taskScheduler.schedule(
|
|
+ // success
|
|
+ run,
|
|
+ // retired
|
|
+ (Entity nmsEntity) -> {
|
|
+ ret.complete(Boolean.FALSE);
|
|
+ },
|
|
+ 1L
|
|
+ );
|
|
+
|
|
+ if (!scheduled) {
|
|
+ ret.complete(Boolean.FALSE);
|
|
+ }
|
|
|
|
return ret;
|
|
+ // Folia end - region threading
|
|
}
|
|
|
|
@Override
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
|
|
index 83aaf3e6e377d731ce02f779f80b7bf5db46f89f..249887106315ed0625b08bc8b2a74404cf0a418b 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
|
|
@@ -591,7 +591,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
|
|
@@ -1298,6 +1298,11 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
|
|
|
|
@Override
|
|
public boolean teleport(Location location, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause, io.papermc.paper.entity.TeleportFlag... flags) {
|
|
+ // Folia start - region threading
|
|
+ if (true) {
|
|
+ throw new UnsupportedOperationException("Must use teleportAsync while in region threading");
|
|
+ }
|
|
+ // Folia end - region threading
|
|
java.util.Set<net.minecraft.world.entity.RelativeMovement> relativeArguments;
|
|
java.util.Set<io.papermc.paper.entity.TeleportFlag> allFlags;
|
|
if (flags.length == 0) {
|
|
@@ -1880,7 +1885,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());
|
|
}
|
|
@@ -1977,7 +1982,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
|
|
if (original != null) otherPlayer.setUUID(original); // Paper - uuid override
|
|
}
|
|
|
|
- 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 5dc160b743534665c6b3efb10b10f7c36e2da5ab..606551330859fd423b31d246bc14891d1236ba42 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
|
|
@@ -246,8 +246,8 @@ import org.bukkit.potion.PotionEffect;
|
|
import org.bukkit.util.Vector;
|
|
|
|
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) {
|
|
@@ -920,7 +920,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 final 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
|
|
@@ -932,7 +932,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()) {
|
|
@@ -1047,8 +1047,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, source.explodedBlockState); // Paper - handle block state in damage
|
|
@@ -1109,13 +1109,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);
|
|
@@ -1123,9 +1123,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)) {
|
|
@@ -1140,9 +1140,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);
|
|
@@ -1150,10 +1150,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) {
|
|
@@ -2123,7 +2123,7 @@ public class CraftEventFactory {
|
|
CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemStack.copyWithCount(1));
|
|
|
|
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(to.getX(), to.getY(), to.getZ()));
|
|
- if (!net.minecraft.world.level.block.DispenserBlock.eventFired) {
|
|
+ if (!net.minecraft.world.level.block.DispenserBlock.eventFired.get()) { // Folia - region threading
|
|
if (!event.callEvent()) {
|
|
return itemStack;
|
|
}
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
|
|
index 3ceb5d83be20183da907915f70ba9e64369373a9..b23e43f7ac22bc106a0bcde4a8bca9ecba1a9ae4 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
|
|
@@ -530,6 +530,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 548c77592a3520e8053483644eba805079a14f1a..73c395cd5fd3dc07640cdc06e174cd99de43df4c 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
|
|
@@ -380,6 +380,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/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
|
|
index cbedb6f002bc01daa16d349421c4ef04d4bcbcb2..2d708bb11f841bb265d12a025dbaa657b0b4d08a 100644
|
|
--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java
|
|
@@ -69,6 +69,13 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel {
|
|
this.handle = worldAccess;
|
|
}
|
|
|
|
+ // Folia start - region threading
|
|
+ @Override
|
|
+ public net.minecraft.world.level.StructureManager structureManager() {
|
|
+ return this.handle.structureManager();
|
|
+ }
|
|
+ // Folia end - region threading
|
|
+
|
|
public WorldGenLevel getHandle() {
|
|
return this.handle;
|
|
}
|
|
diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java
|
|
index 59103744ac6beeb12719fdefcda54eeff498229e..d88988200016c1a3cc76c017dfb7deabf6fc17af 100644
|
|
--- a/src/main/java/org/spigotmc/ActivationRange.java
|
|
+++ b/src/main/java/org/spigotmc/ActivationRange.java
|
|
@@ -65,26 +65,27 @@ public class ActivationRange
|
|
|
|
private static int checkInactiveWakeup(Entity entity) {
|
|
Level world = entity.level();
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions
|
|
SpigotWorldConfig config = world.spigotConfig;
|
|
- long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
|
|
+ long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions
|
|
if (entity.activationType == ActivationType.VILLAGER) {
|
|
- if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) {
|
|
- world.wakeupInactiveRemainingVillagers--;
|
|
+ if (inactiveFor > config.wakeUpInactiveVillagersEvery && worldData.wakeupInactiveRemainingVillagers > 0) { // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingVillagers--; // Folia - threaded regions
|
|
return config.wakeUpInactiveVillagersFor;
|
|
}
|
|
} else if (entity.activationType == ActivationType.ANIMAL) {
|
|
- if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) {
|
|
- world.wakeupInactiveRemainingAnimals--;
|
|
+ if (inactiveFor > config.wakeUpInactiveAnimalsEvery && worldData.wakeupInactiveRemainingAnimals > 0) { // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingAnimals--; // Folia - threaded regions
|
|
return config.wakeUpInactiveAnimalsFor;
|
|
}
|
|
} else if (entity.activationType == ActivationType.FLYING_MONSTER) {
|
|
- if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) {
|
|
- world.wakeupInactiveRemainingFlying--;
|
|
+ if (inactiveFor > config.wakeUpInactiveFlyingEvery && worldData.wakeupInactiveRemainingFlying > 0) { // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingFlying--; // Folia - threaded regions
|
|
return config.wakeUpInactiveFlyingFor;
|
|
}
|
|
} else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) {
|
|
- if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) {
|
|
- world.wakeupInactiveRemainingMonsters--;
|
|
+ if (inactiveFor > config.wakeUpInactiveMonstersEvery && worldData.wakeupInactiveRemainingMonsters > 0) { // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingMonsters--; // Folia - threaded regions
|
|
return config.wakeUpInactiveMonstersFor;
|
|
}
|
|
}
|
|
@@ -174,10 +175,11 @@ public class ActivationRange
|
|
final int waterActivationRange = world.spigotConfig.waterActivationRange;
|
|
final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange;
|
|
final int villagerActivationRange = world.spigotConfig.villagerActivationRange;
|
|
- world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals);
|
|
- world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers);
|
|
- world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters);
|
|
- world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying);
|
|
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingAnimals = Math.min(worldData.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingVillagers = Math.min(worldData.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingMonsters = Math.min(worldData.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); // Folia - threaded regions
|
|
+ worldData.wakeupInactiveRemainingFlying = Math.min(worldData.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); // Folia - threaded regions
|
|
final ServerChunkCache chunkProvider = (ServerChunkCache) world.getChunkSource();
|
|
// Paper end
|
|
|
|
@@ -191,9 +193,9 @@ public class ActivationRange
|
|
// Paper end
|
|
maxRange = Math.min( ( world.spigotConfig.simulationDistance << 4 ) - 8, maxRange );
|
|
|
|
- for ( Player player : world.players() )
|
|
+ for ( Player player : world.getLocalPlayers() ) // Folia - region threading
|
|
{
|
|
- player.activatedTick = MinecraftServer.currentTick;
|
|
+ player.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading
|
|
if ( world.spigotConfig.ignoreSpectatorActivation && player.isSpectator() )
|
|
{
|
|
continue;
|
|
@@ -212,10 +214,16 @@ public class ActivationRange
|
|
// Paper end
|
|
|
|
// Paper start
|
|
- java.util.List<Entity> entities = world.getEntities((Entity)null, ActivationRange.maxBB, null);
|
|
+ java.util.List<Entity> entities = new java.util.ArrayList<>(); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later
|
|
+ ((net.minecraft.server.level.ServerLevel)world).getEntityLookup().getEntities((Entity)null, maxBB, entities, null); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later
|
|
boolean tickMarkers = world.paperConfig().entities.markers.tick; // Paper - configurable marker ticking
|
|
for (int i = 0; i < entities.size(); i++) {
|
|
Entity entity = entities.get(i);
|
|
+ // Folia start - region ticking
|
|
+ if (!io.papermc.paper.util.TickThread.isTickThreadFor(entity)) {
|
|
+ continue;
|
|
+ }
|
|
+ // Folia end - region ticking
|
|
// Paper start - configurable marker ticking
|
|
if (!tickMarkers && entity instanceof net.minecraft.world.entity.Marker) {
|
|
continue;
|
|
@@ -235,16 +243,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
|
|
}
|
|
}
|
|
}
|
|
@@ -267,10 +275,10 @@ public class ActivationRange
|
|
if (entity.getRemainingFireTicks() > 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
|
|
@@ -393,19 +401,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 ac0fd418fcb437896dfdff53ab3eff19833d25fb..130220977477a5d8d51e17dcb989ae0c858643cb 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 68602dfb171d47e47fd0710b4324013ef05214d0..2d75cefc69ea1da96a99370aef3df7bd6fcf11b9 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( SpigotConfig.timeoutTime, SpigotConfig.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 f9b8e2bc039f1a37e47f84909c8785f3ef530284..315b860807bed537f8dbd85c9a053e39216e7b71 100644
|
|
--- a/src/main/java/org/spigotmc/SpigotWorldConfig.java
|
|
+++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java
|
|
@@ -433,7 +433,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 )
|