1268 lines
57 KiB
Diff
1268 lines
57 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
|
|
Date: Fri, 14 Feb 2020 01:24:39 -0800
|
|
Subject: [PATCH] Optimise TickListServer by rewriting it
|
|
|
|
In my profiling TickListServer showed up as
|
|
~10% for saving chunks and ~5% for the scheduling
|
|
of items on a server with ~90 players at
|
|
view distance = 5. Most of the performance
|
|
loss is unneccessary.
|
|
|
|
TickListServer has numerous performance issues:
|
|
1. Handling scheduled items is O(nlogn)
|
|
2. Getting scheduled items for a chunk is O(n),
|
|
with n being the the number of scheduled items
|
|
for all chunks (hits saving very hard)
|
|
3. Checking if an item is scheduled for the current tick is O(n),
|
|
with n being the number of items scheduled for current tick
|
|
4. Items not in ticking chunks are churned in the scheduler
|
|
|
|
The biggest issues are 4 & 2.
|
|
|
|
We solve 1 by splitting up scheduled items into short and long scheduled,
|
|
where we expect the vast majority of our entries to be in the short scheduled
|
|
set. Handling short scheduled items is O(n) due to how the comparison
|
|
process is reduced to mapping. See TickListServerInterval. However,
|
|
this isn't memory-efficient - which is why long scheduled exists.
|
|
Long scheduled is handled the same as TickListServer.
|
|
|
|
2 is solved by mapping what entries are in what chunks.
|
|
|
|
3 is solved by mapping what blocks have what scheduled for them.
|
|
|
|
4 is solved by moving the items that are not in ticking chunks
|
|
into a map of entries for that chunk. Once the chunk is moved
|
|
to ticking, the items are re-scheduled.
|
|
|
|
This patch has also added two flags to debug excessive tick delays:
|
|
-Dpaper.ticklist-warn-on-excessive-delay=true (false by default)
|
|
and -Dpaper.ticklist-excessive-delay-threshold=ticks which
|
|
sets the excessive tick delay to the specified ticks (defaults to
|
|
60 * 20 ticks, aka 60 seconds)
|
|
|
|
diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
|
|
index 8bf4d2b8c38c02d6a5b2fea37113689a252f1571..da93d38fe63035e4ff198ada84a4431f52d97c01 100644
|
|
--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
|
|
+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
|
|
@@ -354,6 +354,13 @@ public class PaperConfig {
|
|
maxBookTotalSizeMultiplier = getDouble("settings.book-size.total-multiplier", maxBookTotalSizeMultiplier);
|
|
}
|
|
|
|
+ public static boolean useOptimizedTickList = true;
|
|
+ private static void useOptimizedTickList() {
|
|
+ if (config.contains("settings.use-optimized-ticklist")) { // don't add default, hopefully temporary config
|
|
+ useOptimizedTickList = config.getBoolean("settings.use-optimized-ticklist");
|
|
+ }
|
|
+ }
|
|
+
|
|
public static boolean asyncChunks = false;
|
|
private static void asyncChunks() {
|
|
ConfigurationSection section;
|
|
diff --git a/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..8918bad880d6eeed30db39b6326b2f65e24edf45
|
|
--- /dev/null
|
|
+++ b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java
|
|
@@ -0,0 +1,628 @@
|
|
+package com.destroystokyo.paper.server.ticklist;
|
|
+
|
|
+import java.util.function.Function;
|
|
+import net.minecraft.CrashReport;
|
|
+import net.minecraft.CrashReportSystemDetails;
|
|
+import net.minecraft.ReportedException;
|
|
+import net.minecraft.core.BlockPosition;
|
|
+import net.minecraft.nbt.NBTTagList;
|
|
+import net.minecraft.resources.MinecraftKey;
|
|
+import net.minecraft.server.MCUtil;
|
|
+import net.minecraft.server.MinecraftServer;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
|
|
+import java.util.ArrayDeque;
|
|
+import java.util.ArrayList;
|
|
+import java.util.Collections;
|
|
+import java.util.Comparator;
|
|
+import java.util.Iterator;
|
|
+import java.util.List;
|
|
+import java.util.function.Consumer;
|
|
+import java.util.function.Predicate;
|
|
+import net.minecraft.server.level.ChunkProviderServer;
|
|
+import net.minecraft.server.level.WorldServer;
|
|
+import net.minecraft.world.level.ChunkCoordIntPair;
|
|
+import net.minecraft.world.level.NextTickListEntry;
|
|
+import net.minecraft.world.level.TickListPriority;
|
|
+import net.minecraft.world.level.TickListServer;
|
|
+import net.minecraft.world.level.block.state.IBlockData;
|
|
+import net.minecraft.world.level.levelgen.structure.StructureBoundingBox;
|
|
+
|
|
+public final class PaperTickList<T> extends TickListServer<T> { // extend to avoid breaking ABI
|
|
+
|
|
+ // in the order the state is expected to change (mostly)
|
|
+ public static final int STATE_UNSCHEDULED = 1 << 0;
|
|
+ public static final int STATE_SCHEDULED = 1 << 1; // scheduled for some tick
|
|
+ public static final int STATE_PENDING_TICK = 1 << 2; // for this tick
|
|
+ public static final int STATE_TICKING = 1 << 3;
|
|
+ public static final int STATE_TICKED = 1 << 4; // after this, it gets thrown back to unscheduled
|
|
+ public static final int STATE_CANCELLED_TICK = 1 << 5; // still gets moved to unscheduled after tick
|
|
+
|
|
+ private static final int SHORT_SCHEDULE_TICK_THRESHOLD = 20 * 20 + 1; // 20 seconds
|
|
+
|
|
+ private final WorldServer world;
|
|
+ private final Predicate<T> excludeFromScheduling;
|
|
+ private final Function<T, MinecraftKey> getMinecraftKeyFrom;
|
|
+ //private final Function<MinecraftKey, T> getObjectFronMinecraftKey;
|
|
+ private final Consumer<NextTickListEntry<T>> tickFunction;
|
|
+
|
|
+ private final co.aikar.timings.Timing timingCleanup; // Paper
|
|
+ private final co.aikar.timings.Timing timingTicking; // Paper
|
|
+ private final co.aikar.timings.Timing timingFinished;
|
|
+
|
|
+ // note: remove ops / add ops suck on fastutil, a chained hashtable implementation would work better, but Long...
|
|
+ // try to alleviate with a very small load factor
|
|
+ private final Long2ObjectOpenHashMap<ArrayList<NextTickListEntry<T>>> entriesByBlock = new Long2ObjectOpenHashMap<>(1024, 0.25f);
|
|
+ private final Long2ObjectOpenHashMap<ObjectRBTreeSet<NextTickListEntry<T>>> entriesByChunk = new Long2ObjectOpenHashMap<>(1024, 0.25f);
|
|
+ private final Long2ObjectOpenHashMap<ArrayList<NextTickListEntry<T>>> pendingChunkTickLoad = new Long2ObjectOpenHashMap<>(1024, 0.5f);
|
|
+
|
|
+ // fastutil has O(1) first/last while TreeMap/TreeSet are log(n)
|
|
+ private final ObjectRBTreeSet<NextTickListEntry<T>> longScheduled = new ObjectRBTreeSet<>(TickListServerInterval.ENTRY_COMPARATOR);
|
|
+
|
|
+ private final ArrayDeque<NextTickListEntry<T>> toTickThisTick = new ArrayDeque<>();
|
|
+
|
|
+ private final TickListServerInterval<T>[] shortScheduled = new TickListServerInterval[SHORT_SCHEDULE_TICK_THRESHOLD];
|
|
+ {
|
|
+ for (int i = 0, len = this.shortScheduled.length; i < len; ++i) {
|
|
+ this.shortScheduled[i] = new TickListServerInterval<>();
|
|
+ }
|
|
+ }
|
|
+ private int shortScheduledIndex;
|
|
+
|
|
+ private long currentTick;
|
|
+
|
|
+ private static final boolean WARN_ON_EXCESSIVE_DELAY = Boolean.getBoolean("paper.ticklist-warn-on-excessive-delay");
|
|
+ private static final long EXCESSIVE_DELAY_THRESHOLD = Long.getLong("paper.ticklist-excessive-delay-threshold", 60 * 20).longValue(); // 1 min dfl
|
|
+
|
|
+ // assume index < length
|
|
+ private static int getWrappedIndex(final int start, final int length, final int index) {
|
|
+ final int next = start + index;
|
|
+ return next < length ? next : next - length;
|
|
+ }
|
|
+
|
|
+ private static int getNextIndex(final int curr, final int length) {
|
|
+ final int next = curr + 1;
|
|
+ return next < length ? next : 0;
|
|
+ }
|
|
+
|
|
+ public PaperTickList(final WorldServer world, final Predicate<T> excludeFromScheduling, final Function<T, MinecraftKey> getMinecraftKeyFrom,
|
|
+ final Consumer<NextTickListEntry<T>> tickFunction, final String timingsType) {
|
|
+ super(world, excludeFromScheduling, getMinecraftKeyFrom, tickFunction, timingsType);
|
|
+ this.world = world;
|
|
+ this.excludeFromScheduling = excludeFromScheduling;
|
|
+ this.getMinecraftKeyFrom = getMinecraftKeyFrom;
|
|
+ this.tickFunction = tickFunction;
|
|
+ this.timingCleanup = co.aikar.timings.WorldTimingsHandler.getTickList(world, timingsType + " - Cleanup"); // Paper
|
|
+ this.timingTicking = co.aikar.timings.WorldTimingsHandler.getTickList(world, timingsType + " - Ticking"); // Paper
|
|
+ this.timingFinished = co.aikar.timings.WorldTimingsHandler.getTickList(world, timingsType + " - Finish");
|
|
+ this.currentTick = this.world.getTime();
|
|
+ }
|
|
+
|
|
+ private void queueEntryForTick(final NextTickListEntry<T> entry, final ChunkProviderServer chunkProvider) {
|
|
+ if (entry.tickState == STATE_SCHEDULED) {
|
|
+ if (chunkProvider.isTickingReadyMainThread(entry.getPosition())) {
|
|
+ this.toTickThisTick.add(entry);
|
|
+ entry.tickState = STATE_PENDING_TICK;
|
|
+ } else {
|
|
+ // we dump them to a map to avoid constantly re-scheduling them
|
|
+ this.addToNotTickingReady(entry);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void addToNotTickingReady(final NextTickListEntry<T> entry) {
|
|
+ this.pendingChunkTickLoad.computeIfAbsent(MCUtil.getCoordinateKey(entry.getPosition()), (long keyInMap) -> {
|
|
+ return new ArrayList<>();
|
|
+ }).add(entry);
|
|
+ }
|
|
+
|
|
+ private void addToSchedule(final NextTickListEntry<T> entry) {
|
|
+ long delay = entry.getTargetTick() - (this.currentTick + 1);
|
|
+ if (delay < SHORT_SCHEDULE_TICK_THRESHOLD) {
|
|
+ if (delay < 0) {
|
|
+ // longScheduled orders by tick time, short scheduled does not
|
|
+ this.longScheduled.add(entry);
|
|
+ } else {
|
|
+ this.shortScheduled[getWrappedIndex(this.shortScheduledIndex, SHORT_SCHEDULE_TICK_THRESHOLD, (int)delay)].addEntryLast(entry);
|
|
+ }
|
|
+ } else {
|
|
+ this.longScheduled.add(entry);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void removeEntry(final NextTickListEntry<T> entry) {
|
|
+ entry.tickState = STATE_CANCELLED_TICK;
|
|
+ // short/long scheduled will skip the entry
|
|
+
|
|
+ final BlockPosition pos = entry.getPosition();
|
|
+ final long blockKey = MCUtil.getBlockKey(pos);
|
|
+
|
|
+ final ArrayList<NextTickListEntry<T>> currentEntries = this.entriesByBlock.get(blockKey);
|
|
+
|
|
+ if (currentEntries.size() == 1) {
|
|
+ // it should contain our entry
|
|
+ this.entriesByBlock.remove(blockKey);
|
|
+ } else {
|
|
+ // it's more likely that this entry is at the start of the list than the end
|
|
+ for (int i = 0, len = currentEntries.size(); i < len; ++i) {
|
|
+ final NextTickListEntry<T> currentEntry = currentEntries.get(i);
|
|
+ if (currentEntry == entry) {
|
|
+ currentEntries.remove(i);
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final long chunkKey = MCUtil.getCoordinateKey(entry.getPosition());
|
|
+
|
|
+ ObjectRBTreeSet<NextTickListEntry<T>> set = this.entriesByChunk.get(chunkKey);
|
|
+
|
|
+ set.remove(entry);
|
|
+
|
|
+ if (set.isEmpty()) {
|
|
+ this.entriesByChunk.remove(chunkKey);
|
|
+ }
|
|
+
|
|
+ ArrayList<NextTickListEntry<T>> pendingTickingLoad = this.pendingChunkTickLoad.get(chunkKey);
|
|
+
|
|
+ if (pendingTickingLoad != null) {
|
|
+ for (int i = 0, len = pendingTickingLoad.size(); i < len; ++i) {
|
|
+ if (pendingTickingLoad.get(i) == entry) {
|
|
+ pendingTickingLoad.remove(i);
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (pendingTickingLoad.isEmpty()) {
|
|
+ this.pendingChunkTickLoad.remove(chunkKey);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ long delay = entry.getTargetTick() - (this.currentTick + 1);
|
|
+ if (delay >= SHORT_SCHEDULE_TICK_THRESHOLD) {
|
|
+ this.longScheduled.remove(entry);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void onChunkSetTicking(final int chunkX, final int chunkZ) {
|
|
+ final ArrayList<NextTickListEntry<T>> pending = this.pendingChunkTickLoad.remove(MCUtil.getCoordinateKey(chunkX, chunkZ));
|
|
+ if (pending == null) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ for (int i = 0, size = pending.size(); i < size; ++i) {
|
|
+ final NextTickListEntry<T> entry = pending.get(i);
|
|
+ // already in all the relevant reference maps, just need to add to longScheduled or shortScheduled
|
|
+ this.addToSchedule(entry);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void prepare() {
|
|
+ final long currentTick = this.currentTick;
|
|
+
|
|
+ final ChunkProviderServer chunkProvider = this.world.getChunkProvider();
|
|
+
|
|
+ // here we setup what's going to tick
|
|
+
|
|
+ // we don't remove items from shortScheduled (but do from longScheduled) because they're cleared at the end of
|
|
+ // this tick
|
|
+ if (this.longScheduled.isEmpty() || this.longScheduled.first().getTargetTick() > currentTick) {
|
|
+ // nothing in longScheduled to worry about
|
|
+ final TickListServerInterval<T> interval = this.shortScheduled[this.shortScheduledIndex];
|
|
+ for (int i = 0, len = interval.byPriority.length; i < len; ++i) {
|
|
+ for (final Iterator<NextTickListEntry<T>> iterator = interval.byPriority[i].iterator(); iterator.hasNext();) {
|
|
+ this.queueEntryForTick(iterator.next(), chunkProvider);
|
|
+ }
|
|
+ }
|
|
+ } else {
|
|
+ final TickListServerInterval<T> interval = this.shortScheduled[this.shortScheduledIndex];
|
|
+
|
|
+ // combine interval and longScheduled, keeping order
|
|
+ final Comparator<NextTickListEntry<T>> comparator = (Comparator)TickListServerInterval.ENTRY_COMPARATOR;
|
|
+ final Iterator<NextTickListEntry<T>> longScheduledIterator = this.longScheduled.iterator();
|
|
+ NextTickListEntry<T> longCurrent = longScheduledIterator.next();
|
|
+
|
|
+ for (int i = 0, len = interval.byPriority.length; i < len; ++i) {
|
|
+ for (final Iterator<NextTickListEntry<T>> iterator = interval.byPriority[i].iterator(); iterator.hasNext();) {
|
|
+ final NextTickListEntry<T> shortCurrent = iterator.next();
|
|
+ if (longCurrent != null) {
|
|
+ // drain longCurrent until we can add shortCurrent
|
|
+ while (comparator.compare(longCurrent, shortCurrent) <= 0) {
|
|
+ this.queueEntryForTick(longCurrent, chunkProvider);
|
|
+ longScheduledIterator.remove();
|
|
+ if (longScheduledIterator.hasNext()) {
|
|
+ longCurrent = longScheduledIterator.next();
|
|
+ if (longCurrent.getTargetTick() > currentTick) {
|
|
+ longCurrent = null;
|
|
+ break;
|
|
+ }
|
|
+ } else {
|
|
+ longCurrent = null;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ this.queueEntryForTick(shortCurrent, chunkProvider);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // add remaining from long scheduled
|
|
+ for (;;) {
|
|
+ if (longCurrent == null || longCurrent.getTargetTick() > currentTick) {
|
|
+ break;
|
|
+ }
|
|
+ longScheduledIterator.remove();
|
|
+ this.queueEntryForTick(longCurrent, chunkProvider);
|
|
+
|
|
+ if (longScheduledIterator.hasNext()) {
|
|
+ longCurrent = longScheduledIterator.next();
|
|
+ } else {
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private boolean warnedAboutDesync;
|
|
+
|
|
+ @Override
|
|
+ public void nextTick() {
|
|
+ ++this.currentTick;
|
|
+ if (this.currentTick != this.world.getTime()) {
|
|
+ if (!this.warnedAboutDesync) {
|
|
+ this.warnedAboutDesync = true;
|
|
+ MinecraftServer.LOGGER.error("World tick desync detected! Expected " + this.currentTick + " ticks, but got " + this.world.getTime() + " ticks for world '" + this.world.getWorld().getName() + "'", new Throwable());
|
|
+ MinecraftServer.LOGGER.error("Preventing redstone from breaking by refusing to accept new tick time");
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void tick() {
|
|
+ final ChunkProviderServer chunkProvider = this.world.getChunkProvider();
|
|
+
|
|
+ this.world.getMethodProfiler().enter("cleaning");
|
|
+ this.timingCleanup.startTiming();
|
|
+
|
|
+ this.prepare();
|
|
+
|
|
+ // this must be done here in case something schedules in the tick code
|
|
+ this.shortScheduled[this.shortScheduledIndex].clear();
|
|
+ this.shortScheduledIndex = getNextIndex(this.shortScheduledIndex, SHORT_SCHEDULE_TICK_THRESHOLD);
|
|
+
|
|
+ this.timingCleanup.stopTiming();
|
|
+ this.world.getMethodProfiler().exitEnter("ticking");
|
|
+ this.timingTicking.startTiming();
|
|
+
|
|
+ for (final NextTickListEntry<T> toTick : this.toTickThisTick) {
|
|
+ if (toTick.tickState != STATE_PENDING_TICK) {
|
|
+ // onTickEnd gets called at end of tick
|
|
+ continue;
|
|
+ }
|
|
+ try {
|
|
+ if (chunkProvider.isTickingReadyMainThread(toTick.getPosition())) {
|
|
+ toTick.tickState = STATE_TICKING;
|
|
+ this.tickFunction.accept(toTick);
|
|
+ if (toTick.tickState == STATE_TICKING) {
|
|
+ toTick.tickState = STATE_TICKED;
|
|
+ } // else it's STATE_CANCELLED_TICK
|
|
+ } else {
|
|
+ // re-schedule eventually
|
|
+ toTick.tickState = STATE_SCHEDULED;
|
|
+ this.addToNotTickingReady(toTick);
|
|
+ }
|
|
+ } catch (final Throwable thr) {
|
|
+ // start copy from TickListServer // TODO check on update
|
|
+ CrashReport crashreport = CrashReport.a(thr, "Exception while ticking");
|
|
+ CrashReportSystemDetails crashreportsystemdetails = crashreport.a("Block being ticked");
|
|
+
|
|
+ CrashReportSystemDetails.a(crashreportsystemdetails, toTick.getPosition(), (IBlockData) null);
|
|
+ throw new ReportedException(crashreport);
|
|
+ // end copy from TickListServer
|
|
+ }
|
|
+ }
|
|
+
|
|
+ this.timingTicking.stopTiming();
|
|
+ this.world.getMethodProfiler().exit();
|
|
+ this.timingFinished.startTiming();
|
|
+
|
|
+ // finished ticking, actual cleanup time
|
|
+ for (int i = 0, len = this.toTickThisTick.size(); i < len; ++i) {
|
|
+ final NextTickListEntry<T> entry = this.toTickThisTick.poll();
|
|
+ if (entry.tickState != STATE_SCHEDULED) {
|
|
+ // some entries get re-scheduled due to their chunk not being loaded/at correct status, so do not
|
|
+ // call onTickEnd for them
|
|
+ this.onTickEnd(entry);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ this.timingFinished.stopTiming();
|
|
+ }
|
|
+
|
|
+ private void onTickEnd(final NextTickListEntry<T> entry) {
|
|
+ if (entry.tickState == STATE_CANCELLED_TICK) {
|
|
+ return;
|
|
+ }
|
|
+ entry.tickState = STATE_UNSCHEDULED;
|
|
+
|
|
+ final BlockPosition pos = entry.getPosition();
|
|
+ final long blockKey = MCUtil.getBlockKey(pos);
|
|
+
|
|
+ final ArrayList<NextTickListEntry<T>> currentEntries = this.entriesByBlock.get(blockKey);
|
|
+
|
|
+ if (currentEntries.size() == 1) {
|
|
+ // it should contain our entry
|
|
+ this.entriesByBlock.remove(blockKey);
|
|
+ } else {
|
|
+ // it's more likely that this entry is at the start of the list than the end
|
|
+ for (int i = 0, len = currentEntries.size(); i < len; ++i) {
|
|
+ final NextTickListEntry<T> currentEntry = currentEntries.get(i);
|
|
+ if (currentEntry == entry) {
|
|
+ currentEntries.remove(i);
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final long chunkKey = MCUtil.getCoordinateKey(entry.getPosition());
|
|
+
|
|
+ ObjectRBTreeSet<NextTickListEntry<T>> set = this.entriesByChunk.get(chunkKey);
|
|
+
|
|
+ set.remove(entry);
|
|
+
|
|
+ if (set.isEmpty()) {
|
|
+ this.entriesByChunk.remove(chunkKey);
|
|
+ }
|
|
+
|
|
+ // already removed from longScheduled or shortScheduled
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isPendingTickThisTick(final BlockPosition blockposition, final T data) {
|
|
+ final ArrayList<NextTickListEntry<T>> entries = this.entriesByBlock.get(MCUtil.getBlockKey(blockposition));
|
|
+
|
|
+ if (entries == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ for (int i = 0, size = entries.size(); i < size; ++i) {
|
|
+ final NextTickListEntry<T> entry = entries.get(i);
|
|
+ if (entry.getData() == data && entry.tickState == STATE_PENDING_TICK) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isScheduledForTick(final BlockPosition blockposition, final T data) {
|
|
+ final ArrayList<NextTickListEntry<T>> entries = this.entriesByBlock.get(MCUtil.getBlockKey(blockposition));
|
|
+
|
|
+ if (entries == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ for (int i = 0, size = entries.size(); i < size; ++i) {
|
|
+ final NextTickListEntry<T> entry = entries.get(i);
|
|
+ if (entry.getData() == data && entry.tickState == STATE_SCHEDULED) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void schedule(BlockPosition blockPosition, T t, int i, TickListPriority tickListPriority) {
|
|
+ this.schedule(blockPosition, t, i + this.currentTick, tickListPriority);
|
|
+ }
|
|
+
|
|
+ public void schedule(final NextTickListEntry<T> entry) {
|
|
+ this.schedule(entry.getPosition(), entry.getData(), entry.getTargetTick(), entry.getPriority());
|
|
+ }
|
|
+
|
|
+ public void schedule(final BlockPosition pos, final T data, final long targetTick, final TickListPriority priority) {
|
|
+ final NextTickListEntry<T> entry = new NextTickListEntry<>(pos, data, targetTick, priority);
|
|
+ if (this.excludeFromScheduling.test(entry.getData())) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (WARN_ON_EXCESSIVE_DELAY) {
|
|
+ final long delay = entry.getTargetTick() - this.currentTick;
|
|
+ if (delay >= EXCESSIVE_DELAY_THRESHOLD) {
|
|
+ MinecraftServer.LOGGER.warn("Entry " + entry.toString() + " has been scheduled with an excessive delay of: " + delay, new Throwable());
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final long blockKey = MCUtil.getBlockKey(pos);
|
|
+
|
|
+ final ArrayList<NextTickListEntry<T>> currentEntries = this.entriesByBlock.computeIfAbsent(blockKey, (long keyInMap) -> new ArrayList<>(3));
|
|
+
|
|
+ if (currentEntries.isEmpty()) {
|
|
+ currentEntries.add(entry);
|
|
+ } else {
|
|
+ for (int i = 0, size = currentEntries.size(); i < size; ++i) {
|
|
+ final NextTickListEntry<T> currentEntry = currentEntries.get(i);
|
|
+
|
|
+ // entries are only blocked from scheduling if currentEntry.equals(toSchedule) && currentEntry is scheduled to tick (NOT including pending)
|
|
+ if (currentEntry.getData() == entry.getData() && currentEntry.tickState == STATE_SCHEDULED) {
|
|
+ // can't add
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ currentEntries.add(entry);
|
|
+ }
|
|
+
|
|
+ entry.tickState = STATE_SCHEDULED;
|
|
+
|
|
+ this.entriesByChunk.computeIfAbsent(MCUtil.getCoordinateKey(entry.getPosition()), (final long keyInMap) -> {
|
|
+ return new ObjectRBTreeSet<>(TickListServerInterval.ENTRY_COMPARATOR);
|
|
+ }).add(entry);
|
|
+
|
|
+ this.addToSchedule(entry);
|
|
+ }
|
|
+
|
|
+ public void scheduleAll(final Iterator<NextTickListEntry<T>> iterator) {
|
|
+ while (iterator.hasNext()) {
|
|
+ this.schedule(iterator.next());
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // this is not the standard interception calculation, but it's the one vanilla uses
|
|
+ // i.e the y value is ignored? the x, z calc isn't correct?
|
|
+ // however for the copy op they use the correct intersection, after using this one of course...
|
|
+ private static boolean isBlockInSortof(final StructureBoundingBox boundingBox, final BlockPosition pos) {
|
|
+ return pos.getX() >= boundingBox.getMinX() && pos.getX() < boundingBox.getMaxX() && pos.getZ() >= boundingBox.getMinZ() && pos.getZ() < boundingBox.getMaxZ();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public List<NextTickListEntry<T>> getEntriesInBoundingBox(final StructureBoundingBox structureboundingbox, final boolean removeReturned, final boolean excludeTicked) {
|
|
+ if (structureboundingbox.getMinX() == structureboundingbox.getMaxX() || structureboundingbox.getMinZ() == structureboundingbox.getMaxZ()) {
|
|
+ return Collections.emptyList(); // vanilla behaviour, check isBlockInSortof above
|
|
+ }
|
|
+
|
|
+ final int lowerChunkX = structureboundingbox.getMinX() >> 4;
|
|
+ final int upperChunkX = (structureboundingbox.getMaxX() - 1) >> 4; // subtract 1 since maxX is exclusive
|
|
+ final int lowerChunkZ = structureboundingbox.getMinZ() >> 4;
|
|
+ final int upperChunkZ = (structureboundingbox.getMaxZ() - 1) >> 4; // subtract 1 since maxZ is exclusive
|
|
+
|
|
+ final int xChunksLength = (upperChunkX - lowerChunkX + 1);
|
|
+ final int zChunksLength = (upperChunkZ - lowerChunkZ + 1);
|
|
+
|
|
+ final ObjectRBTreeSet<NextTickListEntry<T>>[] containingChunks = new ObjectRBTreeSet[xChunksLength * zChunksLength];
|
|
+
|
|
+ final int offset = (xChunksLength * -lowerChunkZ - lowerChunkX);
|
|
+ int totalEntries = 0;
|
|
+ for (int currChunkX = lowerChunkX; currChunkX <= upperChunkX; ++currChunkX) {
|
|
+ for (int currChunkZ = lowerChunkZ; currChunkZ <= upperChunkZ; ++currChunkZ) {
|
|
+ // todo optimize
|
|
+ //final int index = (currChunkX - lowerChunkX) + xChunksLength * (currChunkZ - lowerChunkZ);
|
|
+ final int index = offset + currChunkX + xChunksLength * currChunkZ;
|
|
+ final ObjectRBTreeSet<NextTickListEntry<T>> set = containingChunks[index] = this.entriesByChunk.get(MCUtil.getCoordinateKey(currChunkX, currChunkZ));
|
|
+ if (set != null) {
|
|
+ totalEntries += set.size();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final List<NextTickListEntry<T>> ret = new ArrayList<>(totalEntries);
|
|
+
|
|
+ final int matchOne = (STATE_SCHEDULED | STATE_PENDING_TICK) | (excludeTicked ? 0 : (STATE_TICKING | STATE_TICKED));
|
|
+
|
|
+ MCUtil.mergeSortedSets((NextTickListEntry<T> entry) -> {
|
|
+ if (!isBlockInSortof(structureboundingbox, entry.getPosition())) {
|
|
+ return;
|
|
+ }
|
|
+ final int tickState = entry.tickState;
|
|
+ if ((tickState & matchOne) == 0) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ ret.add(entry);
|
|
+ return;
|
|
+ }, TickListServerInterval.ENTRY_COMPARATOR, containingChunks);
|
|
+
|
|
+ if (removeReturned) {
|
|
+ for (NextTickListEntry<T> entry : ret) {
|
|
+ this.removeEntry(entry);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void copy(StructureBoundingBox structureboundingbox, BlockPosition blockposition) {
|
|
+ // start copy from TickListServer // TODO check on update
|
|
+ List<NextTickListEntry<T>> list = this.getEntriesInBoundingBox(structureboundingbox, false, false);
|
|
+ Iterator<NextTickListEntry<T>> iterator = list.iterator();
|
|
+
|
|
+ while (iterator.hasNext()) {
|
|
+ NextTickListEntry<T> nextticklistentry = iterator.next();
|
|
+
|
|
+ if (structureboundingbox.hasPoint( nextticklistentry.getPosition())) {
|
|
+ BlockPosition blockposition1 = nextticklistentry.getPosition().add(blockposition);
|
|
+ T t0 = nextticklistentry.getData();
|
|
+
|
|
+ this.schedule(new NextTickListEntry<>(blockposition1, t0, nextticklistentry.getTargetTick(), nextticklistentry.getPriority()));
|
|
+ }
|
|
+ }
|
|
+ // end copy from TickListServer
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public List<NextTickListEntry<T>> getEntriesInChunk(ChunkCoordIntPair chunkPos, boolean removeReturned, boolean excludeTicked) {
|
|
+ // Vanilla DOES get the entries 2 blocks out of the chunk too, but that doesn't matter since we ignore chunks
|
|
+ // not at ticking status, and ticking status requires neighbours loaded
|
|
+ // so with this method we will reduce scheduler churning
|
|
+ final int matchOne = (STATE_SCHEDULED | STATE_PENDING_TICK) | (excludeTicked ? 0 : (STATE_TICKING | STATE_TICKED));
|
|
+
|
|
+ final ObjectRBTreeSet<NextTickListEntry<T>> entries = this.entriesByChunk.get(MCUtil.getCoordinateKey(chunkPos));
|
|
+
|
|
+ if (entries == null) {
|
|
+ return Collections.emptyList();
|
|
+ }
|
|
+
|
|
+ final List<NextTickListEntry<T>> ret = new ArrayList<>(entries.size());
|
|
+
|
|
+ for (NextTickListEntry<T> entry : entries) {
|
|
+ if ((entry.tickState & matchOne) == 0) {
|
|
+ continue;
|
|
+ }
|
|
+ ret.add(entry);
|
|
+ }
|
|
+
|
|
+ if (removeReturned) {
|
|
+ for (NextTickListEntry<T> entry : ret) {
|
|
+ this.removeEntry(entry);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public NBTTagList serialize(ChunkCoordIntPair chunkcoordintpair) {
|
|
+ // start copy from TickListServer // TODO check on update
|
|
+ List<NextTickListEntry<T>> list = this.getEntriesInChunk(chunkcoordintpair, false, true);
|
|
+
|
|
+ return TickListServer.serialize(this.getMinecraftKeyFrom, list, this.currentTick);
|
|
+ // end copy from TickListServer
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public int getTotalScheduledEntries() {
|
|
+ // good thing this is only used in debug reports // TODO check on update
|
|
+ int ret = 0;
|
|
+
|
|
+ for (NextTickListEntry<T> entry : this.longScheduled) {
|
|
+ if (entry.tickState == STATE_SCHEDULED) {
|
|
+ ++ret;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for (Iterator<Long2ObjectMap.Entry<ArrayList<NextTickListEntry<T>>>> iterator = this.pendingChunkTickLoad.long2ObjectEntrySet().iterator(); iterator.hasNext();) {
|
|
+ ArrayList<NextTickListEntry<T>> list = iterator.next().getValue();
|
|
+
|
|
+ for (NextTickListEntry<T> entry : list) {
|
|
+ if (entry.tickState == STATE_SCHEDULED) {
|
|
+ ++ret;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for (TickListServerInterval<T> interval : this.shortScheduled) {
|
|
+ for (Iterable<NextTickListEntry<T>> set : interval.byPriority) {
|
|
+ for (NextTickListEntry<T> entry : set) {
|
|
+ if (entry.tickState == STATE_SCHEDULED) {
|
|
+ ++ret;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/com/destroystokyo/paper/server/ticklist/TickListServerInterval.java b/src/main/java/com/destroystokyo/paper/server/ticklist/TickListServerInterval.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..b58432a8b60670562baf00cf5279c702aaad4557
|
|
--- /dev/null
|
|
+++ b/src/main/java/com/destroystokyo/paper/server/ticklist/TickListServerInterval.java
|
|
@@ -0,0 +1,41 @@
|
|
+package com.destroystokyo.paper.server.ticklist;
|
|
+
|
|
+import com.destroystokyo.paper.util.set.LinkedSortedSet;
|
|
+import java.util.Comparator;
|
|
+import net.minecraft.world.level.NextTickListEntry;
|
|
+import net.minecraft.world.level.TickListPriority;
|
|
+
|
|
+// represents a set of entries to tick at a specified time
|
|
+public final class TickListServerInterval<T> {
|
|
+
|
|
+ public static final int TOTAL_PRIORITIES = TickListPriority.values().length;
|
|
+ public static final Comparator<NextTickListEntry<?>> ENTRY_COMPARATOR_BY_ID = (entry1, entry2) -> {
|
|
+ return Long.compare(entry1.getId(), entry2.getId());
|
|
+ };
|
|
+ public static final Comparator<NextTickListEntry<?>> ENTRY_COMPARATOR = (Comparator)NextTickListEntry.comparator();
|
|
+
|
|
+ // we do not record the interval, this class is meant to be used on a ring buffer
|
|
+
|
|
+ // inlined enum map for TickListPriority
|
|
+ public final LinkedSortedSet<NextTickListEntry<T>>[] byPriority = new LinkedSortedSet[TOTAL_PRIORITIES];
|
|
+
|
|
+ {
|
|
+ for (int i = 0, len = this.byPriority.length; i < len; ++i) {
|
|
+ this.byPriority[i] = new LinkedSortedSet<>(ENTRY_COMPARATOR_BY_ID);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void addEntryLast(final NextTickListEntry<T> entry) {
|
|
+ this.byPriority[entry.getPriority().ordinal()].addLast(entry);
|
|
+ }
|
|
+
|
|
+ public void addEntryFirst(final NextTickListEntry<T> entry) {
|
|
+ this.byPriority[entry.getPriority().ordinal()].addFirst(entry);
|
|
+ }
|
|
+
|
|
+ public void clear() {
|
|
+ for (int i = 0, len = this.byPriority.length; i < len; ++i) {
|
|
+ this.byPriority[i].clear(); // O(1) clear
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/com/destroystokyo/paper/util/set/LinkedSortedSet.java b/src/main/java/com/destroystokyo/paper/util/set/LinkedSortedSet.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..118988c39e58f28e8a2851792b9c014f341f06fc
|
|
--- /dev/null
|
|
+++ b/src/main/java/com/destroystokyo/paper/util/set/LinkedSortedSet.java
|
|
@@ -0,0 +1,142 @@
|
|
+package com.destroystokyo.paper.util.set;
|
|
+
|
|
+import java.util.Comparator;
|
|
+import java.util.Iterator;
|
|
+import java.util.NoSuchElementException;
|
|
+
|
|
+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;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Iterator<E> iterator() {
|
|
+ return new Iterator<E>() {
|
|
+
|
|
+ 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 boolean 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;
|
|
+ }
|
|
+ this.head = prev.prev = new Link<>(element, null, prev);
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ 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 true;
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ } else {
|
|
+ this.head = this.tail = new Link<>(element);
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public boolean 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;
|
|
+ }
|
|
+ this.tail = prev.next = new Link<>(element, prev, null);
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ 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 true;
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ } else {
|
|
+ this.head = this.tail = new Link<>(element);
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static final class Link<E> {
|
|
+ public E element;
|
|
+ public Link<E> prev;
|
|
+ public Link<E> next;
|
|
+
|
|
+ public Link() {}
|
|
+
|
|
+ public Link(final E element) {
|
|
+ this.element = element;
|
|
+ }
|
|
+
|
|
+ public 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/core/BlockPosition.java b/src/main/java/net/minecraft/core/BlockPosition.java
|
|
index 8c0aeb51f5e230fd6109e750732eb54559bc9637..1fb931d4c0720a5e496030e25c865771aea3ec70 100644
|
|
--- a/src/main/java/net/minecraft/core/BlockPosition.java
|
|
+++ b/src/main/java/net/minecraft/core/BlockPosition.java
|
|
@@ -111,6 +111,7 @@ public class BlockPosition extends BaseBlockPosition {
|
|
return i == 0 && j == 0 && k == 0 ? this : new BlockPosition(this.getX() + i, this.getY() + j, this.getZ() + k);
|
|
}
|
|
|
|
+ public final BlockPosition add(BaseBlockPosition baseblockposition) { return this.a(baseblockposition); } // Paper - OBFHELPER
|
|
public BlockPosition a(BaseBlockPosition baseblockposition) {
|
|
return this.b(baseblockposition.getX(), baseblockposition.getY(), baseblockposition.getZ());
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/level/ChunkProviderServer.java b/src/main/java/net/minecraft/server/level/ChunkProviderServer.java
|
|
index 5caf2121f2e551d7b4f0ddc74f10a44dc28ac62b..214098d7ad95839e90b0ec9ea997930c27b4bea4 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ChunkProviderServer.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ChunkProviderServer.java
|
|
@@ -21,6 +21,7 @@ import net.minecraft.SystemUtils;
|
|
import net.minecraft.core.BlockPosition;
|
|
import net.minecraft.core.SectionPosition;
|
|
import net.minecraft.network.protocol.Packet;
|
|
+import net.minecraft.server.MCUtil;
|
|
import net.minecraft.server.level.progress.WorldLoadListener;
|
|
import net.minecraft.util.MathHelper;
|
|
import net.minecraft.util.profiling.GameProfilerFiller;
|
|
@@ -217,6 +218,13 @@ public class ChunkProviderServer extends IChunkProvider {
|
|
}
|
|
// Paper end
|
|
|
|
+ // Paper start - rewrite ticklistserver
|
|
+ public final boolean isTickingReadyMainThread(BlockPosition pos) {
|
|
+ PlayerChunk chunk = this.playerChunkMap.getUpdatingChunk(MCUtil.getCoordinateKey(pos));
|
|
+ return chunk != null && chunk.isTickingReady();
|
|
+ }
|
|
+ // Paper end - rewrite ticklistserver
|
|
+
|
|
public ChunkProviderServer(WorldServer worldserver, Convertable.ConversionSession convertable_conversionsession, DataFixer datafixer, DefinedStructureManager definedstructuremanager, Executor executor, ChunkGenerator chunkgenerator, int i, boolean flag, WorldLoadListener worldloadlistener, Supplier<WorldPersistentData> supplier) {
|
|
this.world = worldserver;
|
|
this.serverThreadQueue = new ChunkProviderServer.a(worldserver);
|
|
diff --git a/src/main/java/net/minecraft/server/level/PlayerChunk.java b/src/main/java/net/minecraft/server/level/PlayerChunk.java
|
|
index 6433463938d8bb717840c8f57fe6e7079e1030f2..445dba8ed210407664904b707c36c78a76f25510 100644
|
|
--- a/src/main/java/net/minecraft/server/level/PlayerChunk.java
|
|
+++ b/src/main/java/net/minecraft/server/level/PlayerChunk.java
|
|
@@ -496,7 +496,9 @@ public class PlayerChunk {
|
|
PlayerChunk.this.isTickingReady = true;
|
|
|
|
|
|
-
|
|
+ // Paper start - rewrite ticklistserver
|
|
+ PlayerChunk.this.chunkMap.world.onChunkSetTicking(PlayerChunk.this.location.x, PlayerChunk.this.location.z);
|
|
+ // Paper end - rewrite ticklistserver
|
|
|
|
}
|
|
});
|
|
diff --git a/src/main/java/net/minecraft/server/level/WorldServer.java b/src/main/java/net/minecraft/server/level/WorldServer.java
|
|
index 82f9c49233acce3c95d8a07b2a1b187d5c57552d..7fc75749e4983582275f794ed152af7cffce910f 100644
|
|
--- a/src/main/java/net/minecraft/server/level/WorldServer.java
|
|
+++ b/src/main/java/net/minecraft/server/level/WorldServer.java
|
|
@@ -298,6 +298,15 @@ public class WorldServer extends World implements GeneratorAccessSeed {
|
|
}
|
|
// Paper end
|
|
|
|
+ // Paper start - rewrite ticklistserver
|
|
+ void onChunkSetTicking(int chunkX, int chunkZ) {
|
|
+ if (com.destroystokyo.paper.PaperConfig.useOptimizedTickList) {
|
|
+ ((com.destroystokyo.paper.server.ticklist.PaperTickList) this.nextTickListBlock).onChunkSetTicking(chunkX, chunkZ);
|
|
+ ((com.destroystokyo.paper.server.ticklist.PaperTickList) this.nextTickListFluid).onChunkSetTicking(chunkX, chunkZ);
|
|
+ }
|
|
+ }
|
|
+ // Paper end - rewrite ticklistserver
|
|
+
|
|
// Add env and gen to constructor, WorldData -> WorldDataServer
|
|
public WorldServer(MinecraftServer minecraftserver, Executor executor, Convertable.ConversionSession convertable_conversionsession, IWorldDataServer iworlddataserver, ResourceKey<World> resourcekey, DimensionManager dimensionmanager, WorldLoadListener worldloadlistener, ChunkGenerator chunkgenerator, boolean flag, long i, List<MobSpawner> list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen) {
|
|
super(iworlddataserver, resourcekey, dimensionmanager, minecraftserver::getMethodProfiler, false, flag, i, gen, env, executor); // Paper pass executor
|
|
@@ -305,12 +314,21 @@ public class WorldServer extends World implements GeneratorAccessSeed {
|
|
convertable = convertable_conversionsession;
|
|
uuid = WorldUUID.getUUID(convertable_conversionsession.folder.toFile());
|
|
// CraftBukkit end
|
|
- this.nextTickListBlock = new TickListServer<>(this, (block) -> {
|
|
- return block == null || block.getBlockData().isAir();
|
|
- }, IRegistry.BLOCK::getKey, this::b, "Blocks"); // Paper - Timings
|
|
- this.nextTickListFluid = new TickListServer<>(this, (fluidtype) -> {
|
|
- return fluidtype == null || fluidtype == FluidTypes.EMPTY;
|
|
- }, IRegistry.FLUID::getKey, this::a, "Fluids"); // Paper - Timings
|
|
+ if (com.destroystokyo.paper.PaperConfig.useOptimizedTickList) {
|
|
+ this.nextTickListBlock = new com.destroystokyo.paper.server.ticklist.PaperTickList<>(this, (block) -> {
|
|
+ return block == null || block.getBlockData().isAir();
|
|
+ }, IRegistry.BLOCK::getKey, this::b, "Blocks"); // Paper - Timings
|
|
+ this.nextTickListFluid = new com.destroystokyo.paper.server.ticklist.PaperTickList<>(this, (fluidtype) -> {
|
|
+ return fluidtype == null || fluidtype == FluidTypes.EMPTY;
|
|
+ }, IRegistry.FLUID::getKey, this::a, "Fluids"); // Paper - Timings
|
|
+ } else {
|
|
+ this.nextTickListBlock = new TickListServer<>(this, (block) -> {
|
|
+ return block == null || block.getBlockData().isAir();
|
|
+ }, IRegistry.BLOCK::getKey, this::b, "Blocks"); // Paper - Timings
|
|
+ this.nextTickListFluid = new TickListServer<>(this, (fluidtype) -> {
|
|
+ return fluidtype == null || fluidtype == FluidTypes.EMPTY;
|
|
+ }, IRegistry.FLUID::getKey, this::a, "Fluids"); // Paper - Timings
|
|
+ }
|
|
this.navigators = Sets.newHashSet();
|
|
this.L = new ObjectLinkedOpenHashSet();
|
|
this.Q = flag1;
|
|
@@ -645,7 +663,9 @@ public class WorldServer extends World implements GeneratorAccessSeed {
|
|
if (this.Q) {
|
|
long i = this.worldData.getTime() + 1L;
|
|
|
|
- this.worldDataServer.setTime(i);
|
|
+ this.worldDataServer.setTime(i); // Paper - diff on change, we want the below to be ran right after this
|
|
+ this.nextTickListBlock.nextTick(); // Paper
|
|
+ this.nextTickListFluid.nextTick(); // Paper
|
|
this.worldDataServer.u().a(this.server, i);
|
|
if (this.worldData.q().getBoolean(GameRules.DO_DAYLIGHT_CYCLE)) {
|
|
this.setDayTime(this.worldData.getDayTime() + 1L);
|
|
diff --git a/src/main/java/net/minecraft/world/level/NextTickListEntry.java b/src/main/java/net/minecraft/world/level/NextTickListEntry.java
|
|
index 37b7dd82a227a88b720c13a813dd7e8caf803e03..8eb3084def3aa8776d32f8a3c942c95d24ccea3f 100644
|
|
--- a/src/main/java/net/minecraft/world/level/NextTickListEntry.java
|
|
+++ b/src/main/java/net/minecraft/world/level/NextTickListEntry.java
|
|
@@ -6,11 +6,13 @@ import net.minecraft.core.BlockPosition;
|
|
public class NextTickListEntry<T> {
|
|
|
|
private static final java.util.concurrent.atomic.AtomicLong COUNTER = new java.util.concurrent.atomic.AtomicLong(); // Paper - async chunk loading
|
|
- private final T e;
|
|
- public final BlockPosition a;
|
|
- public final long b;
|
|
- public final TickListPriority c;
|
|
- private final long f;
|
|
+ private final T e; public final T getData() { return this.e; } // Paper - OBFHELPER
|
|
+ public final BlockPosition a; public final BlockPosition getPosition() { return this.a; } // Paper - OBFHELPER
|
|
+ public final long b; public final long getTargetTick() { return this.b; } // Paper - OBFHELPER
|
|
+ public final TickListPriority c; public final TickListPriority getPriority() { return this.c; } // Paper - OBFHELPER
|
|
+ private final long f; public final long getId() { return this.f; } // Paper - OBFHELPER
|
|
+ private final int hash; // Paper
|
|
+ public int tickState; // Paper
|
|
|
|
public NextTickListEntry(BlockPosition blockposition, T t0) {
|
|
this(blockposition, t0, 0L, TickListPriority.NORMAL);
|
|
@@ -22,6 +24,7 @@ public class NextTickListEntry<T> {
|
|
this.e = t0;
|
|
this.b = i;
|
|
this.c = ticklistpriority;
|
|
+ this.hash = this.computeHash(); // Paper
|
|
}
|
|
|
|
public boolean equals(Object object) {
|
|
@@ -34,19 +37,31 @@ public class NextTickListEntry<T> {
|
|
}
|
|
}
|
|
|
|
+ // Paper start - optimize hashcode
|
|
+ @Override
|
|
public int hashCode() {
|
|
+ return this.hash;
|
|
+ }
|
|
+ public final int computeHash() {
|
|
+ // Paper end - optimize hashcode
|
|
return this.a.hashCode();
|
|
}
|
|
|
|
- public static <T> Comparator<Object> a() { // Paper - decompile fix
|
|
- return Comparator.comparingLong((nextticklistentry) -> {
|
|
- return ((NextTickListEntry<T>) nextticklistentry).b; // Paper - decompile fix
|
|
- }).thenComparing((nextticklistentry) -> {
|
|
- return ((NextTickListEntry<T>) nextticklistentry).c; // Paper - decompile fix
|
|
- }).thenComparingLong((nextticklistentry) -> {
|
|
- return ((NextTickListEntry<T>) nextticklistentry).f; // Paper - decompile fix
|
|
- });
|
|
+ // Paper start - let's not use more functional code for no reason.
|
|
+ public static <T> Comparator<Object> comparator() { return NextTickListEntry.a(); } // Paper - OBFHELPER
|
|
+ public static <T> Comparator<Object> a() {
|
|
+ return (Comparator)(Comparator<NextTickListEntry>)(NextTickListEntry nextticklistentry, NextTickListEntry nextticklistentry1) -> {
|
|
+ int i = Long.compare(nextticklistentry.getTargetTick(), nextticklistentry1.getTargetTick());
|
|
+
|
|
+ if (i != 0) {
|
|
+ return i;
|
|
+ } else {
|
|
+ i = nextticklistentry.getPriority().compareTo(nextticklistentry1.getPriority());
|
|
+ return i != 0 ? i : Long.compare(nextticklistentry.getId(), nextticklistentry1.getId());
|
|
+ }
|
|
+ };
|
|
}
|
|
+ // Paper end - let's not use more functional code for no reason.
|
|
|
|
public String toString() {
|
|
return this.e + ": " + this.a + ", " + this.b + ", " + this.c + ", " + this.f;
|
|
diff --git a/src/main/java/net/minecraft/world/level/TickListChunk.java b/src/main/java/net/minecraft/world/level/TickListChunk.java
|
|
index c3cb513d0d107ecb43e98960b25054626aa6a03f..fd293e11ec62a41a53c1e5238cb1219349d446d4 100644
|
|
--- a/src/main/java/net/minecraft/world/level/TickListChunk.java
|
|
+++ b/src/main/java/net/minecraft/world/level/TickListChunk.java
|
|
@@ -9,6 +9,7 @@ import net.minecraft.core.BlockPosition;
|
|
import net.minecraft.nbt.NBTTagCompound;
|
|
import net.minecraft.nbt.NBTTagList;
|
|
import net.minecraft.resources.MinecraftKey;
|
|
+import net.minecraft.server.MinecraftServer;
|
|
|
|
public class TickListChunk<T> implements TickList<T> {
|
|
|
|
@@ -61,6 +62,8 @@ public class TickListChunk<T> implements TickList<T> {
|
|
return nbttaglist;
|
|
}
|
|
|
|
+ private static final int MAX_TICK_DELAY = Integer.getInteger("paper.ticklist-max-tick-delay", -1).intValue(); // Paper - clean up broken entries
|
|
+
|
|
public static <T> TickListChunk<T> a(NBTTagList nbttaglist, Function<T, MinecraftKey> function, Function<MinecraftKey, T> function1) {
|
|
List<TickListChunk.a<T>> list = Lists.newArrayList();
|
|
|
|
@@ -71,7 +74,14 @@ public class TickListChunk<T> implements TickList<T> {
|
|
if (t0 != null) {
|
|
BlockPosition blockposition = new BlockPosition(nbttagcompound.getInt("x"), nbttagcompound.getInt("y"), nbttagcompound.getInt("z"));
|
|
|
|
- list.add(new TickListChunk.a<>(t0, blockposition, nbttagcompound.getInt("t"), TickListPriority.a(nbttagcompound.getInt("p"))));
|
|
+ // Paper start - clean up broken entries
|
|
+ int delay = nbttagcompound.getInt("t");
|
|
+ if (MAX_TICK_DELAY > 0 && delay > MAX_TICK_DELAY) {
|
|
+ MinecraftServer.LOGGER.warn("Dropping tick for pos " + blockposition + ", tick delay " + delay);
|
|
+ continue;
|
|
+ }
|
|
+ list.add(new TickListChunk.a<>(t0, blockposition, delay, TickListPriority.a(nbttagcompound.getInt("p"))));
|
|
+ // Paper end - clean up broken entries
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/TickListServer.java b/src/main/java/net/minecraft/world/level/TickListServer.java
|
|
index c221e5caf518b8c588390e438346fa58fa8c5a38..4fd89bbe6ce578fd3a166bcfbbe41908a7bb4753 100644
|
|
--- a/src/main/java/net/minecraft/world/level/TickListServer.java
|
|
+++ b/src/main/java/net/minecraft/world/level/TickListServer.java
|
|
@@ -50,7 +50,16 @@ public class TickListServer<T> implements TickList<T> {
|
|
private final co.aikar.timings.Timing timingTicking; // Paper
|
|
// Paper end
|
|
|
|
+ // Paper start
|
|
+ public void nextTick() {}
|
|
+ // Paper end
|
|
+
|
|
public void b() {
|
|
+ // Paper start - allow overriding
|
|
+ this.tick();
|
|
+ }
|
|
+ public void tick() {
|
|
+ // Paper end
|
|
int i = this.nextTickList.size();
|
|
|
|
if (false) { // CraftBukkit
|
|
@@ -118,10 +127,20 @@ public class TickListServer<T> implements TickList<T> {
|
|
|
|
@Override
|
|
public boolean b(BlockPosition blockposition, T t0) {
|
|
+ // Paper start - allow overriding
|
|
+ return this.isPendingTickThisTick(blockposition, t0);
|
|
+ }
|
|
+ public boolean isPendingTickThisTick(BlockPosition blockposition, T t0) {
|
|
+ // Paper end
|
|
return this.f.contains(new NextTickListEntry<>(blockposition, t0));
|
|
}
|
|
|
|
public List<NextTickListEntry<T>> a(ChunkCoordIntPair chunkcoordintpair, boolean flag, boolean flag1) {
|
|
+ // Paper start - allow overriding
|
|
+ return this.getEntriesInChunk(chunkcoordintpair, flag, flag1);
|
|
+ }
|
|
+ public List<NextTickListEntry<T>> getEntriesInChunk(ChunkCoordIntPair chunkcoordintpair, boolean flag, boolean flag1) {
|
|
+ // Paper end
|
|
int i = (chunkcoordintpair.x << 4) - 2;
|
|
int j = i + 16 + 2;
|
|
int k = (chunkcoordintpair.z << 4) - 2;
|
|
@@ -131,6 +150,11 @@ public class TickListServer<T> implements TickList<T> {
|
|
}
|
|
|
|
public List<NextTickListEntry<T>> a(StructureBoundingBox structureboundingbox, boolean flag, boolean flag1) {
|
|
+ // Paper start - allow overriding
|
|
+ return this.getEntriesInBoundingBox(structureboundingbox, flag, flag1);
|
|
+ }
|
|
+ public List<NextTickListEntry<T>> getEntriesInBoundingBox(StructureBoundingBox structureboundingbox, boolean flag, boolean flag1) {
|
|
+ // Paper end
|
|
List<NextTickListEntry<T>> list = this.a((List) null, this.nextTickList, structureboundingbox, flag);
|
|
|
|
if (flag && list != null) {
|
|
@@ -170,6 +194,11 @@ public class TickListServer<T> implements TickList<T> {
|
|
}
|
|
|
|
public void a(StructureBoundingBox structureboundingbox, BlockPosition blockposition) {
|
|
+ // Paper start - allow overriding
|
|
+ this.copy(structureboundingbox, blockposition);
|
|
+ }
|
|
+ public void copy(StructureBoundingBox structureboundingbox, BlockPosition blockposition) {
|
|
+ // Paper end
|
|
List<NextTickListEntry<T>> list = this.a(structureboundingbox, false, false);
|
|
Iterator iterator = list.iterator();
|
|
|
|
@@ -187,11 +216,17 @@ public class TickListServer<T> implements TickList<T> {
|
|
}
|
|
|
|
public NBTTagList a(ChunkCoordIntPair chunkcoordintpair) {
|
|
+ // Paper start - allow overriding
|
|
+ return this.serialize(chunkcoordintpair);
|
|
+ }
|
|
+ public NBTTagList serialize(ChunkCoordIntPair chunkcoordintpair) {
|
|
+ // Paper end
|
|
List<NextTickListEntry<T>> list = this.a(chunkcoordintpair, false, true);
|
|
|
|
return a(this.b, list, this.e.getTime());
|
|
}
|
|
|
|
+ public static <T> NBTTagList serialize(Function<T, MinecraftKey> function, Iterable<NextTickListEntry<T>> iterable, long i) { return TickListServer.a(function, iterable, i); } // Paper - OBFHELPER
|
|
private static <T> NBTTagList a(Function<T, MinecraftKey> function, Iterable<NextTickListEntry<T>> iterable, long i) {
|
|
NBTTagList nbttaglist = new NBTTagList();
|
|
Iterator iterator = iterable.iterator();
|
|
@@ -214,11 +249,21 @@ public class TickListServer<T> implements TickList<T> {
|
|
|
|
@Override
|
|
public boolean a(BlockPosition blockposition, T t0) {
|
|
+ // Paper start - allow overriding
|
|
+ return this.isScheduledForTick(blockposition, t0);
|
|
+ }
|
|
+ public boolean isScheduledForTick(BlockPosition blockposition, T t0) {
|
|
+ // Paper end
|
|
return this.nextTickListHash.contains(new NextTickListEntry<>(blockposition, t0));
|
|
}
|
|
|
|
@Override
|
|
public void a(BlockPosition blockposition, T t0, int i, TickListPriority ticklistpriority) {
|
|
+ // Paper start - allow overriding
|
|
+ this.schedule(blockposition, t0, i, ticklistpriority);
|
|
+ }
|
|
+ public void schedule(BlockPosition blockposition, T t0, int i, TickListPriority ticklistpriority) {
|
|
+ // Paper end
|
|
if (!this.a.test(t0)) {
|
|
this.a(new NextTickListEntry<>(blockposition, t0, (long) i + this.e.getTime(), ticklistpriority));
|
|
}
|
|
@@ -234,6 +279,11 @@ public class TickListServer<T> implements TickList<T> {
|
|
}
|
|
|
|
public int a() {
|
|
+ // Paper start - allow overriding
|
|
+ return this.getTotalScheduledEntries();
|
|
+ }
|
|
+ public int getTotalScheduledEntries() {
|
|
+ // Paper end
|
|
return this.nextTickListHash.size();
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureBoundingBox.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureBoundingBox.java
|
|
index b5d6c8163c686c31375fb645d7721af06c01df40..fb4b8d7167ad7f1d24d40bbbda5f52e278f25895 100644
|
|
--- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureBoundingBox.java
|
|
+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureBoundingBox.java
|
|
@@ -8,12 +8,12 @@ import net.minecraft.nbt.NBTTagIntArray;
|
|
|
|
public class StructureBoundingBox {
|
|
|
|
- public int a;
|
|
- public int b;
|
|
- public int c;
|
|
- public int d;
|
|
- public int e;
|
|
- public int f;
|
|
+ public int a; public final int getMinX() { return this.a; } // Paper - OBFHELPER
|
|
+ public int b; public final int getMinY() { return this.b; } // Paper - OBFHELPER
|
|
+ public int c; public final int getMinZ() { return this.c; } // Paper - OBFHELPER
|
|
+ public int d; public final int getMaxX() { return this.d; } // Paper - OBFHELPER
|
|
+ public int e; public final int getMaxY() { return this.e; } // Paper - OBFHELPER
|
|
+ public int f; public final int getMaxZ() { return this.f; } // Paper - OBFHELPER
|
|
|
|
public StructureBoundingBox() {}
|
|
|
|
@@ -92,6 +92,7 @@ public class StructureBoundingBox {
|
|
this.e = 512;
|
|
}
|
|
|
|
+ public final boolean intersects(StructureBoundingBox boundingBox) { return this.b(boundingBox); } // Paper - OBFHELPER
|
|
public boolean b(StructureBoundingBox structureboundingbox) {
|
|
return this.d >= structureboundingbox.a && this.a <= structureboundingbox.d && this.f >= structureboundingbox.c && this.c <= structureboundingbox.f && this.e >= structureboundingbox.b && this.b <= structureboundingbox.e;
|
|
}
|
|
@@ -126,6 +127,7 @@ public class StructureBoundingBox {
|
|
this.a(baseblockposition.getX(), baseblockposition.getY(), baseblockposition.getZ());
|
|
}
|
|
|
|
+ public final boolean hasPoint(BaseBlockPosition baseblockposition) { return this.b(baseblockposition); } // Paper - OBFHELPER
|
|
public boolean b(BaseBlockPosition baseblockposition) {
|
|
return baseblockposition.getX() >= this.a && baseblockposition.getX() <= this.d && baseblockposition.getZ() >= this.c && baseblockposition.getZ() <= this.f && baseblockposition.getY() >= this.b && baseblockposition.getY() <= this.e;
|
|
}
|