mirror of
https://github.com/PaperMC/Paper.git
synced 2024-12-22 08:57:35 +01:00
1128 lines
50 KiB
Diff
1128 lines
50 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 26c5ae72f63d930bf6de2ec18a964ddfeca16379..e9954fa75412a7077950e3813af4b201c084f68f 100644
|
|
--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
|
|
+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
|
|
@@ -375,6 +375,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..5fdaefc128956581be4bb9b34199fd6410563991
|
|
--- /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.isPositionTickingWithEntitiesLoaded(entry.pos)) {
|
|
+ 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.pos), (long keyInMap) -> {
|
|
+ return new ArrayList<>();
|
|
+ }).add(entry);
|
|
+ }
|
|
+
|
|
+ private void addToSchedule(final TickNextTickData<T> entry) {
|
|
+ long delay = entry.triggerTick - (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.pos;
|
|
+ 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.pos);
|
|
+
|
|
+ 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.triggerTick - (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().triggerTick > 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.triggerTick > currentTick) {
|
|
+ longCurrent = null;
|
|
+ break;
|
|
+ }
|
|
+ } else {
|
|
+ longCurrent = null;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ this.queueEntryForTick(shortCurrent, chunkProvider);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // add remaining from long scheduled
|
|
+ for (;;) {
|
|
+ if (longCurrent == null || longCurrent.triggerTick > 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.isPositionTickingWithEntitiesLoaded(toTick.pos)) {
|
|
+ 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, this.world, toTick.pos, (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.pos;
|
|
+ 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.pos);
|
|
+
|
|
+ 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 willTickThisTick(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.getType() == data && entry.tickState == STATE_PENDING_TICK) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean hasScheduledTick(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.getType() == data && entry.tickState == STATE_SCHEDULED) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void scheduleTick(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.pos, entry.getType(), entry.triggerTick, entry.priority);
|
|
+ }
|
|
+
|
|
+ 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.getType())) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (WARN_ON_EXCESSIVE_DELAY) {
|
|
+ final long delay = entry.triggerTick - 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.getType() == entry.getType() && currentEntry.tickState == STATE_SCHEDULED) {
|
|
+ // can't add
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ currentEntries.add(entry);
|
|
+ }
|
|
+
|
|
+ entry.tickState = STATE_SCHEDULED;
|
|
+
|
|
+ this.entriesByChunk.computeIfAbsent(MCUtil.getCoordinateKey(entry.pos), (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.minX() && pos.getX() < boundingBox.maxX() && pos.getZ() >= boundingBox.minZ() && pos.getZ() < boundingBox.maxZ();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public List<TickNextTickData<T>> fetchTicksInArea(final BoundingBox structureboundingbox, final boolean removeReturned, final boolean excludeTicked) {
|
|
+ if (structureboundingbox.minX() == structureboundingbox.maxX() || structureboundingbox.minZ() == structureboundingbox.maxZ()) {
|
|
+ return Collections.emptyList(); // vanilla behaviour, check isBlockInSortof above
|
|
+ }
|
|
+
|
|
+ final int lowerChunkX = structureboundingbox.minX() >> 4;
|
|
+ final int upperChunkX = (structureboundingbox.maxX() - 1) >> 4; // subtract 1 since maxX is exclusive
|
|
+ final int lowerChunkZ = structureboundingbox.minZ() >> 4;
|
|
+ final int upperChunkZ = (structureboundingbox.maxZ() - 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.pos)) {
|
|
+ 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.fetchTicksInArea(structureboundingbox, false, false);
|
|
+ Iterator<TickNextTickData<T>> iterator = list.iterator();
|
|
+
|
|
+ while (iterator.hasNext()) {
|
|
+ TickNextTickData<T> nextticklistentry = iterator.next();
|
|
+
|
|
+ if (structureboundingbox.isInside( nextticklistentry.pos)) {
|
|
+ BlockPos blockposition1 = nextticklistentry.pos.offset(blockposition);
|
|
+ T t0 = nextticklistentry.getType();
|
|
+
|
|
+ this.schedule(new TickNextTickData<>(blockposition1, t0, nextticklistentry.triggerTick, nextticklistentry.priority));
|
|
+ }
|
|
+ }
|
|
+ // end copy from TickListServer
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public List<TickNextTickData<T>> fetchTicksInChunk(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 save(ChunkPos chunkcoordintpair) {
|
|
+ // start copy from TickListServer // TODO check on update
|
|
+ List<TickNextTickData<T>> list = this.fetchTicksInChunk(chunkcoordintpair, false, true);
|
|
+
|
|
+ return ServerTickList.saveTickList(this.getMinecraftKeyFrom, list, this.currentTick);
|
|
+ // end copy from TickListServer
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public int size() {
|
|
+ // 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..a1e6f49274a7ae8057a9112e0dd6597a8e58e6da
|
|
--- /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.createTimeComparator();
|
|
+
|
|
+ // 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.priority.ordinal()].addLast(entry);
|
|
+ }
|
|
+
|
|
+ public void addEntryFirst(final TickNextTickData<T> entry) {
|
|
+ this.byPriority[entry.priority.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/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
index 84f370e887a3e7ff49296bdf8d6d8de9cc194cfb..daeb5b175d17492f382d23af58a9cc46fbb49e60 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
@@ -86,6 +86,19 @@ public class ChunkHolder {
|
|
return null;
|
|
}
|
|
// Paper end - no-tick view distance
|
|
+ // Paper start
|
|
+ public final boolean isEntityTickingReady() {
|
|
+ return this.isEntityTickingReady;
|
|
+ }
|
|
+
|
|
+ public final boolean isTickingReady() {
|
|
+ return this.isTickingReady;
|
|
+ }
|
|
+
|
|
+ public final boolean isFullChunkReady() {
|
|
+ return this.isFullChunkReady;
|
|
+ }
|
|
+ // Paper end
|
|
|
|
public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) {
|
|
this.futures = new AtomicReferenceArray(ChunkHolder.CHUNK_STATUSES.size());
|
|
@@ -541,6 +554,11 @@ public class ChunkHolder {
|
|
// Paper start - ticking chunk set
|
|
ChunkHolder.this.chunkMap.level.getChunkSource().tickingChunks.add(chunk);
|
|
// Paper end - ticking chunk set
|
|
+ // Paper start - rewrite ticklistserver
|
|
+ if (ChunkHolder.this.chunkMap.level.entityManager.areEntitiesLoaded(this.pos.longKey)) {
|
|
+ ChunkHolder.this.chunkMap.level.onChunkSetTicking(ChunkHolder.this.pos.x, ChunkHolder.this.pos.z);
|
|
+ }
|
|
+ // Paper end - rewrite ticklistserver
|
|
});
|
|
});
|
|
// Paper end
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
index c6213a7dfcf9aeccdb546eaf74fa8eb119a6a32c..ec0d8e58a518a20634b902769251d6d04750433e 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
|
|
@@ -218,6 +218,18 @@ public class ServerChunkCache extends ChunkSource {
|
|
}, this.mainThreadProcessor);
|
|
}
|
|
// Paper end
|
|
+ // Paper start - rewrite ticklistserver
|
|
+ public final boolean isPositionTickingReady(long pos) {
|
|
+ final ChunkHolder chunkHolder = this.chunkMap.getUpdatingChunkIfPresent(pos);
|
|
+ return chunkHolder != null && chunkHolder.isTickingReady();
|
|
+ }
|
|
+
|
|
+ public final boolean isPositionTickingWithEntitiesLoaded(BlockPos pos) {
|
|
+ final long position = net.minecraft.server.MCUtil.getCoordinateKey(pos);
|
|
+ final ChunkHolder chunkHolder = this.chunkMap.getUpdatingChunkIfPresent(position);
|
|
+ return chunkHolder != null && chunkHolder.isTickingReady() && this.level.entityManager.areEntitiesLoaded(position);
|
|
+ }
|
|
+ // Paper end - rewrite ticklistserver
|
|
|
|
// Paper start
|
|
// this will try to avoid chunk neighbours for lighting
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
index 527ae7af221c031b4cdf481f097e9062c41af5ac..a19b3c039751da14f044f05fb5ebfa08c051abe4 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
@@ -381,6 +381,15 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
}
|
|
// Paper end
|
|
|
|
+ // Paper start - rewrite ticklistserver
|
|
+ public 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<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, org.bukkit.generator.BiomeProvider biomeProvider) {
|
|
// Objects.requireNonNull(minecraftserver); // CraftBukkit - decompile error
|
|
@@ -397,13 +406,19 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
DefaultedRegistry registryblocks = Registry.BLOCK;
|
|
|
|
Objects.requireNonNull(registryblocks);
|
|
- this.blockTicks = new ServerTickList<>(this, predicate, Registry.BLOCK::getKey, this::tickBlock, "Blocks"); // CraftBukkit - decompile error // Paper - Timings
|
|
+ // this.blockTicks = new ServerTickList<>(this, predicate, Registry.BLOCK::getKey, this::tickBlock, "Blocks"); // CraftBukkit - decompile error // Paper - Timings // Paper - copied down
|
|
Predicate<Fluid> predicate2 = (fluidtype) -> { // CraftBukkit - decompile error
|
|
return fluidtype == null || fluidtype == Fluids.EMPTY;
|
|
};
|
|
registryblocks = Registry.FLUID;
|
|
Objects.requireNonNull(registryblocks);
|
|
+ if (com.destroystokyo.paper.PaperConfig.useOptimizedTickList) {
|
|
+ this.blockTicks = new com.destroystokyo.paper.server.ticklist.PaperTickList<>(this, predicate, Registry.BLOCK::getKey, this::tickBlock, "Blocks"); // Paper - Timings
|
|
+ this.liquidTicks = new com.destroystokyo.paper.server.ticklist.PaperTickList<>(this, predicate2, Registry.FLUID::getKey, this::tickLiquid, "Fluids"); // Paper timings
|
|
+ } else {
|
|
+ this.blockTicks = new ServerTickList<>(this, predicate, Registry.BLOCK::getKey, this::tickBlock, "Blocks"); // CraftBukkit - decompile error // Paper - Timings & copied from above
|
|
this.liquidTicks = new ServerTickList<>(this, predicate2, Registry.FLUID::getKey, this::tickLiquid, "Fluids"); // CraftBukkit - decompile error // Paper - Timings
|
|
+ }
|
|
this.navigatingMobs = new ObjectOpenHashSet();
|
|
this.blockEvents = new ObjectLinkedOpenHashSet();
|
|
this.dragonParts = new Int2ObjectOpenHashMap();
|
|
@@ -689,7 +704,9 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
if (this.tickTime) {
|
|
long i = this.levelData.getGameTime() + 1L;
|
|
|
|
- this.serverLevelData.setGameTime(i);
|
|
+ this.serverLevelData.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.serverLevelData.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 1a05edf041cf4aeee7c165fec564ce45adbdd5c7..febc837d324cbe2cd83aea6c1e0d298c70f45f78 100644
|
|
--- a/src/main/java/net/minecraft/world/level/ChunkTickList.java
|
|
+++ b/src/main/java/net/minecraft/world/level/ChunkTickList.java
|
|
@@ -15,7 +15,7 @@ public class ChunkTickList<T> implements TickList<T> {
|
|
|
|
public ChunkTickList(Function<T, ResourceLocation> identifierProvider, List<TickNextTickData<T>> scheduledTicks, long startTime) {
|
|
this(identifierProvider, scheduledTicks.stream().map((tickNextTickData) -> {
|
|
- return new ChunkTickList.ScheduledTick(tickNextTickData.getType(), tickNextTickData.pos, (int)(tickNextTickData.triggerTick - startTime), tickNextTickData.priority);
|
|
+ return new ChunkTickList.ScheduledTick<>(tickNextTickData.getType(), tickNextTickData.pos, (int)(tickNextTickData.triggerTick - startTime), tickNextTickData.priority); // Paper - decompile error
|
|
}).collect(Collectors.toList()));
|
|
}
|
|
|
|
@@ -56,6 +56,7 @@ public class ChunkTickList<T> implements TickList<T> {
|
|
return listTag;
|
|
}
|
|
|
|
+ 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> function2) {
|
|
List<ChunkTickList.ScheduledTick<T>> list = Lists.newArrayList();
|
|
|
|
@@ -64,7 +65,14 @@ public class ChunkTickList<T> implements TickList<T> {
|
|
T object = function2.apply(new ResourceLocation(compoundTag.getString("i")));
|
|
if (object != null) {
|
|
BlockPos blockPos = new BlockPos(compoundTag.getInt("x"), compoundTag.getInt("y"), compoundTag.getInt("z"));
|
|
- list.add(new ChunkTickList.ScheduledTick<>(object, blockPos, compoundTag.getInt("t"), TickPriority.byValue(compoundTag.getInt("p"))));
|
|
+ // Paper start - clean up broken entries
|
|
+ int delay = compoundTag.getInt("t");
|
|
+ if (MAX_TICK_DELAY > 0 && delay > MAX_TICK_DELAY) {
|
|
+ net.minecraft.server.MinecraftServer.LOGGER.warn("Dropping tick for pos " + blockPos + ", tick delay " + delay);
|
|
+ continue;
|
|
+ }
|
|
+ // Paper end - clean up broken entries
|
|
+ list.add(new ChunkTickList.ScheduledTick<>(object, blockPos, delay, TickPriority.byValue(compoundTag.getInt("p"))));
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/ServerTickList.java b/src/main/java/net/minecraft/world/level/ServerTickList.java
|
|
index dfd41db804acde50339de9b18566b845401d7cbe..0f61dd3a3115f68ccd2c7cb4c9309f5ace96a406 100644
|
|
--- a/src/main/java/net/minecraft/world/level/ServerTickList.java
|
|
+++ b/src/main/java/net/minecraft/world/level/ServerTickList.java
|
|
@@ -51,6 +51,9 @@ public class ServerTickList<T> implements TickList<T> {
|
|
this.ticker = tickConsumer;
|
|
}
|
|
|
|
+ // Paper start
|
|
+ public void nextTick() {}
|
|
+ // Paper end
|
|
public void tick() {
|
|
int i = this.tickNextTickList.size();
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/TickNextTickData.java b/src/main/java/net/minecraft/world/level/TickNextTickData.java
|
|
index 3b8c04f6ffd7e6c197465aa1caf633ba92529472..1007bfc9c19641f42afd5526cfe7bdb61906d1a0 100644
|
|
--- a/src/main/java/net/minecraft/world/level/TickNextTickData.java
|
|
+++ b/src/main/java/net/minecraft/world/level/TickNextTickData.java
|
|
@@ -9,7 +9,9 @@ public class TickNextTickData<T> {
|
|
public final BlockPos pos;
|
|
public final long triggerTick;
|
|
public final TickPriority priority;
|
|
- private final long c;
|
|
+ private final long c; @Deprecated 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);
|
|
@@ -21,6 +23,7 @@ public class TickNextTickData<T> {
|
|
this.type = t;
|
|
this.triggerTick = time;
|
|
this.priority = priority;
|
|
+ this.hash = this.computeHash(); // Paper
|
|
}
|
|
|
|
@Override
|
|
@@ -35,17 +38,27 @@ public class TickNextTickData<T> {
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
+ // Paper start - optimize hashcode
|
|
+ return this.hash;
|
|
+ }
|
|
+ public final int computeHash() {
|
|
+ // Paper end - optimize hashcode
|
|
return this.pos.hashCode();
|
|
}
|
|
|
|
public static <T> Comparator<TickNextTickData<T>> createTimeComparator() {
|
|
- return Comparator.<TickNextTickData<T>>comparingLong((tickNextTickData) -> { // Paper - decompile fix
|
|
- return tickNextTickData.triggerTick;
|
|
- }).thenComparing((tickNextTickData) -> {
|
|
- return tickNextTickData.priority;
|
|
- }).thenComparingLong((tickNextTickData) -> {
|
|
- return tickNextTickData.c;
|
|
- });
|
|
+ // Paper start - let's not use more functional code for no reason.
|
|
+ return (Comparator) (Comparator<TickNextTickData>) (TickNextTickData nextticklistentry, TickNextTickData nextticklistentry1) -> {
|
|
+ int i = Long.compare(nextticklistentry.triggerTick, nextticklistentry1.triggerTick);
|
|
+
|
|
+ if (i != 0) {
|
|
+ return i;
|
|
+ } else {
|
|
+ i = nextticklistentry.priority.compareTo(nextticklistentry1.priority);
|
|
+ return i != 0 ? i : Long.compare(nextticklistentry.getId(), nextticklistentry1.getId());
|
|
+ }
|
|
+ };
|
|
+ // Paper end - let's not use more functional code for no reason.
|
|
}
|
|
|
|
@Override
|
|
diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
|
|
index f698723f8f7feecc749df10a316118184391f31a..be65a8a5a853d4e014d44730a48ccf247acf08d2 100644
|
|
--- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
|
|
+++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
|
|
@@ -308,6 +308,12 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A
|
|
List<Entity> entities = this.getEntities(chunkentities.getPos()); // PAIL rename getChunkPos
|
|
CraftEventFactory.callEntitiesLoadEvent(((EntityStorage) this.permanentStorage).level, chunkentities.getPos(), entities);
|
|
// CraftBukkit end
|
|
+ // Paper start - rewrite ServerTickList
|
|
+ final net.minecraft.server.level.ServerLevel level = ((net.minecraft.world.level.chunk.storage.EntityStorage) this.permanentStorage).level;
|
|
+ if (level.chunkSource.isPositionTickingReady(chunkentities.getPos().longKey)) {
|
|
+ level.onChunkSetTicking(chunkentities.getPos().x, chunkentities.getPos().z);
|
|
+ }
|
|
+ // Paper end
|
|
}
|
|
|
|
}
|