From 44ddc5267e4bb471da0c37d6e633a786954453f6 Mon Sep 17 00:00:00 2001 From: Newwind Date: Mon, 16 Feb 2026 23:41:20 +0000 Subject: [PATCH 1/6] Create 0033-Speculative-Portal-Loading.patch --- .../0033-Speculative-Portal-Loading.patch | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 paper-server/patches/features/0033-Speculative-Portal-Loading.patch diff --git a/paper-server/patches/features/0033-Speculative-Portal-Loading.patch b/paper-server/patches/features/0033-Speculative-Portal-Loading.patch new file mode 100644 index 000000000000..c4ae1f744573 --- /dev/null +++ b/paper-server/patches/features/0033-Speculative-Portal-Loading.patch @@ -0,0 +1,106 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Newwind +Date: Mon, 16 Feb 2026 23:35:37 +0200 +Subject: [PATCH] Speculative portal chunk loading + +Nether portals loading chunks on the main thread is a big cause of lag, freezing the server upwards of 1-2 seconds. +Since there isn't an easy way to load these chunks async, we can instead preload them, as it takes some time for survival players to travel through a portal. +This shouldn't break any vanilla behaviour as the chunk ticket does not simulate. + +diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java +index af67b07..c1f2f63 100644 +--- a/net/minecraft/server/level/ServerChunkCache.java ++++ b/net/minecraft/server/level/ServerChunkCache.java +@@ -679,9 +679,13 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } + + public CompletableFuture addTicketAndLoadWithRadius(TicketType ticketType, ChunkPos chunkPos, int radius) { ++ return addTicketAndLoadWithRadius(ticketType, chunkPos, radius, ca.spottedleaf.concurrentutil.util.Priority.NORMAL); ++ } ++ ++ public CompletableFuture addTicketAndLoadWithRadius(TicketType ticketType, ChunkPos chunkPos, int radius, ca.spottedleaf.concurrentutil.util.Priority priority) { + // Paper start - rewrite chunk system +- return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAndLoadWithRadius( +- ticketType, chunkPos, radius, ChunkStatus.FULL, ca.spottedleaf.concurrentutil.util.Priority.NORMAL ++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel) this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAndLoadWithRadius( ++ ticketType, chunkPos, radius, ChunkStatus.FULL, priority + ); + // Paper end - rewrite chunk system + } +diff --git a/net/minecraft/server/level/TicketType.java b/net/minecraft/server/level/TicketType.java +index 6d5bfc6..a6024d3 100644 +--- a/net/minecraft/server/level/TicketType.java ++++ b/net/minecraft/server/level/TicketType.java +@@ -68,6 +68,7 @@ public final class TicketType implements ca.spottedleaf.moonrise.patches.chun + public static final TicketType PLUGIN_TICKET = register("plugin_ticket", NO_TIMEOUT, FLAG_LOADING | FLAG_SIMULATION); static { ((TicketType)PLUGIN_TICKET).moonrise$setIdentifierComparator((org.bukkit.plugin.Plugin p1, org.bukkit.plugin.Plugin p2) -> p1.getName().compareTo(p2.getName())); } // Paper // Paper - rewrite chunk system + public static final TicketType FUTURE_AWAIT = register("future_await", NO_TIMEOUT, FLAG_LOADING | FLAG_SIMULATION); // Paper + public static final TicketType CHUNK_LOAD = register("chunk_load", NO_TIMEOUT, FLAG_LOADING); // Paper - moonrise ++ public static final TicketType SPECULATIVE_PORTAL = Registry.register(BuiltInRegistries.TICKET_TYPE, "speculative_portal", new TicketType<>(100L, TicketType.FLAG_LOADING | TicketType.FLAG_CAN_EXPIRE_IF_UNLOADED)); // Paper - Speculative portal loading + + private static TicketType register(String name, long timeout, @TicketType.Flags int flags) { + return Registry.register(BuiltInRegistries.TICKET_TYPE, name, new TicketType(timeout, flags)); +diff --git a/net/minecraft/world/entity/PortalProcessor.java b/net/minecraft/world/entity/PortalProcessor.java +index 4e69e32..22992e0 100644 +--- a/net/minecraft/world/entity/PortalProcessor.java ++++ b/net/minecraft/world/entity/PortalProcessor.java +@@ -6,6 +6,8 @@ import net.minecraft.world.level.block.Portal; + import net.minecraft.world.level.portal.TeleportTransition; + import org.jspecify.annotations.Nullable; + ++import static net.minecraft.server.level.TicketType.SPECULATIVE_PORTAL; ++ + public class PortalProcessor { + private final Portal portal; + private BlockPos entryPosition; +@@ -24,9 +26,50 @@ public class PortalProcessor { + return false; + } else { + this.insidePortalThisTick = false; +- return canChangeDimensions && this.portalTime++ >= this.portal.getPortalTransitionTime(level, entity); ++ ++ // Paper start - Portal speculator ++ int transitionTime = this.portal.getPortalTransitionTime(level, entity); ++ ++ if (level.paperConfig().environment.netherPortalPreloading) { ++ int ticksBefore = level.paperConfig().environment.netherPortalPreloadingTicksBefore; ++ if (canChangeDimensions && this.portalTime == transitionTime - ticksBefore && portal instanceof net.minecraft.world.level.block.NetherPortalBlock) { ++ preloadPortalDestinationChunks(level, entity); ++ } ++ } ++ ++ return canChangeDimensions && this.portalTime++ >= transitionTime; ++ // Paper end ++ } ++ } ++ ++ // Paper start - Portal speculator, copying from NetherPortalBlock.getPortalDestination() ++ private void preloadPortalDestinationChunks(ServerLevel sourceLevel, Entity entity) { ++ net.minecraft.resources.ResourceKey resourceKey = sourceLevel.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER ? net.minecraft.world.level.Level.OVERWORLD : net.minecraft.world.level.Level.NETHER; ++ ServerLevel destinationLevel = sourceLevel.getServer().getLevel(resourceKey); ++ ++ boolean flag = destinationLevel.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER; ++ net.minecraft.world.level.border.WorldBorder worldBorder = destinationLevel.getWorldBorder(); ++ double teleportationScale = net.minecraft.world.level.dimension.DimensionType.getTeleportationScale(sourceLevel.dimensionType(), destinationLevel.dimensionType()); ++ BlockPos blockPos = worldBorder.clampToBounds(entity.getX() * teleportationScale, entity.getY(), entity.getZ() * teleportationScale); ++ ++ int portalSearchRadius = destinationLevel.paperConfig().environment.portalSearchRadius; ++ if (entity.level().paperConfig().environment.portalSearchVanillaDimensionScaling && flag) { // flag = is going to nether ++ portalSearchRadius = (int) (portalSearchRadius / destinationLevel.dimensionType().coordinateScale()); + } ++ ++ addLoadingTicketToChunks(destinationLevel, blockPos, portalSearchRadius); ++ } ++ ++ private void addLoadingTicketToChunks(ServerLevel level, BlockPos center, int portalSearchRadius) { ++ net.minecraft.server.level.ServerChunkCache chunkSource = level.getChunkSource(); ++ ++ int chunkRadius = (portalSearchRadius + 15) >> 4; ++ ++ chunkSource.addTicketAndLoadWithRadius(SPECULATIVE_PORTAL, new net.minecraft.world.level.ChunkPos(center), chunkRadius, ++ ca.spottedleaf.concurrentutil.util.Priority.HIGHEST ++ ); + } ++ // Paper end + + public @Nullable TeleportTransition getPortalDestination(ServerLevel level, Entity entity) { + return this.portal.getPortalDestination(level, entity, this.entryPosition); From 672396341e455d5aa3c1630ad9e5a1312f759882 Mon Sep 17 00:00:00 2001 From: Newwind Date: Mon, 16 Feb 2026 23:42:16 +0000 Subject: [PATCH 2/6] Update WorldConfiguration.java --- .../java/io/papermc/paper/configuration/WorldConfiguration.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java index d34e627f8b67..e941fde0c8de 100644 --- a/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java +++ b/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java @@ -441,6 +441,8 @@ public class TreasureMaps extends ConfigurationPart { public int waterOverLavaFlowSpeed = 5; public int portalSearchRadius = 128; public int portalCreateRadius = 16; + public boolean netherPortalPreloading; + public int netherPortalPreloadingTicksBefore = 30; public boolean portalSearchVanillaDimensionScaling = true; public IntOr.Disabled netherCeilingVoidDamageHeight = IntOr.Disabled.DISABLED; public int maxFluidTicks = 65536; From 67861008c71134f6ef3bbcdd30227e5e40939524 Mon Sep 17 00:00:00 2001 From: Newwind Date: Tue, 17 Feb 2026 13:50:17 +0000 Subject: [PATCH 3/6] Update 0033-Speculative-Portal-Loading.patch --- .../0033-Speculative-Portal-Loading.patch | 38 ++----------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/paper-server/patches/features/0033-Speculative-Portal-Loading.patch b/paper-server/patches/features/0033-Speculative-Portal-Loading.patch index c4ae1f744573..1341179cd17b 100644 --- a/paper-server/patches/features/0033-Speculative-Portal-Loading.patch +++ b/paper-server/patches/features/0033-Speculative-Portal-Loading.patch @@ -7,40 +7,8 @@ Nether portals loading chunks on the main thread is a big cause of lag, freezing Since there isn't an easy way to load these chunks async, we can instead preload them, as it takes some time for survival players to travel through a portal. This shouldn't break any vanilla behaviour as the chunk ticket does not simulate. -diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java -index af67b07..c1f2f63 100644 ---- a/net/minecraft/server/level/ServerChunkCache.java -+++ b/net/minecraft/server/level/ServerChunkCache.java -@@ -679,9 +679,13 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - } - - public CompletableFuture addTicketAndLoadWithRadius(TicketType ticketType, ChunkPos chunkPos, int radius) { -+ return addTicketAndLoadWithRadius(ticketType, chunkPos, radius, ca.spottedleaf.concurrentutil.util.Priority.NORMAL); -+ } -+ -+ public CompletableFuture addTicketAndLoadWithRadius(TicketType ticketType, ChunkPos chunkPos, int radius, ca.spottedleaf.concurrentutil.util.Priority priority) { - // Paper start - rewrite chunk system -- return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAndLoadWithRadius( -- ticketType, chunkPos, radius, ChunkStatus.FULL, ca.spottedleaf.concurrentutil.util.Priority.NORMAL -+ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel) this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAndLoadWithRadius( -+ ticketType, chunkPos, radius, ChunkStatus.FULL, priority - ); - // Paper end - rewrite chunk system - } -diff --git a/net/minecraft/server/level/TicketType.java b/net/minecraft/server/level/TicketType.java -index 6d5bfc6..a6024d3 100644 ---- a/net/minecraft/server/level/TicketType.java -+++ b/net/minecraft/server/level/TicketType.java -@@ -68,6 +68,7 @@ public final class TicketType implements ca.spottedleaf.moonrise.patches.chun - public static final TicketType PLUGIN_TICKET = register("plugin_ticket", NO_TIMEOUT, FLAG_LOADING | FLAG_SIMULATION); static { ((TicketType)PLUGIN_TICKET).moonrise$setIdentifierComparator((org.bukkit.plugin.Plugin p1, org.bukkit.plugin.Plugin p2) -> p1.getName().compareTo(p2.getName())); } // Paper // Paper - rewrite chunk system - public static final TicketType FUTURE_AWAIT = register("future_await", NO_TIMEOUT, FLAG_LOADING | FLAG_SIMULATION); // Paper - public static final TicketType CHUNK_LOAD = register("chunk_load", NO_TIMEOUT, FLAG_LOADING); // Paper - moonrise -+ public static final TicketType SPECULATIVE_PORTAL = Registry.register(BuiltInRegistries.TICKET_TYPE, "speculative_portal", new TicketType<>(100L, TicketType.FLAG_LOADING | TicketType.FLAG_CAN_EXPIRE_IF_UNLOADED)); // Paper - Speculative portal loading - - private static TicketType register(String name, long timeout, @TicketType.Flags int flags) { - return Registry.register(BuiltInRegistries.TICKET_TYPE, name, new TicketType(timeout, flags)); diff --git a/net/minecraft/world/entity/PortalProcessor.java b/net/minecraft/world/entity/PortalProcessor.java -index 4e69e32..22992e0 100644 +index 4e69e32..5b41a8a 100644 --- a/net/minecraft/world/entity/PortalProcessor.java +++ b/net/minecraft/world/entity/PortalProcessor.java @@ -6,6 +6,8 @@ import net.minecraft.world.level.block.Portal; @@ -61,8 +29,8 @@ index 4e69e32..22992e0 100644 + // Paper start - Portal speculator + int transitionTime = this.portal.getPortalTransitionTime(level, entity); + -+ if (level.paperConfig().environment.netherPortalPreloading) { -+ int ticksBefore = level.paperConfig().environment.netherPortalPreloadingTicksBefore; ++ if (level.paperConfig().environment.netherPortalPreloadingTicksBefore.enabled()) { ++ int ticksBefore = level.paperConfig().environment.netherPortalPreloadingTicksBefore.intValue(); + if (canChangeDimensions && this.portalTime == transitionTime - ticksBefore && portal instanceof net.minecraft.world.level.block.NetherPortalBlock) { + preloadPortalDestinationChunks(level, entity); + } From ebdfc6a911e4edeb0d0514ba08cfb36e363c35a7 Mon Sep 17 00:00:00 2001 From: Newwind Date: Tue, 17 Feb 2026 13:51:11 +0000 Subject: [PATCH 4/6] Update WorldConfiguration.java --- .../io/papermc/paper/configuration/WorldConfiguration.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java index e941fde0c8de..88a202c637a2 100644 --- a/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java +++ b/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java @@ -441,9 +441,8 @@ public class TreasureMaps extends ConfigurationPart { public int waterOverLavaFlowSpeed = 5; public int portalSearchRadius = 128; public int portalCreateRadius = 16; - public boolean netherPortalPreloading; - public int netherPortalPreloadingTicksBefore = 30; public boolean portalSearchVanillaDimensionScaling = true; + public IntOr.Disabled netherPortalPreloadingTicksBefore = IntOr.Disabled.DISABLED; public IntOr.Disabled netherCeilingVoidDamageHeight = IntOr.Disabled.DISABLED; public int maxFluidTicks = 65536; public int maxBlockTicks = 65536; From f50ce16b20fc9be14d82d5a7a008dabdfd3b74f3 Mon Sep 17 00:00:00 2001 From: Newwind Date: Tue, 17 Feb 2026 13:56:10 +0000 Subject: [PATCH 5/6] Update 0033-Speculative-Portal-Loading.patch --- .../0033-Speculative-Portal-Loading.patch | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/paper-server/patches/features/0033-Speculative-Portal-Loading.patch b/paper-server/patches/features/0033-Speculative-Portal-Loading.patch index 1341179cd17b..9bf7228cfb05 100644 --- a/paper-server/patches/features/0033-Speculative-Portal-Loading.patch +++ b/paper-server/patches/features/0033-Speculative-Portal-Loading.patch @@ -7,6 +7,20 @@ Nether portals loading chunks on the main thread is a big cause of lag, freezing Since there isn't an easy way to load these chunks async, we can instead preload them, as it takes some time for survival players to travel through a portal. This shouldn't break any vanilla behaviour as the chunk ticket does not simulate. +diff --git a/net/minecraft/server/level/TicketType.java b/net/minecraft/server/level/TicketType.java +index 6d5bfc6..a6024d3 100644 +--- a/net/minecraft/server/level/TicketType.java ++++ b/net/minecraft/server/level/TicketType.java +@@ -68,6 +68,7 @@ public final class TicketType implements ca.spottedleaf.moonrise.patches.chun + public static final TicketType PLUGIN_TICKET = register("plugin_ticket", NO_TIMEOUT, FLAG_LOADING | FLAG_SIMULATION); static { ((TicketType)PLUGIN_TICKET).moonrise$setIdentifierComparator((org.bukkit.plugin.Plugin p1, org.bukkit.plugin.Plugin p2) -> p1.getName().compareTo(p2.getName())); } // Paper // Paper - rewrite chunk system + public static final TicketType FUTURE_AWAIT = register("future_await", NO_TIMEOUT, FLAG_LOADING | FLAG_SIMULATION); // Paper + public static final TicketType CHUNK_LOAD = register("chunk_load", NO_TIMEOUT, FLAG_LOADING); // Paper - moonrise ++ public static final TicketType SPECULATIVE_PORTAL = Registry.register(BuiltInRegistries.TICKET_TYPE, "speculative_portal", new TicketType<>(100L, TicketType.FLAG_LOADING | TicketType.FLAG_CAN_EXPIRE_IF_UNLOADED)); // Paper - Speculative portal loading + + private static TicketType register(String name, long timeout, @TicketType.Flags int flags) { + return Registry.register(BuiltInRegistries.TICKET_TYPE, name, new TicketType(timeout, flags)); + + diff --git a/net/minecraft/world/entity/PortalProcessor.java b/net/minecraft/world/entity/PortalProcessor.java index 4e69e32..5b41a8a 100644 --- a/net/minecraft/world/entity/PortalProcessor.java @@ -72,3 +86,24 @@ index 4e69e32..5b41a8a 100644 public @Nullable TeleportTransition getPortalDestination(ServerLevel level, Entity entity) { return this.portal.getPortalDestination(level, entity, this.entryPosition); + +diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java +index af67b07..c1f2f63 100644 +--- a/net/minecraft/server/level/ServerChunkCache.java ++++ b/net/minecraft/server/level/ServerChunkCache.java +@@ -679,9 +679,13 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } + + public CompletableFuture addTicketAndLoadWithRadius(TicketType ticketType, ChunkPos chunkPos, int radius) { ++ return addTicketAndLoadWithRadius(ticketType, chunkPos, radius, ca.spottedleaf.concurrentutil.util.Priority.NORMAL); ++ } ++ ++ public CompletableFuture addTicketAndLoadWithRadius(TicketType ticketType, ChunkPos chunkPos, int radius, ca.spottedleaf.concurrentutil.util.Priority priority) { + // Paper start - rewrite chunk system +- return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAndLoadWithRadius( +- ticketType, chunkPos, radius, ChunkStatus.FULL, ca.spottedleaf.concurrentutil.util.Priority.NORMAL ++ return ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel) this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAndLoadWithRadius( ++ ticketType, chunkPos, radius, ChunkStatus.FULL, priority + ); + // Paper end - rewrite chunk system + } From 2960c6aa15d3a8522f720485be8d9eca6015b651 Mon Sep 17 00:00:00 2001 From: Newwind Date: Tue, 17 Feb 2026 14:09:48 +0000 Subject: [PATCH 6/6] Update 0033-Speculative-Portal-Loading.patch --- .../patches/features/0033-Speculative-Portal-Loading.patch | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paper-server/patches/features/0033-Speculative-Portal-Loading.patch b/paper-server/patches/features/0033-Speculative-Portal-Loading.patch index 9bf7228cfb05..fc58137d123c 100644 --- a/paper-server/patches/features/0033-Speculative-Portal-Loading.patch +++ b/paper-server/patches/features/0033-Speculative-Portal-Loading.patch @@ -8,14 +8,14 @@ Since there isn't an easy way to load these chunks async, we can instead preload This shouldn't break any vanilla behaviour as the chunk ticket does not simulate. diff --git a/net/minecraft/server/level/TicketType.java b/net/minecraft/server/level/TicketType.java -index 6d5bfc6..a6024d3 100644 +index 6d5bfc6..fc772bf 100644 --- a/net/minecraft/server/level/TicketType.java +++ b/net/minecraft/server/level/TicketType.java @@ -68,6 +68,7 @@ public final class TicketType implements ca.spottedleaf.moonrise.patches.chun public static final TicketType PLUGIN_TICKET = register("plugin_ticket", NO_TIMEOUT, FLAG_LOADING | FLAG_SIMULATION); static { ((TicketType)PLUGIN_TICKET).moonrise$setIdentifierComparator((org.bukkit.plugin.Plugin p1, org.bukkit.plugin.Plugin p2) -> p1.getName().compareTo(p2.getName())); } // Paper // Paper - rewrite chunk system public static final TicketType FUTURE_AWAIT = register("future_await", NO_TIMEOUT, FLAG_LOADING | FLAG_SIMULATION); // Paper public static final TicketType CHUNK_LOAD = register("chunk_load", NO_TIMEOUT, FLAG_LOADING); // Paper - moonrise -+ public static final TicketType SPECULATIVE_PORTAL = Registry.register(BuiltInRegistries.TICKET_TYPE, "speculative_portal", new TicketType<>(100L, TicketType.FLAG_LOADING | TicketType.FLAG_CAN_EXPIRE_IF_UNLOADED)); // Paper - Speculative portal loading ++ public static final TicketType SPECULATIVE_PORTAL = Registry.register(BuiltInRegistries.TICKET_TYPE, "speculative_portal", new TicketType<>(300L, FLAG_LOADING | FLAG_KEEP_DIMENSION_ACTIVE)); // Paper - Speculative portal loading private static TicketType register(String name, long timeout, @TicketType.Flags int flags) { return Registry.register(BuiltInRegistries.TICKET_TYPE, name, new TicketType(timeout, flags));