diff --git a/sparklypaper-api/paper-patches/files/src/main/java/com/destroystokyo/paper/MaterialTags.java.patch b/sparklypaper-api/paper-patches/files/src/main/java/com/destroystokyo/paper/MaterialTags.java.patch index 81d7577..cc67acc 100644 --- a/sparklypaper-api/paper-patches/files/src/main/java/com/destroystokyo/paper/MaterialTags.java.patch +++ b/sparklypaper-api/paper-patches/files/src/main/java/com/destroystokyo/paper/MaterialTags.java.patch @@ -1,6 +1,6 @@ --- a/src/main/java/com/destroystokyo/paper/MaterialTags.java +++ b/src/main/java/com/destroystokyo/paper/MaterialTags.java -@@ -83,6 +_,7 @@ +@@ -99,6 +_,7 @@ */ public static final MaterialSetTag CONCRETES = new MaterialSetTag(keyFor("concretes")) .endsWith("_CONCRETE") @@ -8,7 +8,7 @@ .ensureSize("CONCRETES", 16).lock(); /** -@@ -161,6 +_,7 @@ +@@ -181,6 +_,7 @@ .endsWith("TERRACOTTA") .not(Material.TERRACOTTA) .notEndsWith("GLAZED_TERRACOTTA") @@ -16,7 +16,7 @@ .ensureSize("STAINED_TERRACOTTA", 16).lock(); /** -@@ -168,6 +_,7 @@ +@@ -188,6 +_,7 @@ */ public static final MaterialSetTag TERRACOTTA = new MaterialSetTag(keyFor("terracotta")) .endsWith("TERRACOTTA") diff --git a/sparklypaper-api/paper-patches/files/src/main/java/org/bukkit/Material.java.patch b/sparklypaper-api/paper-patches/files/src/main/java/org/bukkit/Material.java.patch index 57b02ce..823ac7f 100644 --- a/sparklypaper-api/paper-patches/files/src/main/java/org/bukkit/Material.java.patch +++ b/sparklypaper-api/paper-patches/files/src/main/java/org/bukkit/Material.java.patch @@ -1,6 +1,6 @@ --- a/src/main/java/org/bukkit/Material.java +++ b/src/main/java/org/bukkit/Material.java -@@ -1718,6 +_,16 @@ +@@ -1791,6 +_,16 @@ ZOMBIE_HEAD(-1, Skull.class), ZOMBIE_WALL_HEAD(-1, WallSkull.class), // End generate - Blocks diff --git a/sparklypaper-server/minecraft-patches/features/0001-Parallel-World-Ticking.patch b/sparklypaper-server/minecraft-patches/features/0001-Parallel-World-Ticking.patch new file mode 100644 index 0000000..8474662 --- /dev/null +++ b/sparklypaper-server/minecraft-patches/features/0001-Parallel-World-Ticking.patch @@ -0,0 +1,823 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrPowerGamerBR +Date: Thu, 25 Sep 2025 21:17:14 -0300 +Subject: [PATCH] Parallel World Ticking + + +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +index 80f13315e8c7c0e3c82ab98f92387f56bb3b6481..d1cef62d7003f675d902c1c88ee42422ec1a8404 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +@@ -1124,7 +1124,7 @@ public final class ChunkHolderManager { + if (changedFullStatus.isEmpty()) { + return; + } +- if (!TickThread.isTickThread()) { ++ if (!TickThread.isTickThreadFor(world)) { // SparklyPaper - parallel world ticking + // These will be handled on the next ServerChunkCache$MainThreadExecutor#pollTask, as it runs the distance manager update + // which will invoke processTicketUpdates + this.offThreadPendingFullLoadUpdate.addAll(changedFullStatus); +@@ -1145,7 +1145,7 @@ public final class ChunkHolderManager { + + // note: never call while inside the chunk system, this will absolutely break everything + public void processUnloads() { +- TickThread.ensureTickThread("Cannot unload chunks off-main"); ++ TickThread.ensureTickThread(world, "Cannot unload chunks off-main"); // SparklyPaper - parallel world ticking + + if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { + throw new IllegalStateException("Cannot unload chunks recursively"); +@@ -1411,12 +1411,12 @@ public final class ChunkHolderManager { + if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { + throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager"); + } +- final boolean isTickThread = TickThread.isTickThread(); ++ final boolean isTickThread = TickThread.isTickThreadFor(world); + + if (!PlatformHooks.get().allowAsyncTicketUpdates() && isTickThread) { + TickThread.ensureTickThread("Cannot asynchronously process ticket updates"); + } +- ++ + boolean ret = false; + + if (this.ticketLevelPropagator.hasPendingUpdates()) { +diff --git a/net/minecraft/core/dispenser/DispenseItemBehavior.java b/net/minecraft/core/dispenser/DispenseItemBehavior.java +index 703a75c7c6cd05a95afb630973250898dbc7223d..1bc84d3aa569222d20bc3c05b99de7590c35a195 100644 +--- a/net/minecraft/core/dispenser/DispenseItemBehavior.java ++++ b/net/minecraft/core/dispenser/DispenseItemBehavior.java +@@ -410,8 +410,8 @@ public interface DispenseItemBehavior { + // CraftBukkit start + level.captureTreeGeneration = false; + if (!level.capturedBlockStates.isEmpty()) { +- org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType; +- net.minecraft.world.level.block.SaplingBlock.treeType = null; ++ org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeTypeRT.get(); // SparklyPaper - parallel world ticking ++ net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(null); // SparklyPaper - parallel world ticking + org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(blockPos, level); + List states = new java.util.ArrayList<>(level.capturedBlockStates.values()); + level.capturedBlockStates.clear(); +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 4607ba3fbc327fd1f6a7701af05cd18d6e4107e6..e34a8bea43d1aa82081b21c468d3b8315d77c151 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -310,6 +310,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping + public static final long SERVER_INIT = System.nanoTime(); // Paper - Lag compensation ++ public java.util.concurrent.Semaphore serverLevelTickingSemaphore = null; // SparklyPaper - parallel world ticking + + public static S spin(Function threadFunction) { + ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system +@@ -1642,6 +1643,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop> tasks = new java.util.ArrayDeque<>(); ++ try { + for (ServerLevel serverLevel : this.getAllLevels()) { + serverLevel.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent + serverLevel.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent +@@ -1658,18 +1662,45 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { ++ try { ++ ca.spottedleaf.moonrise.common.util.TickThread.ServerLevelTickThread currentThread = (ca.spottedleaf.moonrise.common.util.TickThread.ServerLevelTickThread) Thread.currentThread(); ++ currentThread.currentlyTickingServerLevel = serverLevel; ++ ++ long i = Util.getNanos(); // SparklyPaper - track world's MSPT ++ serverLevel.tick(hasTimeLeft); ++ // SparklyPaper start - track world's MSPT ++ long j = Util.getNanos() - i; ++ ++ // These are from the "tickServer" function ++ serverLevel.tickTimes5s.add(this.tickCount, j); ++ serverLevel.tickTimes10s.add(this.tickCount, j); ++ serverLevel.tickTimes60s.add(this.tickCount, j); ++ // SparklyPaper end ++ } catch (Throwable throwable) { ++ CrashReport crashreport = CrashReport.forThrowable(throwable, "Exception ticking world"); ++ serverLevel.fillReportDetails(crashreport); ++ throw new ReportedException(crashreport); ++ } finally { ++ serverLevelTickingSemaphore.release(); ++ } ++ }, serverLevel) ++ ); + + profilerFiller.pop(); + profilerFiller.pop(); + serverLevel.explosionDensityCache.clear(); // Paper - Optimize explosions + } ++ ++ while (!tasks.isEmpty()) { ++ tasks.pop().get(); ++ } ++ } catch (java.lang.InterruptedException | java.util.concurrent.ExecutionException e) { ++ throw new RuntimeException(e); // Propagate exception ++ } ++ // SparklyPaper end + this.isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked + + profilerFiller.popPush("connection"); +@@ -1772,6 +1803,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop, ServerLevel> oldLevels = this.levels; + Map, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels); + newLevels.remove(level.dimension()); ++ level.tickExecutor.shutdown(); // SparklyPaper - parallel world ticking (We remove it in here instead of ServerLevel.close() because ServerLevel.close() is never called!) + this.levels = Collections.unmodifiableMap(newLevels); + } + // CraftBukkit end +diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java +index bd10a93b7cb856459bdd2639b245fe7da84f0063..1f9db5bed6796b9a07e30535877723b82468772b 100644 +--- a/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/net/minecraft/server/dedicated/DedicatedServer.java +@@ -269,6 +269,10 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + } + net.sparklypower.sparklypaper.SparklyPaperCommands.INSTANCE.registerCommands(this); + // SparklyPaper end ++ // SparklyPaper start - parallel world ticking start ++ serverLevelTickingSemaphore = new java.util.concurrent.Semaphore(net.sparklypower.sparklypaper.configs.SparklyPaperConfigUtils.INSTANCE.getConfig().getParallelWorldTicking().getThreads()); ++ DedicatedServer.LOGGER.info("Using " + serverLevelTickingSemaphore.availablePermits() + " permits for parallel world ticking"); // SparklyPaper - parallel world ticking ++ // SparklyPaper end + // Paper start - initialize global and world-defaults configuration + this.paperConfigurations.initializeGlobalConfiguration(this.registryAccess()); + this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess()); +diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java +index 413f0673557b9e3f9177d15e9bef61bded209e34..db9c312c13c971e75fc4afeff5159564b69f5ca0 100644 +--- a/net/minecraft/server/level/ServerChunkCache.java ++++ b/net/minecraft/server/level/ServerChunkCache.java +@@ -178,7 +178,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + + // call mid-tick tasks for chunk system + if ((i & 7) == 0) { +- ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.level.getServer()).moonrise$executeMidTickTasks(); ++ // ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.level.getServer()).moonrise$executeMidTickTasks(); // SparklyPaper - parallel world ticking (only run mid-tick at the end of each tick / fixes concurrency bugs related to executeMidTickTasks) + continue; + } + } +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index 6620678f5d51379c376b425302a98200283ab84f..25910e2bbe55852d6cd5f1338cec4041356e96b8 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -188,7 +188,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + public final ServerChunkCache chunkSource; + private final MinecraftServer server; + public final net.minecraft.world.level.storage.PrimaryLevelData serverLevelData; // CraftBukkit - type +- final EntityTickList entityTickList = new EntityTickList(); ++ final EntityTickList entityTickList = new EntityTickList(this); // SparklyPaper - parallel world ticking + private final ServerWaypointManager waypointManager; + // Paper - rewrite chunk system + private final GameEventDispatcher gameEventDispatcher; +@@ -214,6 +214,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + private final boolean tickTime; + private final RandomSequences randomSequences; + final LevelDebugSynchronizers debugSynchronizers = new LevelDebugSynchronizers(this); ++ public java.util.concurrent.ExecutorService tickExecutor; // SparklyPaper - parallel world ticking + + // CraftBukkit start + public final LevelStorageSource.LevelStorageAccess levelStorageAccess; +@@ -682,7 +683,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + this.sleepStatus = new SleepStatus(); + this.gameEventDispatcher = new GameEventDispatcher(this); + this.randomSequences = Objects.requireNonNullElseGet(randomSequences, () -> this.getDataStorage().computeIfAbsent(RandomSequences.TYPE)); +- this.waypointManager = new ServerWaypointManager(); ++ this.waypointManager = new ServerWaypointManager(this); // SparklyPaper - parallel world ticking + // Paper start - rewrite chunk system + this.moonrise$setEntityLookup(new ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup((ServerLevel)(Object)this, ((ServerLevel)(Object)this).new EntityCallbacks())); + this.chunkTaskScheduler = new ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler((ServerLevel)(Object)this); +@@ -698,6 +699,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this, this.chunkTaskScheduler); + // Paper end - rewrite chunk system + this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit ++ this.tickExecutor = java.util.concurrent.Executors.newSingleThreadExecutor(new net.sparklypower.sparklypaper.ServerLevelTickExecutorThreadFactory(getWorld().getName())); // SparklyPaper - parallel world ticking + } + + // Paper start +@@ -1265,7 +1267,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + // Paper start - rewrite chunk system + if ((++this.tickedBlocksOrFluids & 7L) != 0L) { +- ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks(); ++ // ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks(); // SparklyPaper - parallel world ticking (only run mid-tick at the end of each tick / fixes concurrency bugs related to executeMidTickTasks) + } + // Paper end - rewrite chunk system + +@@ -1278,7 +1280,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + // Paper start - rewrite chunk system + if ((++this.tickedBlocksOrFluids & 7L) != 0L) { +- ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks(); ++ // ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks(); // SparklyPaper - parallel world ticking (only run mid-tick at the end of each tick / fixes concurrency bugs related to executeMidTickTasks) + } + // Paper end - rewrite chunk system + +@@ -1549,6 +1551,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + private void addPlayer(ServerPlayer player) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot add player off-main"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + Entity entity = this.getEntity(player.getUUID()); + if (entity != null) { + LOGGER.warn("Force-added player with duplicate UUID {}", player.getUUID()); +@@ -1561,7 +1564,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + // CraftBukkit start + private boolean addEntity(Entity entity, @Nullable org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason) { +- org.spigotmc.AsyncCatcher.catchOp("entity add"); // Spigot ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot add entity off-main"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + entity.generation = false; // Paper - Don't fire sync event during generation; Reset flag if it was added during a ServerLevel generation process + // Paper start - extra debug info + if (entity.valid) { +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index 2a10190f00d406b4fc747ef207a399f6d3fa4c08..cb42ab07921b1ddc0411cf7c64548a1c6be2b6d1 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -472,6 +472,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + return this.viewDistanceHolder; + } + // Paper end - rewrite chunk system ++ public boolean hasTickedAtLeastOnceInNewWorld = false; // SparklyPaper - parallel world ticking (fixes bug in DreamResourceReset where the inventory is opened AFTER the player has changed worlds, if you click with the quick tp torch in a chest, because the inventory is opened AFTER the player has teleported) + + public ServerPlayer(MinecraftServer server, ServerLevel level, GameProfile gameProfile, ClientInformation clientInformation) { + super(level, gameProfile); +@@ -716,6 +717,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + + @Override + public void tick() { ++ hasTickedAtLeastOnceInNewWorld = true; // SparklyPaper - parallel world ticking (see: PARALLEL_NOTES.md - Opening an inventory after a world switch) + // CraftBukkit start + if (this.joining) { + this.joining = false; +@@ -1518,6 +1520,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + return this; + } else { + // CraftBukkit start ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureOnlyTickThread("Cannot change dimension of a player off-main, from world " + serverLevel.getWorld().getName() + " to world " + level.getWorld().getName()); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + /* + this.isChangingDimension = true; + LevelData levelData = level.getLevelData(); +@@ -1857,6 +1860,12 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + return OptionalInt.empty(); + } else { + // CraftBukkit start ++ // SparklyPaper start - parallel world ticking (see: PARALLEL_NOTES.md - Opening an inventory after a world switch) ++ if (!hasTickedAtLeastOnceInNewWorld) { ++ MinecraftServer.LOGGER.warn("Ignoring request to open container " + abstractContainerMenu + " because we haven't ticked in the current world yet!", new Throwable()); ++ return OptionalInt.empty(); ++ } ++ // SparklyPaper end + this.containerMenu = abstractContainerMenu; // Moved up + if (!this.isImmobile()) + this.connection +@@ -1921,6 +1930,11 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + } + @Override + public void closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) { ++ // SparklyPaper start - parallel world ticking (debugging) ++ if (net.sparklypower.sparklypaper.configs.SparklyPaperConfigUtils.INSTANCE.getLogContainerCreationStacktraces()) { ++ MinecraftServer.LOGGER.warn("Closing " + this.getBukkitEntity().getName() + " inventory that was created at", this.containerMenu.containerCreationStacktrace); ++ } ++ // SparklyPaper end + org.bukkit.craftbukkit.event.CraftEventFactory.handleInventoryCloseEvent(this, reason); // CraftBukkit + // Paper end - Inventory close reason + this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId)); +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 9d7be1695f797c2c041d21a8326aeea46b0488d7..dfc77fa5baead50008381272b0f8e06e54346be5 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -110,7 +110,7 @@ public abstract class PlayerList { + private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); + private final MinecraftServer server; + public final List players = new java.util.concurrent.CopyOnWriteArrayList(); // CraftBukkit - ArrayList -> CopyOnWriteArrayList: Iterator safety +- private final Map playersByUUID = Maps.newHashMap(); ++ private final Map playersByUUID = Maps.newHashMap(); // SparklyPaper - parallel world ticking (we don't need to replace the original map because we never iterate on top of this map) + private final UserBanList bans; + private final IpBanList ipBans; + private final ServerOpList ops; +@@ -128,7 +128,7 @@ public abstract class PlayerList { + + // CraftBukkit start + private org.bukkit.craftbukkit.CraftServer cserver; +- private final Map playersByName = new java.util.HashMap<>(); ++ private final Map playersByName = new java.util.HashMap<>(); // SparklyPaper - parallel world ticking (we don't need to replace the original map because we never iterate on top of this map) + public @Nullable String collideRuleTeamName; // Paper - Configurable player collision + + public PlayerList( +@@ -149,6 +149,7 @@ public abstract class PlayerList { + abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor + + public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie cookie) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureOnlyTickThread("Cannot place new player off-main"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + player.isRealPlayer = true; // Paper + player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed + NameAndId nameAndId = player.nameAndId(); +@@ -591,6 +592,12 @@ public abstract class PlayerList { + } + + public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason, @Nullable org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason, @Nullable org.bukkit.Location location) { ++ // SparklyPaper - parallel world ticking (additional concurrency issues logs) ++ if (location != null) // TODO: Is this really never null, or is IntelliJ IDEA tripping? Because I'm pretty sure that this can be null and there isn't any @NotNull annotations ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureOnlyTickThread("Cannot respawn player off-main, from world " + player.level().getWorld().getName() + " to world " + location.getWorld().getName()); ++ else ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureOnlyTickThread("Cannot respawn player off-main, respawning in world " + player.level().getWorld().getName()); ++ // SparklyPaper end + player.stopRiding(); // CraftBukkit + // TeleportTransition teleportTransition = player.findRespawnPositionAndUseSpawnBlock(!keepInventory, TeleportTransition.DO_NOTHING); + this.players.remove(player); +@@ -601,6 +608,7 @@ public abstract class PlayerList { + ServerPlayer serverPlayer = player; + Level fromWorld = player.level(); + player.wonGame = false; ++ serverPlayer.hasTickedAtLeastOnceInNewWorld = false; // SparklyPaper - parallel world ticking (see: PARALLEL_NOTES.md - Opening an inventory after a world switch) + // CraftBukkit end + serverPlayer.connection = player.connection; + serverPlayer.restoreFrom(player, keepInventory); +diff --git a/net/minecraft/server/waypoints/ServerWaypointManager.java b/net/minecraft/server/waypoints/ServerWaypointManager.java +index f9e7532f86122a379692561a639a209a126e8bba..2709dfae53c1a210f36c45fa0df0d49536341a38 100644 +--- a/net/minecraft/server/waypoints/ServerWaypointManager.java ++++ b/net/minecraft/server/waypoints/ServerWaypointManager.java +@@ -20,8 +20,16 @@ public class ServerWaypointManager implements WaypointManager players = new HashSet<>(); + private final Table connections = HashBasedTable.create(); + ++ // SparklyPaper start - parallel world ticking ++ private final net.minecraft.server.level.ServerLevel level; ++ public ServerWaypointManager(net.minecraft.server.level.ServerLevel level) { ++ this.level = level; ++ } ++ // SparklyPaper end ++ + @Override + public void trackWaypoint(WaypointTransmitter waypoint) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, "Cannot track waypoints off-main"); // SparklyPaper - parallel world ticking + this.waypoints.add(waypoint); + + for (ServerPlayer serverPlayer : this.players) { +@@ -31,6 +39,7 @@ public class ServerWaypointManager implements WaypointManager map = Tables.transpose(this.connections).row(waypoint); + SetView set = Sets.difference(this.players, map.keySet()); +@@ -47,12 +56,14 @@ public class ServerWaypointManager implements WaypointManager connection.disconnect()); + Tables.transpose(this.connections).row(waypoint).clear(); + this.waypoints.remove(waypoint); + } + + public void addPlayer(ServerPlayer player) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, "Cannot add player to waypoints off-main"); // SparklyPaper - parallel world ticking + this.players.add(player); + + for (WaypointTransmitter waypointTransmitter : this.waypoints) { +@@ -65,6 +76,7 @@ public class ServerWaypointManager implements WaypointManager map = this.connections.row(player); + SetView set = Sets.difference(this.waypoints, map.keySet()); + +@@ -78,6 +90,7 @@ public class ServerWaypointManager implements WaypointManager { + connection.disconnect(); + return true; +@@ -87,6 +100,7 @@ public class ServerWaypointManager implements WaypointManager { +@@ -122,6 +137,7 @@ public class ServerWaypointManager implements WaypointManager passengers = this.getPassengers(); + List list = new ArrayList<>(passengers.size()); + this.ejectPassengers(); +diff --git a/net/minecraft/world/inventory/AbstractContainerMenu.java b/net/minecraft/world/inventory/AbstractContainerMenu.java +index 06846950348954328c07f64cd9b3359e79a1a468..6fc0fd000c0fd41a8253c6829b2aa38e374b58d7 100644 +--- a/net/minecraft/world/inventory/AbstractContainerMenu.java ++++ b/net/minecraft/world/inventory/AbstractContainerMenu.java +@@ -95,8 +95,14 @@ public abstract class AbstractContainerMenu { + + public void startOpen() {} + // CraftBukkit end ++ public Throwable containerCreationStacktrace; // SparklyPaper - parallel world ticking (debugging) + + protected AbstractContainerMenu(@Nullable MenuType menuType, int containerId) { ++ // SparklyPaper - parallel world ticking (debugging) ++ if (net.sparklypower.sparklypaper.configs.SparklyPaperConfigUtils.INSTANCE.getLogContainerCreationStacktraces()) { ++ this.containerCreationStacktrace = new Throwable(); ++ } ++ // SparklyPaper end + this.menuType = menuType; + this.containerId = containerId; + } +diff --git a/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java +index 25bc90eb527547487fb5191dc135f2c6030f3a19..5b3dbf15347af18065f1758c2bf06aee8af90abf 100644 +--- a/net/minecraft/world/item/ItemStack.java ++++ b/net/minecraft/world/item/ItemStack.java +@@ -399,8 +399,8 @@ public final class ItemStack implements DataComponentHolder { + if (interactionResult.consumesAction() && serverLevel.captureTreeGeneration && !serverLevel.capturedBlockStates.isEmpty()) { + serverLevel.captureTreeGeneration = false; + org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(clickedPos, serverLevel); +- org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType; +- net.minecraft.world.level.block.SaplingBlock.treeType = null; ++ org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeTypeRT.get(); // SparklyPaper - parallel world ticking ++ net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(null); + List blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values()); + serverLevel.capturedBlockStates.clear(); + org.bukkit.event.world.StructureGrowEvent structureEvent = null; +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index 133e60d0457b3067b5cf1f9c8374b67172b0596c..55a8da685402046183d950eb6266dc48e05e5eec 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -166,6 +166,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + } + // Paper end - add paper world config + ++ public io.papermc.paper.redstone.RedstoneWireTurbo turbo = new io.papermc.paper.redstone.RedstoneWireTurbo((net.minecraft.world.level.block.RedStoneWireBlock) net.minecraft.world.level.block.Blocks.REDSTONE_WIRE); // SparklyPaper - parallel world ticking (moved to world) + public static @Nullable BlockPos lastPhysicsProblem; // Spigot + private int tileTickPosition; + public final Map explosionDensityCache = new java.util.HashMap<>(); // Paper - Optimize explosions +@@ -1038,6 +1039,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + @Override + public boolean setBlock(BlockPos pos, BlockState state, int flags, int recursionLeft) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, pos, "Updating block asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + // CraftBukkit start - tree generation + if (this.captureTreeGeneration) { + // Paper start - Protect Bedrock and End Portal/Frames from being destroyed +@@ -1417,7 +1419,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + tickingBlockEntity.tick(); + // Paper start - rewrite chunk system + if ((++tickedEntities & 7) == 0) { +- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)(Level)(Object)this).moonrise$midTickTasks(); ++ // ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)(Level)(Object)this).moonrise$midTickTasks(); // SparklyPaper - parallel world ticking (only run mid-tick at the end of each tick / fixes concurrency bugs related to executeMidTickTasks) + } + // Paper end - rewrite chunk system + } +@@ -1439,7 +1441,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); + // Paper end - Prevent block entity and entity crashes + } +- this.moonrise$midTickTasks(); // Paper - rewrite chunk system ++ // this.moonrise$midTickTasks(); // Paper - rewrite chunk system // SparklyPaper - parallel world ticking (only run mid-tick at the end of each tick / fixes concurrency bugs related to executeMidTickTasks) + } + + // Paper start - Option to prevent armor stands from doing entity lookups +@@ -1581,6 +1583,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + @Nullable + @Override + public BlockEntity getBlockEntity(BlockPos pos) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThreadOrAsyncThread((ServerLevel) this, "Cannot read world asynchronously"); // SparklyPaper - parallel world ticking + // Paper start - Perf: Optimize capturedTileEntities lookup + net.minecraft.world.level.block.entity.BlockEntity blockEntity; + if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(pos)) != null) { +@@ -1597,6 +1600,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + } + + public void setBlockEntity(BlockEntity blockEntity) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel) this, "Cannot modify world asynchronously"); // SparklyPaper - parallel world ticking + BlockPos blockPos = blockEntity.getBlockPos(); + if (!this.isOutsideBuildHeight(blockPos)) { + // CraftBukkit start +@@ -1682,6 +1686,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + + @Override + public List getEntities(@Nullable Entity entity, AABB boundingBox, Predicate predicate) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, boundingBox, "Cannot getEntities asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + Profiler.get().incrementCounter("getEntities"); + List list = Lists.newArrayList(); + +@@ -2035,8 +2040,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + public abstract RecipeAccess recipeAccess(); + + public BlockPos getBlockRandomPos(int x, int y, int z, int yMask) { +- this.randValue = this.randValue * 3 + 1013904223; +- int i = this.randValue >> 2; ++ int i = this.random.nextInt() >> 2; // SparklyPaper - parallel world ticking + return new BlockPos(x + (i & 15), y + (i >> 16 & yMask), z + (i >> 8 & 15)); + } + +diff --git a/net/minecraft/world/level/block/FungusBlock.java b/net/minecraft/world/level/block/FungusBlock.java +index 9711efb088bd0da9168e9bcd0496bd7caddd2974..63da283d68c7e1184d1995aeb08cd8720f1b02ee 100644 +--- a/net/minecraft/world/level/block/FungusBlock.java ++++ b/net/minecraft/world/level/block/FungusBlock.java +@@ -76,9 +76,9 @@ public class FungusBlock extends VegetationBlock implements BonemealableBlock { + // CraftBukkit start + .map((value) -> { + if (this == Blocks.WARPED_FUNGUS) { +- SaplingBlock.treeType = org.bukkit.TreeType.WARPED_FUNGUS; ++ SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.WARPED_FUNGUS); // SparklyPaper - parallel world ticking + } else if (this == Blocks.CRIMSON_FUNGUS) { +- SaplingBlock.treeType = org.bukkit.TreeType.CRIMSON_FUNGUS; ++ SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.CRIMSON_FUNGUS); // SparklyPaper - parallel world ticking + } + return value; + }) +diff --git a/net/minecraft/world/level/block/MushroomBlock.java b/net/minecraft/world/level/block/MushroomBlock.java +index d306f5f524dc64618df94c9783c2168dc561a5e3..72363832a60a380869b60b1ec4a352a1e11608a8 100644 +--- a/net/minecraft/world/level/block/MushroomBlock.java ++++ b/net/minecraft/world/level/block/MushroomBlock.java +@@ -93,7 +93,7 @@ public class MushroomBlock extends VegetationBlock implements BonemealableBlock + return false; + } else { + level.removeBlock(pos, false); +- SaplingBlock.treeType = (this == Blocks.BROWN_MUSHROOM) ? org.bukkit.TreeType.BROWN_MUSHROOM : org.bukkit.TreeType.RED_MUSHROOM; // CraftBukkit ++ SaplingBlock.treeTypeRT.set((this == Blocks.BROWN_MUSHROOM) ? org.bukkit.TreeType.BROWN_MUSHROOM : org.bukkit.TreeType.RED_MUSHROOM); // CraftBukkit // SparklyPaper - parallel world ticking + if (optional.get().value().place(level, level.getChunkSource().getGenerator(), random, pos)) { + return true; + } else { +diff --git a/net/minecraft/world/level/block/RedStoneWireBlock.java b/net/minecraft/world/level/block/RedStoneWireBlock.java +index 7d7e7a87f5c9bbd9535bf2105e05d7abf08fc3dc..293877b519a8f2ac63a02823e92ba5c9d0468d30 100644 +--- a/net/minecraft/world/level/block/RedStoneWireBlock.java ++++ b/net/minecraft/world/level/block/RedStoneWireBlock.java +@@ -267,7 +267,7 @@ public class RedStoneWireBlock extends Block { + + // Paper start - Optimize redstone (Eigencraft) + // The bulk of the new functionality is found in RedstoneWireTurbo.java +- io.papermc.paper.redstone.RedstoneWireTurbo turbo = new io.papermc.paper.redstone.RedstoneWireTurbo(this); ++ // io.papermc.paper.redstone.RedstoneWireTurbo turbo = new io.papermc.paper.redstone.RedstoneWireTurbo(this); // SparklyPaper - parallel world ticking (moved to world) + + /* + * Modified version of pre-existing updateSurroundingRedstone, which is called from +@@ -283,7 +283,7 @@ public class RedStoneWireBlock extends Block { + if (orientation != null) { + source = pos.relative(orientation.getFront().getOpposite()); + } +- turbo.updateSurroundingRedstone(worldIn, pos, state, source); ++ worldIn.turbo.updateSurroundingRedstone(worldIn, pos, state, source); // SparklyPaper - parallel world ticking + return; + } + updatePowerStrength(worldIn, pos, state, orientation, blockAdded); +@@ -311,7 +311,7 @@ public class RedStoneWireBlock extends Block { + // [Space Walker] suppress shape updates and emit those manually to + // bypass the new neighbor update stack. + if (level.setBlock(pos, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) { +- turbo.updateNeighborShapes(level, pos, state); ++ level.turbo.updateNeighborShapes(level, pos, state); // SparklyPaper - parallel world ticking + } + } + } +diff --git a/net/minecraft/world/level/block/SaplingBlock.java b/net/minecraft/world/level/block/SaplingBlock.java +index a5e4959c1b5133cfaeb9259d7e59b38a06453785..aa5e8d8ef47f7792b7204423f60ef4638c6d9821 100644 +--- a/net/minecraft/world/level/block/SaplingBlock.java ++++ b/net/minecraft/world/level/block/SaplingBlock.java +@@ -25,7 +25,7 @@ public class SaplingBlock extends VegetationBlock implements BonemealableBlock { + public static final IntegerProperty STAGE = BlockStateProperties.STAGE; + private static final VoxelShape SHAPE = Block.column(12.0, 0.0, 12.0); + protected final TreeGrower treeGrower; +- public static @javax.annotation.Nullable org.bukkit.TreeType treeType; // CraftBukkit ++ public static final ThreadLocal treeTypeRT = new ThreadLocal<>(); // SparklyPaper - parallel world ticking (from Folia) + + @Override + public MapCodec codec() { +@@ -62,8 +62,8 @@ public class SaplingBlock extends VegetationBlock implements BonemealableBlock { + this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random); + level.captureTreeGeneration = false; + if (!level.capturedBlockStates.isEmpty()) { +- org.bukkit.TreeType treeType = SaplingBlock.treeType; +- SaplingBlock.treeType = null; ++ org.bukkit.TreeType treeType = SaplingBlock.treeTypeRT.get(); // SparklyPaper - parallel world ticking ++ SaplingBlock.treeTypeRT.set(null); + org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(pos, level); + java.util.List blocks = new java.util.ArrayList<>(level.capturedBlockStates.values()); + level.capturedBlockStates.clear(); +diff --git a/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java b/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java +index 1b2f8c4e1e362dc63fde2c7139039f0ce7eb762f..2ca4a34b3d9341f087fba031930ffd42a21c1db0 100644 +--- a/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java +@@ -76,6 +76,12 @@ public abstract class BaseContainerBlockEntity extends BlockEntity implements Co + } + + public static boolean canUnlock(Player player, LockCode code, Component displayName, @Nullable BlockEntity blockEntity) { ++ // SparklyPaper - parallel world ticking (see: PARALLEL_NOTES.md - Opening an inventory after a world switch) ++ if (player instanceof net.minecraft.server.level.ServerPlayer serverPlayer && blockEntity != null && blockEntity.getLevel() != serverPlayer.level()) { ++ net.minecraft.server.MinecraftServer.LOGGER.warn("Player " + serverPlayer.getScoreboardName() + " (" + serverPlayer.getStringUUID() + ") attempted to open a BlockEntity @ " + blockEntity.getLevel().getWorld().getName() + " " + blockEntity.getBlockPos().getX() + ", " + blockEntity.getBlockPos().getY() + ", " + blockEntity.getBlockPos().getZ() + " while they were in a different world " + serverPlayer.level().getWorld().getName() + " than the block themselves!"); ++ return false; ++ } ++ // SparklyPaper end + if (player instanceof net.minecraft.server.level.ServerPlayer serverPlayer && blockEntity != null && blockEntity.getLevel() != null && blockEntity.getLevel().getBlockEntity(blockEntity.getBlockPos()) == blockEntity) { + final org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(blockEntity.getLevel(), blockEntity.getBlockPos()); + net.kyori.adventure.text.Component lockedMessage = net.kyori.adventure.text.Component.translatable("container.isLocked", io.papermc.paper.adventure.PaperAdventure.asAdventure(displayName)); +diff --git a/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java b/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +index 0a94670dc20bb9c521b0395633eb100393895f6a..4ad9c47862a9791f72a18835a343bf0e962c14c8 100644 +--- a/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +@@ -43,9 +43,7 @@ public class SculkCatalystBlockEntity extends BlockEntity implements GameEventLi + // Paper end - Fix NPE in SculkBloomEvent world access + + public static void serverTick(Level level, BlockPos pos, BlockState state, SculkCatalystBlockEntity sculkCatalyst) { +- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = sculkCatalyst.getBlockPos(); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. + sculkCatalyst.catalystListener.getSculkSpreader().updateCursors(level, pos, level.getRandom(), true); +- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = null; // CraftBukkit + } + + @Override +diff --git a/net/minecraft/world/level/block/grower/TreeGrower.java b/net/minecraft/world/level/block/grower/TreeGrower.java +index d23f255de9208f42125fa358a9e8194c984fe4d3..54d64e77dc8c4bef25822d74a30be1a860cfec36 100644 +--- a/net/minecraft/world/level/block/grower/TreeGrower.java ++++ b/net/minecraft/world/level/block/grower/TreeGrower.java +@@ -203,55 +203,57 @@ public final class TreeGrower { + + // CraftBukkit start + private void setTreeType(Holder> feature) { ++ org.bukkit.TreeType treeType; // SparklyPaper - parallel world ticking + if (feature.is(TreeFeatures.OAK) || feature.is(TreeFeatures.OAK_BEES_005)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TREE; ++ treeType = org.bukkit.TreeType.TREE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.HUGE_RED_MUSHROOM)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.RED_MUSHROOM; ++ treeType = org.bukkit.TreeType.RED_MUSHROOM; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.HUGE_BROWN_MUSHROOM)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BROWN_MUSHROOM; ++ treeType = org.bukkit.TreeType.BROWN_MUSHROOM; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.JUNGLE_TREE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.COCOA_TREE; ++ treeType = org.bukkit.TreeType.COCOA_TREE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.JUNGLE_TREE_NO_VINE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SMALL_JUNGLE; ++ treeType = org.bukkit.TreeType.SMALL_JUNGLE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.PINE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_REDWOOD; ++ treeType = org.bukkit.TreeType.TALL_REDWOOD; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.SPRUCE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.REDWOOD; ++ treeType = org.bukkit.TreeType.REDWOOD; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.ACACIA)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.ACACIA; ++ treeType = org.bukkit.TreeType.ACACIA; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.BIRCH) || feature.is(TreeFeatures.BIRCH_BEES_005)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIRCH; ++ treeType = org.bukkit.TreeType.BIRCH; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.SUPER_BIRCH_BEES_0002)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_BIRCH; ++ treeType = org.bukkit.TreeType.TALL_BIRCH; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.SWAMP_OAK)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SWAMP; ++ treeType = org.bukkit.TreeType.SWAMP; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.FANCY_OAK) || feature.is(TreeFeatures.FANCY_OAK_BEES_005)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIG_TREE; ++ treeType = org.bukkit.TreeType.BIG_TREE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.JUNGLE_BUSH)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE_BUSH; ++ treeType = org.bukkit.TreeType.JUNGLE_BUSH; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.DARK_OAK)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.DARK_OAK; ++ treeType = org.bukkit.TreeType.DARK_OAK; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.MEGA_SPRUCE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_REDWOOD; ++ treeType = org.bukkit.TreeType.MEGA_REDWOOD; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.MEGA_PINE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_PINE; ++ treeType = org.bukkit.TreeType.MEGA_PINE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.MEGA_JUNGLE_TREE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE; ++ treeType = org.bukkit.TreeType.JUNGLE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.AZALEA_TREE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.AZALEA; ++ treeType = org.bukkit.TreeType.AZALEA; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.MANGROVE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MANGROVE; ++ treeType = org.bukkit.TreeType.MANGROVE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.TALL_MANGROVE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_MANGROVE; ++ treeType = org.bukkit.TreeType.TALL_MANGROVE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.CHERRY) || feature.is(TreeFeatures.CHERRY_BEES_005)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.CHERRY; ++ treeType = org.bukkit.TreeType.CHERRY; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.PALE_OAK) || feature.is(TreeFeatures.PALE_OAK_BONEMEAL)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK; ++ treeType = org.bukkit.TreeType.PALE_OAK; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.PALE_OAK_CREAKING)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK_CREAKING; ++ treeType = org.bukkit.TreeType.PALE_OAK_CREAKING; // SparklyPaper - parallel world ticking + } else { + throw new IllegalArgumentException("Unknown tree generator " + feature); + } ++ net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(treeType); // SparklyPaper - parallel world ticking + } + // CraftBukkit end + } +diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java +index 4dcec2e8a3120a3dfa078e8cf6857ba99ca01a2d..48f92923508802707968f9086a3bf38f0fc9a302 100644 +--- a/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/net/minecraft/world/level/chunk/LevelChunk.java +@@ -367,6 +367,7 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + @Nullable + @Override + public BlockState setBlockState(BlockPos pos, BlockState state, int flags) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, pos, "Updating block asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + int y = pos.getY(); + LevelChunkSection section = this.getSection(this.getSectionIndex(y)); + boolean hasOnlyAir = section.hasOnlyAir(); +diff --git a/net/minecraft/world/level/entity/EntityTickList.java b/net/minecraft/world/level/entity/EntityTickList.java +index 423779a2b690f387a4f0bd07b97b50e0baefda76..24f9632e73d73c2ad68ebf30eb0e4cca7befae2d 100644 +--- a/net/minecraft/world/level/entity/EntityTickList.java ++++ b/net/minecraft/world/level/entity/EntityTickList.java +@@ -10,17 +10,26 @@ import net.minecraft.world.entity.Entity; + + public class EntityTickList { + private final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet entities = new ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<>(); // Paper - rewrite chunk system ++ // SparklyPaper start - parallel world ticking ++ // Used to track async entity additions/removals/loops ++ private final net.minecraft.server.level.ServerLevel serverLevel; ++ public EntityTickList(net.minecraft.server.level.ServerLevel serverLevel) { ++ this.serverLevel = serverLevel; ++ } ++ // SparklyPaper end + + private void ensureActiveIsNotIterated() { + // Paper - rewrite chunk system + } + + public void add(Entity entity) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(entity, "Asynchronous entity ticklist addition"); // Paper // SparklyPaper - parallel world ticking (additional concurrency issues logs) + this.ensureActiveIsNotIterated(); + this.entities.add(entity); // Paper - rewrite chunk system + } + + public void remove(Entity entity) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(entity, "Asynchronous entity ticklist removal"); // Paper // SparklyPaper - parallel world ticking (additional concurrency issues logs) + this.ensureActiveIsNotIterated(); + this.entities.remove(entity); // Paper - rewrite chunk system + } +@@ -30,6 +39,7 @@ public class EntityTickList { + } + + public void forEach(Consumer entity) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverLevel, "Asynchronous entity ticklist iteration"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + // Paper start - rewrite chunk system + // To ensure nothing weird happens with dimension travelling, do not iterate over new entries... + // (by dfl iterator() is configured to not iterate over new entries) +diff --git a/net/minecraft/world/level/saveddata/maps/MapIndex.java b/net/minecraft/world/level/saveddata/maps/MapIndex.java +index 06025d79cc2297119b22224d777aca79f9d3d9c1..c4c6e28b58ec921ebc18b427e562fbaa51da65c8 100644 +--- a/net/minecraft/world/level/saveddata/maps/MapIndex.java ++++ b/net/minecraft/world/level/saveddata/maps/MapIndex.java +@@ -23,8 +23,10 @@ public class MapIndex extends SavedData { + } + + public MapId getNextMapId() { ++ synchronized (TYPE) { // SparklyPaper start - parallel world ticking + MapId mapId = new MapId(++this.lastMapId); + this.setDirty(); + return mapId; ++ } // SparklyPaper end + } + } diff --git a/sparklypaper-server/paper-patches/features/0001-Parallel-World-Ticking.patch b/sparklypaper-server/paper-patches/features/0001-Parallel-World-Ticking.patch new file mode 100644 index 0000000..cac91b8 --- /dev/null +++ b/sparklypaper-server/paper-patches/features/0001-Parallel-World-Ticking.patch @@ -0,0 +1,649 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrPowerGamerBR +Date: Thu, 25 Sep 2025 21:20:48 -0300 +Subject: [PATCH] Parallel World Ticking + + +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java +index 69cdd304d255d52c9b7dc9b6a33ffdb630b79abe..636ec7172e9b535f7b4f95b4a97432178aa6711c 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java +@@ -14,6 +14,7 @@ import java.util.concurrent.atomic.AtomicInteger; + public class TickThread extends Thread { + + private static final Logger LOGGER = LoggerFactory.getLogger(TickThread.class); ++ public static final boolean HARD_THROW = !Boolean.getBoolean("sparklypaper.disableHardThrow"); // SparklyPaper - parallel world ticking - THIS SHOULD NOT BE DISABLED SINCE IT CAN CAUSE DATA CORRUPTION!!! Anyhow, for production servers, if you want to make a test run to see if the server could crash, you can test it with this disabled + + private static String getThreadContext() { + return "thread=" + Thread.currentThread().getName(); +@@ -26,6 +27,7 @@ public class TickThread extends Thread { + public static void ensureTickThread(final String reason) { + if (!isTickThread()) { + LOGGER.error("Thread failed main thread check: " + reason + ", context=" + getThreadContext(), new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(reason); + } + } +@@ -33,8 +35,9 @@ public class TickThread extends Thread { + public static void ensureTickThread(final Level world, final BlockPos pos, final String reason) { + if (!isTickThreadFor(world, pos)) { + final String ex = "Thread failed main thread check: " + +- reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + pos; ++ reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + pos + " - " + getTickThreadInformation(world.getServer()); + LOGGER.error(ex, new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(ex); + } + } +@@ -42,8 +45,9 @@ public class TickThread extends Thread { + public static void ensureTickThread(final Level world, final BlockPos pos, final int blockRadius, final String reason) { + if (!isTickThreadFor(world, pos, blockRadius)) { + final String ex = "Thread failed main thread check: " + +- reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + pos + ", block_radius=" + blockRadius; ++ reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + pos + ", block_radius=" + blockRadius + " - " + getTickThreadInformation(world.getServer()); + LOGGER.error(ex, new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(ex); + } + } +@@ -51,8 +55,9 @@ public class TickThread extends Thread { + public static void ensureTickThread(final Level world, final ChunkPos pos, final String reason) { + if (!isTickThreadFor(world, pos)) { + final String ex = "Thread failed main thread check: " + +- reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", chunk_pos=" + pos; ++ reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", chunk_pos=" + pos + " - " + getTickThreadInformation(world.getServer()); + LOGGER.error(ex, new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(ex); + } + } +@@ -60,8 +65,9 @@ public class TickThread extends Thread { + public static void ensureTickThread(final Level world, final int chunkX, final int chunkZ, final String reason) { + if (!isTickThreadFor(world, chunkX, chunkZ)) { + final String ex = "Thread failed main thread check: " + +- reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", chunk_pos=" + new ChunkPos(chunkX, chunkZ); ++ reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", chunk_pos=" + new ChunkPos(chunkX, chunkZ) + " - " + getTickThreadInformation(world.getServer()); + LOGGER.error(ex, new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(ex); + } + } +@@ -69,8 +75,9 @@ public class TickThread extends Thread { + public static void ensureTickThread(final Entity entity, final String reason) { + if (!isTickThreadFor(entity)) { + final String ex = "Thread failed main thread check: " + +- reason + ", context=" + getThreadContext() + ", entity=" + EntityUtil.dumpEntity(entity); ++ reason + ", context=" + getThreadContext() + ", entity=" + EntityUtil.dumpEntity(entity) + " - " + getTickThreadInformation(entity.level().getServer()); + LOGGER.error(ex, new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(ex); + } + } +@@ -78,8 +85,9 @@ public class TickThread extends Thread { + public static void ensureTickThread(final Level world, final AABB aabb, final String reason) { + if (!isTickThreadFor(world, aabb)) { + final String ex = "Thread failed main thread check: " + +- reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", aabb=" + aabb; ++ reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", aabb=" + aabb + " - " + getTickThreadInformation(world.getServer()); + LOGGER.error(ex, new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(ex); + } + } +@@ -87,12 +95,75 @@ public class TickThread extends Thread { + public static void ensureTickThread(final Level world, final double blockX, final double blockZ, final String reason) { + if (!isTickThreadFor(world, blockX, blockZ)) { + final String ex = "Thread failed main thread check: " + +- reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + new Vec3(blockX, 0.0, blockZ); ++ reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + new Vec3(blockX, 0.0, blockZ) + " - " + getTickThreadInformation(world.getServer()); + LOGGER.error(ex, new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(ex); + } + } + ++ // SparklyPaper - parallel world ticking ++ // This is an additional method to check if the tick thread is bound to a specific world because, by default, Paper's isTickThread methods do not provide this information ++ // Because we only tick worlds in parallel (instead of regions), we can use this for our checks ++ public static void ensureTickThread(final net.minecraft.server.level.ServerLevel world, final String reason) { ++ if (!isTickThreadFor(world)) { ++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason + " @ world " + world.getWorld().getName() + " - " + getTickThreadInformation(world.getServer()), new Throwable()); ++ if (HARD_THROW) ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ // SparklyPaper - parallel world ticking ++ // This is an additional method to check if it is a tick thread but ONLY a tick thread ++ public static void ensureOnlyTickThread(final String reason) { ++ boolean isTickThread = isTickThread(); ++ boolean isServerLevelTickThread = isServerLevelTickThread(); ++ if (!isTickThread || isServerLevelTickThread) { ++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread ONLY tick thread check: " + reason, new Throwable()); ++ if (HARD_THROW) ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ // SparklyPaper - parallel world ticking ++ // This is an additional method to check if the tick thread is bound to a specific world or if it is an async thread. ++ public static void ensureTickThreadOrAsyncThread(final net.minecraft.server.level.ServerLevel world, final String reason) { ++ boolean isValidTickThread = isTickThreadFor(world); ++ boolean isAsyncThread = !isTickThread(); ++ boolean isValid = isAsyncThread || isValidTickThread; ++ if (!isValid) { ++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread or async thread check: " + reason + " @ world " + world.getWorld().getName() + " - " + getTickThreadInformation(world.getServer()), new Throwable()); ++ if (HARD_THROW) ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ public static String getTickThreadInformation(net.minecraft.server.MinecraftServer minecraftServer) { ++ StringBuilder sb = new StringBuilder(); ++ Thread currentThread = Thread.currentThread(); ++ sb.append("Is tick thread? "); ++ sb.append(currentThread instanceof TickThread); ++ sb.append("; Is server level tick thread? "); ++ sb.append(currentThread instanceof ServerLevelTickThread); ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ sb.append("; Currently ticking level: "); ++ if (serverLevelTickThread.currentlyTickingServerLevel != null) { ++ sb.append(serverLevelTickThread.currentlyTickingServerLevel.getWorld().getName()); ++ } else { ++ sb.append("null"); ++ } ++ } ++ sb.append("; Is iterating over levels? "); ++ sb.append(minecraftServer.isIteratingOverLevels); ++ sb.append("; Are we going to hard throw? "); ++ sb.append(HARD_THROW); ++ return sb.toString(); ++ } ++ ++ public static boolean isServerLevelTickThread() { ++ return Thread.currentThread() instanceof ServerLevelTickThread; ++ } ++ + public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */ + + private static final AtomicInteger ID_GENERATOR = new AtomicInteger(); +@@ -127,7 +198,11 @@ public class TickThread extends Thread { + } + + public static boolean isTickThreadFor(final Level world, final BlockPos pos) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final BlockPos pos, final int blockRadius) { +@@ -135,38 +210,103 @@ public class TickThread extends Thread { + } + + public static boolean isTickThreadFor(final Level world, final ChunkPos pos) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final Vec3 pos) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final AABB aabb) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final double blockX, final double blockZ) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final Vec3 position, final Vec3 deltaMovement, final int buffer) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ, final int radius) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; ++ } ++ ++ // SparklyPaper - parallel world ticking ++ // This is an additional method to check if the tick thread is bound to a specific world because, by default, Paper's isTickThread methods do not provide this information ++ // Because we only tick worlds in parallel (instead of regions), we can use this for our checks ++ public static boolean isTickThreadFor(final Level world) { ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Entity entity) { +- return isTickThread(); ++ if (entity == null) { ++ return true; ++ } ++ ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == entity.level(); ++ } else return currentThread instanceof TickThread; ++ } ++ ++ // SparklyPaper start - parallel world ticking ++ public static class ServerLevelTickThread extends TickThread { ++ public ServerLevelTickThread(String name) { ++ super(name); ++ } ++ ++ public ServerLevelTickThread(Runnable run, String name) { ++ super(run, name); ++ } ++ ++ public net.minecraft.server.level.ServerLevel currentlyTickingServerLevel; + } ++ // SparklyPaper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 48f6f41d59983e52b9db41a7555423d6e9a416d4..c85d682b85830f1c70d0bba192f8a0275c5dba49 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -472,6 +472,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + private boolean unloadChunk0(int x, int z, boolean save) { + org.spigotmc.AsyncCatcher.catchOp("chunk unload"); // Spigot ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot unload chunk asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + if (!this.isChunkLoaded(x, z)) { + return true; + } +@@ -488,6 +489,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean refreshChunk(int x, int z) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot refresh chunk asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); + if (playerChunk == null) return false; + +@@ -538,6 +540,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean loadChunk(int x, int z, boolean generate) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.getHandle(), x, z, "May not sync load chunks asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot + warnUnsafeChunk("loading a faraway chunk", x, z); // Paper + ChunkAccess chunk = this.world.getChunkSource().getChunk(x, z, generate || isChunkGenerated(x, z) ? ChunkStatus.FULL : ChunkStatus.EMPTY, true); // Paper +@@ -751,6 +754,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean generateTree(Location loc, TreeType type, BlockChangeDelegate delegate) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, loc.getX(), loc.getZ(), "Cannot generate tree asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + this.world.captureTreeGeneration = true; + this.world.captureBlockStates = true; + boolean grownTree = this.generateTree(loc, type); +@@ -852,6 +856,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + private boolean createExplosion(double x, double y, double z, float power, boolean setFire, boolean breakBlocks, Entity source, Consumer configurator) { + // Paper end - expand explosion API ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot create explosion asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + net.minecraft.world.level.Level.ExplosionInteraction explosionType; + if (!breakBlocks) { + explosionType = net.minecraft.world.level.Level.ExplosionInteraction.NONE; // Don't break blocks +@@ -908,6 +913,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public int getHighestBlockYAt(int x, int z, org.bukkit.HeightMap heightMap) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x >> 4, z >> 4, "Cannot retrieve chunk asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + warnUnsafeChunk("getting a faraway chunk", x >> 4, z >> 4); // Paper + // Transient load for this tick + return this.world.getChunk(x >> 4, z >> 4).getHeight(CraftHeightMap.toNMS(heightMap), x, z); +@@ -915,6 +921,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void setBiome(int x, int z, Biome bio) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, pos, "Cannot retrieve chunk asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + for (int y = this.getMinHeight(); y < this.getMaxHeight(); y++) { + this.setBiome(x, y, z, bio); + } +@@ -1902,6 +1909,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void sendGameEvent(Entity sourceEntity, org.bukkit.GameEvent gameEvent, Vector position) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, position.getX(), position.getZ(), "Cannot send game event asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + getHandle().gameEvent(sourceEntity != null ? ((CraftEntity) sourceEntity).getHandle(): null, net.minecraft.core.registries.BuiltInRegistries.GAME_EVENT.get(org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(gameEvent.getKey())).orElseThrow(), org.bukkit.craftbukkit.util.CraftVector.toBlockPos(position)); + } + // Paper end +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +index 6809f9dcf8510c714145d99d250eb69f98d9bf27..ee04049faf283ac929b6b0806eaed430a7c57db8 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +@@ -75,6 +75,11 @@ public class CraftBlock implements Block { + } + + public net.minecraft.world.level.block.state.BlockState getNMS() { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + return this.world.getBlockState(this.position); + } + +@@ -155,6 +160,11 @@ public class CraftBlock implements Block { + } + + private void setData(final byte data, int flags) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + this.world.setBlock(this.position, CraftMagicNumbers.getBlock(this.getType(), data), flags); + } + +@@ -196,6 +206,11 @@ public class CraftBlock implements Block { + } + + public static boolean setBlockState(LevelAccessor world, BlockPos pos, net.minecraft.world.level.block.state.BlockState oldState, net.minecraft.world.level.block.state.BlockState newState, boolean applyPhysics) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, pos, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + // SPIGOT-611: need to do this to prevent glitchiness. Easier to handle this here (like /setblock) than to fix weirdness in block entity cleanup + if (oldState.hasBlockEntity() && newState.getBlock() != oldState.getBlock()) { // SPIGOT-3725 remove old block entity if block changes + // SPIGOT-4612: faster - just clear tile +@@ -344,18 +359,33 @@ public class CraftBlock implements Block { + + @Override + public Biome getBiome() { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + return this.getWorld().getBiome(this.getX(), this.getY(), this.getZ()); + } + + // Paper start + @Override + public Biome getComputedBiome() { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + return this.getWorld().getComputedBiome(this.getX(), this.getY(), this.getZ()); + } + // Paper end + + @Override + public void setBiome(Biome bio) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + this.getWorld().setBiome(this.getX(), this.getY(), this.getZ(), bio); + } + +@@ -376,6 +406,11 @@ public class CraftBlock implements Block { + + @Override + public boolean isBlockIndirectlyPowered() { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + return this.world.getMinecraftWorld().hasNeighborSignal(this.position); + } + +@@ -415,6 +450,11 @@ public class CraftBlock implements Block { + + @Override + public int getBlockPower(BlockFace face) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + int power = 0; + net.minecraft.world.level.Level world = this.world.getMinecraftWorld(); + int x = this.getX(); +@@ -504,6 +544,11 @@ public class CraftBlock implements Block { + + @Override + public boolean breakNaturally(ItemStack item, boolean triggerEffect, boolean dropExperience, boolean forceEffect) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + // Paper end + // Order matters here, need to drop before setting to air so skulls can get their data + net.minecraft.world.level.block.state.BlockState state = this.getNMS(); +@@ -548,6 +593,11 @@ public class CraftBlock implements Block { + + @Override + public boolean applyBoneMeal(BlockFace face) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + Direction direction = CraftBlock.blockFaceToNotch(face); + BlockFertilizeEvent event = null; + ServerLevel world = this.getCraftWorld().getHandle(); +@@ -559,8 +609,8 @@ public class CraftBlock implements Block { + world.captureTreeGeneration = false; + + if (!world.capturedBlockStates.isEmpty()) { +- TreeType treeType = SaplingBlock.treeType; +- SaplingBlock.treeType = null; ++ TreeType treeType = SaplingBlock.treeTypeRT.get(); // SparklyPaper - parallel world ticking ++ SaplingBlock.treeTypeRT.set(null);// SparklyPaper - parallel world ticking + List states = new ArrayList<>(world.capturedBlockStates.values()); + world.capturedBlockStates.clear(); + StructureGrowEvent structureEvent = null; +@@ -599,6 +649,11 @@ public class CraftBlock implements Block { + @Override + public Collection getDrops(ItemStack item, Entity entity) { + net.minecraft.world.level.block.state.BlockState state = this.getNMS(); ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + net.minecraft.world.item.ItemStack nms = CraftItemStack.asNMSCopy(item); + + // Modelled off Player#hasCorrectToolForDrops +@@ -650,6 +705,11 @@ public class CraftBlock implements Block { + + @Override + public RayTraceResult rayTrace(Location start, Vector direction, double maxDistance, FluidCollisionMode fluidCollisionMode) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + Preconditions.checkArgument(start != null, "Location start cannot be null"); + Preconditions.checkArgument(this.getWorld().equals(start.getWorld()), "Location start cannot be a different world"); + start.checkFinite(); +@@ -691,6 +751,11 @@ public class CraftBlock implements Block { + + @Override + public boolean canPlace(BlockData data) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + Preconditions.checkArgument(data != null, "BlockData cannot be null"); + net.minecraft.world.level.block.state.BlockState iblockdata = ((CraftBlockData) data).getState(); + net.minecraft.world.level.Level world = this.world.getMinecraftWorld(); +@@ -730,6 +795,11 @@ public class CraftBlock implements Block { + + @Override + public void tick() { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + final ServerLevel level = this.world.getMinecraftWorld(); + this.getNMS().tick(level, this.position, level.random); + } +@@ -737,6 +807,11 @@ public class CraftBlock implements Block { + + @Override + public void fluidTick() { ++ // SparklyPaper start - parallel world ticking ++ if (this.world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + this.getNMSFluid().tick(this.world.getMinecraftWorld(), this.position, this.getNMS()); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java +index 5d4faad9df4824cfd61abfd4df011c006f114424..d1227c670db2dbe1816d78be51796c43e99a4d9b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java +@@ -32,7 +32,7 @@ public abstract class CraftBlockEntityState extends Craft + private final T blockEntity; + private final T snapshot; + public boolean snapshotDisabled; // Paper +- public static boolean DISABLE_SNAPSHOT = false; // Paper ++ public static ThreadLocal DISABLE_SNAPSHOT = ThreadLocal.withInitial(() -> Boolean.FALSE); // SparklyPaper - parallel world ticking + + public CraftBlockEntityState(World world, T blockEntity) { + super(world, blockEntity.getBlockPos(), blockEntity.getBlockState()); +@@ -41,8 +41,8 @@ public abstract class CraftBlockEntityState extends Craft + + try { // Paper - Show blockstate location if we failed to read it + // Paper start +- this.snapshotDisabled = DISABLE_SNAPSHOT; +- if (DISABLE_SNAPSHOT) { ++ this.snapshotDisabled = DISABLE_SNAPSHOT.get(); // SparklyPaper - parallel world ticking ++ if (DISABLE_SNAPSHOT.get()) { // SparklyPaper - parallel world ticking + this.snapshot = this.blockEntity; + } else { + this.snapshot = this.createSnapshot(blockEntity); +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java +index 196835bdf95ba0e149b2977e9ef41698971f501f..641adf9666fef4d15bc9b585aabfe687a83fbe86 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java +@@ -218,6 +218,12 @@ public class CraftBlockState implements BlockState { + LevelAccessor access = this.getWorldHandle(); + CraftBlock block = this.getBlock(); + ++ // SparklyPaper start - parallel world ticking ++ if (access instanceof net.minecraft.server.level.ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking ++ + if (block.getType() != this.getType()) { + if (!force) { + return false; +@@ -365,6 +371,7 @@ public class CraftBlockState implements BlockState { + + @Override + public java.util.Collection getDrops(org.bukkit.inventory.ItemStack item, org.bukkit.entity.Entity entity) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(world.getHandle(), position, "Cannot modify world asynchronously"); // SparklyPaper - parallel world ticking + this.requirePlaced(); + net.minecraft.world.item.ItemStack nms = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(item); + +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java +index bcd73c788768f9a278181926fb56df510d271a76..ab2cec18178d917bb9253facd0312917b4519c59 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java +@@ -196,14 +196,14 @@ public final class CraftBlockStates { + BlockPos pos = craftBlock.getPosition(); + net.minecraft.world.level.block.state.BlockState state = craftBlock.getNMS(); + BlockEntity blockEntity = craftBlock.getHandle().getBlockEntity(pos); +- boolean prev = CraftBlockEntityState.DISABLE_SNAPSHOT; +- CraftBlockEntityState.DISABLE_SNAPSHOT = !useSnapshot; ++ boolean prev = CraftBlockEntityState.DISABLE_SNAPSHOT.get(); // SparklyPaper - parallel world ticking ++ CraftBlockEntityState.DISABLE_SNAPSHOT.set(!useSnapshot); // SparklyPaper - parallel world ticking + try { + CraftBlockState blockState = CraftBlockStates.getBlockState(world, pos, state, blockEntity); + blockState.setWorldHandle(craftBlock.getHandle()); // Inject the block's generator access + return blockState; + } finally { +- CraftBlockEntityState.DISABLE_SNAPSHOT = prev; ++ CraftBlockEntityState.DISABLE_SNAPSHOT.set(prev); // SparklyPaper - parallel world ticking + } + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index 9ccaf437ce8a779074f5abaf69fa6dc653d0064e..e25d72905347abfb6efce7f0a656970ccb30c3ad 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -810,7 +810,7 @@ public class CraftEventFactory { + return false; + } + +- public static BlockPos sourceBlockOverride = null; // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPos up to five methods deep. ++ public static final ThreadLocal sourceBlockOverrideRT = new ThreadLocal<>(); // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // SparklyPaper - parallel world ticking (this is from Folia, fixes concurrency bugs with sculk catalysts) + + public static boolean handleBlockSpreadEvent(LevelAccessor world, BlockPos source, BlockPos target, net.minecraft.world.level.block.state.BlockState state, int flags) { + return handleBlockSpreadEvent(world, source, target, state, flags, false); +@@ -826,7 +826,7 @@ public class CraftEventFactory { + CraftBlockState snapshot = CraftBlockStates.getBlockState(world, target); + snapshot.setData(state); + +- BlockSpreadEvent event = new BlockSpreadEvent(snapshot.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverride != null ? CraftEventFactory.sourceBlockOverride : source), snapshot); ++ BlockSpreadEvent event = new BlockSpreadEvent(snapshot.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverrideRT.get() != null ? CraftEventFactory.sourceBlockOverrideRT.get() : source), snapshot); + if (event.callEvent()) { + boolean result = snapshot.place(flags); + return !checkSetResult || result;