1302 lines
59 KiB
Diff
1302 lines
59 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..1ce556fb808a8c93106e26da7b24f9a2d579909b
|
|
--- /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.CrashReportCategory;
|
|
+import net.minecraft.ReportedException;
|
|
+import net.minecraft.core.BlockPos;
|
|
+import net.minecraft.nbt.ListTag;
|
|
+import net.minecraft.resources.ResourceLocation;
|
|
+import net.minecraft.server.MCUtil;
|
|
+import net.minecraft.server.MinecraftServer;
|
|
+import net.minecraft.server.level.ServerChunkCache;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.world.level.ChunkPos;
|
|
+import net.minecraft.world.level.ServerTickList;
|
|
+import net.minecraft.world.level.TickNextTickData;
|
|
+import net.minecraft.world.level.TickPriority;
|
|
+import net.minecraft.world.level.block.state.BlockState;
|
|
+import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
|
+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;
|
|
+
|
|
+public final class PaperTickList<T> extends ServerTickList<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 ServerLevel world;
|
|
+ private final Predicate<T> excludeFromScheduling;
|
|
+ private final Function<T, ResourceLocation> getMinecraftKeyFrom;
|
|
+ //private final Function<MinecraftKey, T> getObjectFronMinecraftKey;
|
|
+ private final Consumer<TickNextTickData<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<TickNextTickData<T>>> entriesByBlock = new Long2ObjectOpenHashMap<>(1024, 0.25f);
|
|
+ private final Long2ObjectOpenHashMap<ObjectRBTreeSet<TickNextTickData<T>>> entriesByChunk = new Long2ObjectOpenHashMap<>(1024, 0.25f);
|
|
+ private final Long2ObjectOpenHashMap<ArrayList<TickNextTickData<T>>> pendingChunkTickLoad = new Long2ObjectOpenHashMap<>(1024, 0.5f);
|
|
+
|
|
+ // fastutil has O(1) first/last while TreeMap/TreeSet are log(n)
|
|
+ private final ObjectRBTreeSet<TickNextTickData<T>> longScheduled = new ObjectRBTreeSet<>(TickListServerInterval.ENTRY_COMPARATOR);
|
|
+
|
|
+ private final ArrayDeque<TickNextTickData<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 ServerLevel world, final Predicate<T> excludeFromScheduling, final Function<T, ResourceLocation> getMinecraftKeyFrom,
|
|
+ final Consumer<TickNextTickData<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.getGameTime();
|
|
+ }
|
|
+
|
|
+ private void queueEntryForTick(final TickNextTickData<T> entry, final ServerChunkCache 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 TickNextTickData<T> entry) {
|
|
+ this.pendingChunkTickLoad.computeIfAbsent(MCUtil.getCoordinateKey(entry.getPosition()), (long keyInMap) -> {
|
|
+ return new ArrayList<>();
|
|
+ }).add(entry);
|
|
+ }
|
|
+
|
|
+ private void addToSchedule(final TickNextTickData<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 TickNextTickData<T> entry) {
|
|
+ entry.tickState = STATE_CANCELLED_TICK;
|
|
+ // short/long scheduled will skip the entry
|
|
+
|
|
+ final BlockPos pos = entry.getPosition();
|
|
+ final long blockKey = MCUtil.getBlockKey(pos);
|
|
+
|
|
+ final ArrayList<TickNextTickData<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 TickNextTickData<T> currentEntry = currentEntries.get(i);
|
|
+ if (currentEntry == entry) {
|
|
+ currentEntries.remove(i);
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final long chunkKey = MCUtil.getCoordinateKey(entry.getPosition());
|
|
+
|
|
+ ObjectRBTreeSet<TickNextTickData<T>> set = this.entriesByChunk.get(chunkKey);
|
|
+
|
|
+ set.remove(entry);
|
|
+
|
|
+ if (set.isEmpty()) {
|
|
+ this.entriesByChunk.remove(chunkKey);
|
|
+ }
|
|
+
|
|
+ ArrayList<TickNextTickData<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<TickNextTickData<T>> pending = this.pendingChunkTickLoad.remove(MCUtil.getCoordinateKey(chunkX, chunkZ));
|
|
+ if (pending == null) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ for (int i = 0, size = pending.size(); i < size; ++i) {
|
|
+ final TickNextTickData<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 ServerChunkCache chunkProvider = this.world.getChunkSource();
|
|
+
|
|
+ // 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<TickNextTickData<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<TickNextTickData<T>> comparator = (Comparator)TickListServerInterval.ENTRY_COMPARATOR;
|
|
+ final Iterator<TickNextTickData<T>> longScheduledIterator = this.longScheduled.iterator();
|
|
+ TickNextTickData<T> longCurrent = longScheduledIterator.next();
|
|
+
|
|
+ for (int i = 0, len = interval.byPriority.length; i < len; ++i) {
|
|
+ for (final Iterator<TickNextTickData<T>> iterator = interval.byPriority[i].iterator(); iterator.hasNext();) {
|
|
+ final TickNextTickData<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.getGameTime()) {
|
|
+ if (!this.warnedAboutDesync) {
|
|
+ this.warnedAboutDesync = true;
|
|
+ MinecraftServer.LOGGER.error("World tick desync detected! Expected " + this.currentTick + " ticks, but got " + this.world.getGameTime() + " 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 ServerChunkCache chunkProvider = this.world.getChunkSource();
|
|
+
|
|
+ this.world.getProfiler().push("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.getProfiler().popPush("ticking");
|
|
+ this.timingTicking.startTiming();
|
|
+
|
|
+ for (final TickNextTickData<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.forThrowable(thr, "Exception while ticking");
|
|
+ CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Block being ticked");
|
|
+
|
|
+ CrashReportCategory.populateBlockDetails(crashreportsystemdetails, toTick.getPosition(), (BlockState) null);
|
|
+ throw new ReportedException(crashreport);
|
|
+ // end copy from TickListServer
|
|
+ }
|
|
+ }
|
|
+
|
|
+ this.timingTicking.stopTiming();
|
|
+ this.world.getProfiler().pop();
|
|
+ this.timingFinished.startTiming();
|
|
+
|
|
+ // finished ticking, actual cleanup time
|
|
+ for (int i = 0, len = this.toTickThisTick.size(); i < len; ++i) {
|
|
+ final TickNextTickData<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 TickNextTickData<T> entry) {
|
|
+ if (entry.tickState == STATE_CANCELLED_TICK) {
|
|
+ return;
|
|
+ }
|
|
+ entry.tickState = STATE_UNSCHEDULED;
|
|
+
|
|
+ final BlockPos pos = entry.getPosition();
|
|
+ final long blockKey = MCUtil.getBlockKey(pos);
|
|
+
|
|
+ final ArrayList<TickNextTickData<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 TickNextTickData<T> currentEntry = currentEntries.get(i);
|
|
+ if (currentEntry == entry) {
|
|
+ currentEntries.remove(i);
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final long chunkKey = MCUtil.getCoordinateKey(entry.getPosition());
|
|
+
|
|
+ ObjectRBTreeSet<TickNextTickData<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 BlockPos blockposition, final T data) {
|
|
+ final ArrayList<TickNextTickData<T>> entries = this.entriesByBlock.get(MCUtil.getBlockKey(blockposition));
|
|
+
|
|
+ if (entries == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ for (int i = 0, size = entries.size(); i < size; ++i) {
|
|
+ final TickNextTickData<T> entry = entries.get(i);
|
|
+ if (entry.getData() == data && entry.tickState == STATE_PENDING_TICK) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isScheduledForTick(final BlockPos blockposition, final T data) {
|
|
+ final ArrayList<TickNextTickData<T>> entries = this.entriesByBlock.get(MCUtil.getBlockKey(blockposition));
|
|
+
|
|
+ if (entries == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ for (int i = 0, size = entries.size(); i < size; ++i) {
|
|
+ final TickNextTickData<T> entry = entries.get(i);
|
|
+ if (entry.getData() == data && entry.tickState == STATE_SCHEDULED) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void schedule(BlockPos blockPosition, T t, int i, TickPriority tickListPriority) {
|
|
+ this.schedule(blockPosition, t, i + this.currentTick, tickListPriority);
|
|
+ }
|
|
+
|
|
+ public void schedule(final TickNextTickData<T> entry) {
|
|
+ this.schedule(entry.getPosition(), entry.getData(), entry.getTargetTick(), entry.getPriority());
|
|
+ }
|
|
+
|
|
+ public void schedule(final BlockPos pos, final T data, final long targetTick, final TickPriority priority) {
|
|
+ final TickNextTickData<T> entry = new TickNextTickData<>(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<TickNextTickData<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 TickNextTickData<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<TickNextTickData<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 BoundingBox boundingBox, final BlockPos pos) {
|
|
+ return pos.getX() >= boundingBox.getMinX() && pos.getX() < boundingBox.getMaxX() && pos.getZ() >= boundingBox.getMinZ() && pos.getZ() < boundingBox.getMaxZ();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public List<TickNextTickData<T>> getEntriesInBoundingBox(final BoundingBox 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<TickNextTickData<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<TickNextTickData<T>> set = containingChunks[index] = this.entriesByChunk.get(MCUtil.getCoordinateKey(currChunkX, currChunkZ));
|
|
+ if (set != null) {
|
|
+ totalEntries += set.size();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final List<TickNextTickData<T>> ret = new ArrayList<>(totalEntries);
|
|
+
|
|
+ final int matchOne = (STATE_SCHEDULED | STATE_PENDING_TICK) | (excludeTicked ? 0 : (STATE_TICKING | STATE_TICKED));
|
|
+
|
|
+ MCUtil.mergeSortedSets((TickNextTickData<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 (TickNextTickData<T> entry : ret) {
|
|
+ this.removeEntry(entry);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void copy(BoundingBox structureboundingbox, BlockPos blockposition) {
|
|
+ // start copy from TickListServer // TODO check on update
|
|
+ List<TickNextTickData<T>> list = this.getEntriesInBoundingBox(structureboundingbox, false, false);
|
|
+ Iterator<TickNextTickData<T>> iterator = list.iterator();
|
|
+
|
|
+ while (iterator.hasNext()) {
|
|
+ TickNextTickData<T> nextticklistentry = iterator.next();
|
|
+
|
|
+ if (structureboundingbox.hasPoint( nextticklistentry.getPosition())) {
|
|
+ BlockPos blockposition1 = nextticklistentry.getPosition().add(blockposition);
|
|
+ T t0 = nextticklistentry.getData();
|
|
+
|
|
+ this.schedule(new TickNextTickData<>(blockposition1, t0, nextticklistentry.getTargetTick(), nextticklistentry.getPriority()));
|
|
+ }
|
|
+ }
|
|
+ // end copy from TickListServer
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public List<TickNextTickData<T>> getEntriesInChunk(ChunkPos 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<TickNextTickData<T>> entries = this.entriesByChunk.get(MCUtil.getCoordinateKey(chunkPos));
|
|
+
|
|
+ if (entries == null) {
|
|
+ return Collections.emptyList();
|
|
+ }
|
|
+
|
|
+ final List<TickNextTickData<T>> ret = new ArrayList<>(entries.size());
|
|
+
|
|
+ for (TickNextTickData<T> entry : entries) {
|
|
+ if ((entry.tickState & matchOne) == 0) {
|
|
+ continue;
|
|
+ }
|
|
+ ret.add(entry);
|
|
+ }
|
|
+
|
|
+ if (removeReturned) {
|
|
+ for (TickNextTickData<T> entry : ret) {
|
|
+ this.removeEntry(entry);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ListTag serialize(ChunkPos chunkcoordintpair) {
|
|
+ // start copy from TickListServer // TODO check on update
|
|
+ List<TickNextTickData<T>> list = this.getEntriesInChunk(chunkcoordintpair, false, true);
|
|
+
|
|
+ return ServerTickList.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 (TickNextTickData<T> entry : this.longScheduled) {
|
|
+ if (entry.tickState == STATE_SCHEDULED) {
|
|
+ ++ret;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for (Iterator<Long2ObjectMap.Entry<ArrayList<TickNextTickData<T>>>> iterator = this.pendingChunkTickLoad.long2ObjectEntrySet().iterator(); iterator.hasNext();) {
|
|
+ ArrayList<TickNextTickData<T>> list = iterator.next().getValue();
|
|
+
|
|
+ for (TickNextTickData<T> entry : list) {
|
|
+ if (entry.tickState == STATE_SCHEDULED) {
|
|
+ ++ret;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for (TickListServerInterval<T> interval : this.shortScheduled) {
|
|
+ for (Iterable<TickNextTickData<T>> set : interval.byPriority) {
|
|
+ for (TickNextTickData<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..d40a7aff6c27883f3ae8ba878a94c97242619f2c
|
|
--- /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.TickNextTickData;
|
|
+import net.minecraft.world.level.TickPriority;
|
|
+
|
|
+// represents a set of entries to tick at a specified time
|
|
+public final class TickListServerInterval<T> {
|
|
+
|
|
+ public static final int TOTAL_PRIORITIES = TickPriority.values().length;
|
|
+ public static final Comparator<TickNextTickData<?>> ENTRY_COMPARATOR_BY_ID = (entry1, entry2) -> {
|
|
+ return Long.compare(entry1.getId(), entry2.getId());
|
|
+ };
|
|
+ public static final Comparator<TickNextTickData<?>> ENTRY_COMPARATOR = (Comparator)TickNextTickData.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<TickNextTickData<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 TickNextTickData<T> entry) {
|
|
+ this.byPriority[entry.getPriority().ordinal()].addLast(entry);
|
|
+ }
|
|
+
|
|
+ public void addEntryFirst(final TickNextTickData<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/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java
|
|
index 3db77d9eda98eacb099135643aff5e94751f4c7c..595abf528a7862478100770987906af1b13439fe 100644
|
|
--- a/src/main/java/net/minecraft/core/BlockPos.java
|
|
+++ b/src/main/java/net/minecraft/core/BlockPos.java
|
|
@@ -111,6 +111,7 @@ public class BlockPos extends Vec3i {
|
|
return x == 0 && y == 0 && z == 0 ? this : new BlockPos(this.getX() + x, this.getY() + y, this.getZ() + z);
|
|
}
|
|
|
|
+ public final BlockPos add(Vec3i baseblockposition) { return this.offset(baseblockposition); } // Paper - OBFHELPER
|
|
public BlockPos offset(Vec3i pos) {
|
|
return this.offset(pos.getX(), pos.getY(), pos.getZ());
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
index 77d3969200ac6f88f3af9add05def0b627ce6db3..d12d5459c847d3f0d655c85e31d81c27b7a2face 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
@@ -495,7 +495,9 @@ public class ChunkHolder {
|
|
ChunkHolder.this.isTickingReady = true;
|
|
|
|
|
|
-
|
|
+ // Paper start - rewrite ticklistserver
|
|
+ ChunkHolder.this.chunkMap.level.onChunkSetTicking(ChunkHolder.this.pos.x, ChunkHolder.this.pos.z);
|
|
+ // Paper end - rewrite ticklistserver
|
|
|
|
}
|
|
});
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
index 3aeb8426b0461ec572c1499116be80f968bb4104..e2b1541042bceac965411e3176d08c61f217c07f 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
@@ -21,6 +21,7 @@ import net.minecraft.Util;
|
|
import net.minecraft.core.BlockPos;
|
|
import net.minecraft.core.SectionPos;
|
|
import net.minecraft.network.protocol.Packet;
|
|
+import net.minecraft.server.MCUtil;
|
|
import net.minecraft.server.level.progress.ChunkProgressListener;
|
|
import net.minecraft.util.Mth;
|
|
import net.minecraft.util.profiling.ProfilerFiller;
|
|
@@ -217,6 +218,13 @@ public class ServerChunkCache extends ChunkSource {
|
|
}
|
|
// Paper end
|
|
|
|
+ // Paper start - rewrite ticklistserver
|
|
+ public final boolean isTickingReadyMainThread(BlockPos pos) {
|
|
+ ChunkHolder chunk = this.chunkMap.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(pos));
|
|
+ return chunk != null && chunk.isTickingReady();
|
|
+ }
|
|
+ // Paper end - rewrite ticklistserver
|
|
+
|
|
public ServerChunkCache(ServerLevel worldserver, LevelStorageSource.LevelStorageAccess convertable_conversionsession, DataFixer dataFixer, StructureManager structureManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, boolean flag, ChunkProgressListener worldloadlistener, Supplier<DimensionDataStorage> supplier) {
|
|
this.level = worldserver;
|
|
this.mainThreadProcessor = new ServerChunkCache.MainThreadExecutor(worldserver);
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
index 5d85895456b5d65954889cadf932027ea23b400b..9da0d98bc2ed7876a00a734690ed42f01b9a9a9b 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
@@ -292,6 +292,15 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
}
|
|
// 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.blockTicks).onChunkSetTicking(chunkX, chunkZ);
|
|
+ ((com.destroystokyo.paper.server.ticklist.PaperTickList) this.liquidTicks).onChunkSetTicking(chunkX, chunkZ);
|
|
+ }
|
|
+ }
|
|
+ // Paper end - rewrite ticklistserver
|
|
+
|
|
// Add env and gen to constructor, WorldData -> WorldDataServer
|
|
public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, ServerLevelData iworlddataserver, ResourceKey<net.minecraft.world.level.Level> resourcekey, DimensionType dimensionmanager, ChunkProgressListener worldloadlistener, ChunkGenerator chunkgenerator, boolean flag, long i, List<CustomSpawner> list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen) {
|
|
super(iworlddataserver, resourcekey, dimensionmanager, minecraftserver::getProfiler, false, flag, i, gen, env, executor); // Paper pass executor
|
|
@@ -299,12 +308,21 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
convertable = convertable_conversionsession;
|
|
uuid = WorldUUID.getUUID(convertable_conversionsession.levelPath.toFile());
|
|
// CraftBukkit end
|
|
- this.blockTicks = new ServerTickList<>(this, (block) -> {
|
|
- return block == null || block.defaultBlockState().isAir();
|
|
- }, Registry.BLOCK::getKey, this::tickBlock, "Blocks"); // Paper - Timings
|
|
- this.liquidTicks = new ServerTickList<>(this, (fluidtype) -> {
|
|
- return fluidtype == null || fluidtype == Fluids.EMPTY;
|
|
- }, Registry.FLUID::getKey, this::tickLiquid, "Fluids"); // Paper - Timings
|
|
+ if (com.destroystokyo.paper.PaperConfig.useOptimizedTickList) {
|
|
+ this.blockTicks = new com.destroystokyo.paper.server.ticklist.PaperTickList<>(this, (block) -> {
|
|
+ return block == null || block.defaultBlockState().isAir();
|
|
+ }, Registry.BLOCK::getKey, this::tickBlock, "Blocks"); // Paper - Timings
|
|
+ this.liquidTicks = new com.destroystokyo.paper.server.ticklist.PaperTickList<>(this, (fluidtype) -> {
|
|
+ return fluidtype == null || fluidtype == Fluids.EMPTY;
|
|
+ }, Registry.FLUID::getKey, this::tickLiquid, "Fluids"); // Paper - Timings
|
|
+ } else {
|
|
+ this.blockTicks = new ServerTickList<>(this, (block) -> {
|
|
+ return block == null || block.defaultBlockState().isAir();
|
|
+ }, Registry.BLOCK::getKey, this::tickBlock, "Blocks"); // Paper - Timings
|
|
+ this.liquidTicks = new ServerTickList<>(this, (fluidtype) -> {
|
|
+ return fluidtype == null || fluidtype == Fluids.EMPTY;
|
|
+ }, Registry.FLUID::getKey, this::tickLiquid, "Fluids"); // Paper - Timings
|
|
+ }
|
|
this.navigations = Sets.newHashSet();
|
|
this.blockEvents = new ObjectLinkedOpenHashSet();
|
|
this.tickTime = flag1;
|
|
@@ -639,7 +657,9 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
|
|
if (this.tickTime) {
|
|
long i = this.levelData.getGameTime() + 1L;
|
|
|
|
- this.worldDataServer.setGameTime(i);
|
|
+ this.worldDataServer.setGameTime(i); // Paper - diff on change, we want the below to be ran right after this
|
|
+ this.blockTicks.nextTick(); // Paper
|
|
+ this.liquidTicks.nextTick(); // Paper
|
|
this.worldDataServer.getScheduledEvents().tick(this.server, i);
|
|
if (this.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
|
|
this.setDayTime(this.levelData.getDayTime() + 1L);
|
|
diff --git a/src/main/java/net/minecraft/world/level/ChunkTickList.java b/src/main/java/net/minecraft/world/level/ChunkTickList.java
|
|
index 16757eb9c03c0dab51a7a1b569daff81cf9654f3..3008e0c42efe908e45dba1a1437928d4d4378f24 100644
|
|
--- a/src/main/java/net/minecraft/world/level/ChunkTickList.java
|
|
+++ b/src/main/java/net/minecraft/world/level/ChunkTickList.java
|
|
@@ -9,6 +9,7 @@ import net.minecraft.core.BlockPos;
|
|
import net.minecraft.nbt.CompoundTag;
|
|
import net.minecraft.nbt.ListTag;
|
|
import net.minecraft.resources.ResourceLocation;
|
|
+import net.minecraft.server.MinecraftServer;
|
|
|
|
public class ChunkTickList<T> implements TickList<T> {
|
|
|
|
@@ -61,6 +62,8 @@ public class ChunkTickList<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> ChunkTickList<T> create(ListTag ticks, Function<T, ResourceLocation> function, Function<ResourceLocation, T> function1) {
|
|
List<ChunkTickList.ScheduledTick<T>> list = Lists.newArrayList();
|
|
|
|
@@ -71,7 +74,14 @@ public class ChunkTickList<T> implements TickList<T> {
|
|
if (t0 != null) {
|
|
BlockPos blockposition = new BlockPos(nbttagcompound.getInt("x"), nbttagcompound.getInt("y"), nbttagcompound.getInt("z"));
|
|
|
|
- list.add(new ChunkTickList.ScheduledTick<>(t0, blockposition, nbttagcompound.getInt("t"), TickPriority.byValue(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 ChunkTickList.ScheduledTick<>(t0, blockposition, delay, TickPriority.byValue(nbttagcompound.getInt("p"))));
|
|
+ // Paper end - clean up broken entries
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/ServerTickList.java b/src/main/java/net/minecraft/world/level/ServerTickList.java
|
|
index 10ac1ba0a3d192486f22c2127d5bc30353f0edb6..65b3a16f40a295c2916be2a9fd019b452fb65e4f 100644
|
|
--- a/src/main/java/net/minecraft/world/level/ServerTickList.java
|
|
+++ b/src/main/java/net/minecraft/world/level/ServerTickList.java
|
|
@@ -50,7 +50,16 @@ public class ServerTickList<T> implements TickList<T> {
|
|
private final co.aikar.timings.Timing timingTicking; // Paper
|
|
// Paper end
|
|
|
|
+ // Paper start
|
|
+ public void nextTick() {}
|
|
+ // Paper end
|
|
+
|
|
+ public void tick() {
|
|
+ // Paper start - allow overriding
|
|
+ this.tick();
|
|
+ }
|
|
public void tick() {
|
|
+ // Paper end
|
|
int i = this.tickNextTickList.size();
|
|
|
|
if (false) { // CraftBukkit
|
|
@@ -118,28 +127,43 @@ public class ServerTickList<T> implements TickList<T> {
|
|
|
|
@Override
|
|
public boolean willTickThisTick(BlockPos pos, T object) {
|
|
- return this.currentlyTicking.contains(new TickNextTickData<>(pos, object));
|
|
+ // Paper start - allow overriding
|
|
+ return this.isPendingTickThisTick(pos, object);
|
|
+ }
|
|
+ public boolean isPendingTickThisTick(BlockPos blockposition, T t0) {
|
|
+ // Paper end
|
|
+ return this.currentlyTicking.contains(new TickNextTickData<>(blockposition, t0));
|
|
}
|
|
|
|
public List<TickNextTickData<T>> fetchTicksInChunk(ChunkPos chunkcoordintpair, boolean updateState, boolean getStaleTicks) {
|
|
+ // Paper start - allow overriding
|
|
+ return this.getEntriesInChunk(chunkcoordintpair, updateState, getStaleTicks);
|
|
+ }
|
|
+ public List<TickNextTickData<T>> getEntriesInChunk(ChunkPos chunkcoordintpair, boolean flag, boolean flag1) {
|
|
+ // Paper end
|
|
int i = (chunkcoordintpair.x << 4) - 2;
|
|
int j = i + 16 + 2;
|
|
int k = (chunkcoordintpair.z << 4) - 2;
|
|
int l = k + 16 + 2;
|
|
|
|
- return this.fetchTicksInArea(new BoundingBox(i, 0, k, j, 256, l), updateState, getStaleTicks);
|
|
+ return this.fetchTicksInArea(new BoundingBox(i, 0, k, j, 256, l), flag, flag1);
|
|
}
|
|
|
|
public List<TickNextTickData<T>> fetchTicksInArea(BoundingBox bounds, boolean updateState, boolean getStaleTicks) {
|
|
- List<TickNextTickData<T>> list = this.fetchTicksInArea((List) null, this.tickNextTickList, bounds, updateState);
|
|
+ // Paper start - allow overriding
|
|
+ return this.getEntriesInBoundingBox(bounds, updateState, getStaleTicks);
|
|
+ }
|
|
+ public List<TickNextTickData<T>> getEntriesInBoundingBox(BoundingBox structureboundingbox, boolean flag, boolean flag1) {
|
|
+ // Paper end
|
|
+ List<TickNextTickData<T>> list = this.fetchTicksInArea((List) null, this.tickNextTickList, structureboundingbox, flag);
|
|
|
|
- if (updateState && list != null) {
|
|
+ if (flag && list != null) {
|
|
this.tickNextTickSet.removeAll(list);
|
|
}
|
|
|
|
- list = this.fetchTicksInArea(list, this.currentlyTicking, bounds, updateState);
|
|
- if (!getStaleTicks) {
|
|
- list = this.fetchTicksInArea(list, this.alreadyTicked, bounds, updateState);
|
|
+ list = this.fetchTicksInArea(list, this.currentlyTicking, structureboundingbox, flag);
|
|
+ if (!flag1) {
|
|
+ list = this.fetchTicksInArea(list, this.alreadyTicked, structureboundingbox, flag);
|
|
}
|
|
|
|
return list == null ? Collections.emptyList() : list;
|
|
@@ -170,14 +194,19 @@ public class ServerTickList<T> implements TickList<T> {
|
|
}
|
|
|
|
public void copy(BoundingBox box, BlockPos offset) {
|
|
- List<TickNextTickData<T>> list = this.fetchTicksInArea(box, false, false);
|
|
+ // Paper start - allow overriding
|
|
+ this.copy(box, offset);
|
|
+ }
|
|
+ public void copy(BoundingBox structureboundingbox, BlockPos blockposition) {
|
|
+ // Paper end
|
|
+ List<TickNextTickData<T>> list = this.fetchTicksInArea(structureboundingbox, false, false);
|
|
Iterator iterator = list.iterator();
|
|
|
|
while (iterator.hasNext()) {
|
|
TickNextTickData<T> nextticklistentry = (TickNextTickData) iterator.next();
|
|
|
|
- if (box.isInside((Vec3i) nextticklistentry.pos)) {
|
|
- BlockPos blockposition1 = nextticklistentry.pos.offset((Vec3i) offset);
|
|
+ if (structureboundingbox.isInside((Vec3i) nextticklistentry.pos)) {
|
|
+ BlockPos blockposition1 = nextticklistentry.pos.offset((Vec3i) blockposition);
|
|
T t0 = nextticklistentry.getType();
|
|
|
|
this.addTickData(new TickNextTickData<>(blockposition1, t0, nextticklistentry.triggerTick, nextticklistentry.priority));
|
|
@@ -187,11 +216,17 @@ public class ServerTickList<T> implements TickList<T> {
|
|
}
|
|
|
|
public ListTag save(ChunkPos chunkcoordintpair) {
|
|
+ // Paper start - allow overriding
|
|
+ return this.serialize(chunkcoordintpair);
|
|
+ }
|
|
+ public ListTag serialize(ChunkPos chunkcoordintpair) {
|
|
+ // Paper end
|
|
List<TickNextTickData<T>> list = this.fetchTicksInChunk(chunkcoordintpair, false, true);
|
|
|
|
return saveTickList(this.toId, list, this.level.getGameTime());
|
|
}
|
|
|
|
+ public static <T> ListTag serialize(Function<T, ResourceLocation> function, Iterable<TickNextTickData<T>> iterable, long i) { return ServerTickList.saveTickList(function, iterable, i); } // Paper - OBFHELPER
|
|
private static <T> ListTag saveTickList(Function<T, ResourceLocation> identifierProvider, Iterable<TickNextTickData<T>> scheduledTicks, long time) {
|
|
ListTag nbttaglist = new ListTag();
|
|
Iterator iterator = scheduledTicks.iterator();
|
|
@@ -214,13 +249,23 @@ public class ServerTickList<T> implements TickList<T> {
|
|
|
|
@Override
|
|
public boolean hasScheduledTick(BlockPos pos, T object) {
|
|
- return this.tickNextTickSet.contains(new TickNextTickData<>(pos, object));
|
|
+ // Paper start - allow overriding
|
|
+ return this.isScheduledForTick(pos, object);
|
|
+ }
|
|
+ public boolean isScheduledForTick(BlockPos blockposition, T t0) {
|
|
+ // Paper end
|
|
+ return this.tickNextTickSet.contains(new TickNextTickData<>(blockposition, t0));
|
|
}
|
|
|
|
@Override
|
|
public void scheduleTick(BlockPos pos, T object, int delay, TickPriority priority) {
|
|
- if (!this.ignore.test(object)) {
|
|
- this.addTickData(new TickNextTickData<>(pos, object, (long) delay + this.level.getGameTime(), priority));
|
|
+ // Paper start - allow overriding
|
|
+ this.schedule(pos, object, delay, priority);
|
|
+ }
|
|
+ public void schedule(BlockPos blockposition, T t0, int i, TickPriority ticklistpriority) {
|
|
+ // Paper end
|
|
+ if (!this.ignore.test(t0)) {
|
|
+ this.addTickData(new TickNextTickData<>(blockposition, t0, (long) i + this.level.getGameTime(), ticklistpriority));
|
|
}
|
|
|
|
}
|
|
@@ -234,6 +279,11 @@ public class ServerTickList<T> implements TickList<T> {
|
|
}
|
|
|
|
public int size() {
|
|
+ // Paper start - allow overriding
|
|
+ return this.getTotalScheduledEntries();
|
|
+ }
|
|
+ public int getTotalScheduledEntries() {
|
|
+ // Paper end
|
|
return this.tickNextTickSet.size();
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/TickNextTickData.java b/src/main/java/net/minecraft/world/level/TickNextTickData.java
|
|
index 90833389022d7412bdda8868a356b84f62a00e03..61cdaf45368dcc40f3311e8b7f8637a6c93a2d76 100644
|
|
--- a/src/main/java/net/minecraft/world/level/TickNextTickData.java
|
|
+++ b/src/main/java/net/minecraft/world/level/TickNextTickData.java
|
|
@@ -6,11 +6,13 @@ import net.minecraft.core.BlockPos;
|
|
public class TickNextTickData<T> {
|
|
|
|
private static final java.util.concurrent.atomic.AtomicLong COUNTER = new java.util.concurrent.atomic.AtomicLong(); // Paper - async chunk loading
|
|
- private final T type;
|
|
- public final BlockPos pos;
|
|
- public final long triggerTick;
|
|
- public final TickPriority priority;
|
|
- private final long c;
|
|
+ private final T type; public final T getData() { return this.type; } // Paper - OBFHELPER
|
|
+ public final BlockPos pos; public final BlockPos getPosition() { return this.pos; } // Paper - OBFHELPER
|
|
+ public final long triggerTick; public final long getTargetTick() { return this.triggerTick; } // Paper - OBFHELPER
|
|
+ public final TickPriority priority; public final TickPriority getPriority() { return this.priority; } // Paper - OBFHELPER
|
|
+ private final long c; public final long getId() { return this.c; } // Paper - OBFHELPER
|
|
+ private final int hash; // Paper
|
|
+ public int tickState; // Paper
|
|
|
|
public TickNextTickData(BlockPos pos, T t) {
|
|
this(pos, t, 0L, TickPriority.NORMAL);
|
|
@@ -22,6 +24,7 @@ public class TickNextTickData<T> {
|
|
this.type = t;
|
|
this.triggerTick = time;
|
|
this.priority = priority;
|
|
+ this.hash = this.computeHash(); // Paper
|
|
}
|
|
|
|
public boolean equals(Object object) {
|
|
@@ -34,19 +37,31 @@ public class TickNextTickData<T> {
|
|
}
|
|
}
|
|
|
|
+ // Paper start - optimize hashcode
|
|
+ @Override
|
|
public int hashCode() {
|
|
+ return this.hash;
|
|
+ }
|
|
+ public final int computeHash() {
|
|
+ // Paper end - optimize hashcode
|
|
return this.pos.hashCode();
|
|
}
|
|
|
|
- public static <T> Comparator<Object> createTimeComparator() { // Paper - decompile fix
|
|
- return Comparator.comparingLong((nextticklistentry) -> {
|
|
- return ((TickNextTickData<T>) nextticklistentry).triggerTick; // Paper - decompile fix
|
|
- }).thenComparing((nextticklistentry) -> {
|
|
- return ((TickNextTickData<T>) nextticklistentry).priority; // Paper - decompile fix
|
|
- }).thenComparingLong((nextticklistentry) -> {
|
|
- return ((TickNextTickData<T>) nextticklistentry).c; // Paper - decompile fix
|
|
- });
|
|
+ // Paper start - let's not use more functional code for no reason.
|
|
+ public static <T> Comparator<Object> comparator() { return TickNextTickData.createTimeComparator(); } // Paper - OBFHELPER
|
|
+ public static <T> Comparator<Object> createTimeComparator() {
|
|
+ return (Comparator)(Comparator<TickNextTickData>)(TickNextTickData nextticklistentry, TickNextTickData 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.type + ": " + this.pos + ", " + this.triggerTick + ", " + this.priority + ", " + this.c;
|
|
diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/BoundingBox.java b/src/main/java/net/minecraft/world/level/levelgen/structure/BoundingBox.java
|
|
index 76bea58d35d352ee6f3d4bd0d10af3b6d615ae2c..4288fa253668196e6c32a876f18ec496fb3abad6 100644
|
|
--- a/src/main/java/net/minecraft/world/level/levelgen/structure/BoundingBox.java
|
|
+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/BoundingBox.java
|
|
@@ -8,12 +8,12 @@ import net.minecraft.nbt.IntArrayTag;
|
|
|
|
public class BoundingBox {
|
|
|
|
- public int x0;
|
|
- public int y0;
|
|
- public int z0;
|
|
- public int x1;
|
|
- public int y1;
|
|
- public int z1;
|
|
+ public int x0; public final int getMinX() { return this.x0; } // Paper - OBFHELPER
|
|
+ public int y0; public final int getMinY() { return this.y0; } // Paper - OBFHELPER
|
|
+ public int z0; public final int getMinZ() { return this.z0; } // Paper - OBFHELPER
|
|
+ public int x1; public final int getMaxX() { return this.x1; } // Paper - OBFHELPER
|
|
+ public int y1; public final int getMaxY() { return this.y1; } // Paper - OBFHELPER
|
|
+ public int z1; public final int getMaxZ() { return this.z1; } // Paper - OBFHELPER
|
|
|
|
public BoundingBox() {}
|
|
|
|
@@ -92,6 +92,7 @@ public class BoundingBox {
|
|
this.y1 = 512;
|
|
}
|
|
|
|
+ public final boolean intersects(BoundingBox boundingBox) { return this.intersects(boundingBox); } // Paper - OBFHELPER
|
|
public boolean intersects(BoundingBox other) {
|
|
return this.x1 >= other.x0 && this.x0 <= other.x1 && this.z1 >= other.z0 && this.z0 <= other.z1 && this.y1 >= other.y0 && this.y0 <= other.y1;
|
|
}
|
|
@@ -126,6 +127,7 @@ public class BoundingBox {
|
|
this.move(baseblockposition.getX(), baseblockposition.getY(), baseblockposition.getZ());
|
|
}
|
|
|
|
+ public final boolean hasPoint(Vec3i baseblockposition) { return this.isInside(baseblockposition); } // Paper - OBFHELPER
|
|
public boolean isInside(Vec3i vec) {
|
|
return vec.getX() >= this.x0 && vec.getX() <= this.x1 && vec.getZ() >= this.z0 && vec.getZ() <= this.z1 && vec.getY() >= this.y0 && vec.getY() <= this.y1;
|
|
}
|