diff --git a/PARALLEL_INCOMPATIBLE_PLUGINS.md b/PARALLEL_INCOMPATIBLE_PLUGINS.md new file mode 100644 index 0000000..875eca7 --- /dev/null +++ b/PARALLEL_INCOMPATIBLE_PLUGINS.md @@ -0,0 +1,135 @@ +# Plugins incompatible with Parallel World Ticking + +A list of plugins that I found out that causes issues with Parallel World Ticking. + +This is not a full comprehensive list because I only use a few public plugins, most of the plugins I use on SparklyPower are made by myself. + +## NoCheatPlus (Updated) + +The movement checks can crash the server due to concurrency issues (more below), disable them in the config. + +```javastacktrace +[03:14:05] [serverlevel-tick-worker-8/ERROR]: THE SERVER IS GOING TO CRASH! - Thread serverlevel-tick-worker-8 failed main thread check: Cannot query another world's (world) chunk (25, 16) in a ServerLevelTickThread - Is tick thread? true; Is server level tick thread? true; Currently ticking level: ArenasPvP; Is iterating over levels? true; Are we going to hard throw? false +java.lang.Throwable: null + at net.minecraft.server.level.ServerChunkCache.getChunk(ServerChunkCache.java:272) ~[?:?] + at net.minecraft.world.level.Level.getChunk(Level.java:900) ~[?:?] + at net.minecraft.world.level.Level.getBlockState(Level.java:1175) ~[?:?] + at org.bukkit.craftbukkit.v1_20_R2.block.CraftBlock.getType(CraftBlock.java:238) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at fr.neatmonster.nocheatplus.compat.bukkit.BlockCacheBukkit.fetchTypeId(BlockCacheBukkit.java:52) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.utilities.map.BlockCache.getOrCreateNode(BlockCache.java:317) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.utilities.map.BlockCache.getType(BlockCache.java:379) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.utilities.map.BlockProperties.collectFlagsSimple(BlockProperties.java:4454) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.utilities.location.RichBoundsLocation.collectBlockFlags(RichBoundsLocation.java:1327) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.utilities.location.RichBoundsLocation.collectBlockFlags(RichBoundsLocation.java:1309) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.checks.moving.model.LocationData.setExtraProperties(LocationData.java:92) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.checks.moving.model.MoveData.setWithExtraProperties(MoveData.java:207) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.checks.moving.MovingData.resetPlayerPositions(MovingData.java:585) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.checks.moving.util.AuxMoving.resetPositionsAndMediumProperties(AuxMoving.java:116) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.checks.moving.MovingListener.onPlayerTeleportMonitor(MovingListener.java:1989) ~[NoCheatPlus.jar:?] + at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[?:?] + at java.lang.reflect.Method.invoke(Method.java:580) ~[?:?] + at fr.neatmonster.nocheatplus.event.mini.MultiListenerRegistry$AutoListener.onEvent(MultiListenerRegistry.java:82) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.event.mini.MiniListenerNode.onEvent(MiniListenerNode.java:157) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.event.mini.EventRegistryBukkit$4.execute(EventRegistryBukkit.java:124) ~[NoCheatPlus.jar:?] + at co.aikar.timings.TimedEventExecutor.execute(TimedEventExecutor.java:77) ~[sparklypaper-api-1.20.2-R0.1-SNAPSHOT.jar:git-SparklyPaper-"049a8e5"] + at org.bukkit.plugin.RegisteredListener.callEvent(RegisteredListener.java:70) ~[sparklypaper-api-1.20.2-R0.1-SNAPSHOT.jar:?] + at io.papermc.paper.plugin.manager.PaperEventManager.callEvent(PaperEventManager.java:54) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at io.papermc.paper.plugin.manager.PaperPluginManagerImpl.callEvent(PaperPluginManagerImpl.java:126) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at org.bukkit.plugin.SimplePluginManager.callEvent(SimplePluginManager.java:615) ~[sparklypaper-api-1.20.2-R0.1-SNAPSHOT.jar:?] + at org.bukkit.craftbukkit.v1_20_R2.entity.CraftPlayer.teleport(CraftPlayer.java:1354) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at org.bukkit.craftbukkit.v1_20_R2.entity.CraftPlayer.teleport(CraftPlayer.java:1252) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at net.perfectdreams.dreamcore.utils.extensions.EntityExtensionsKt.teleportToServerSpawn(EntityExtensions.kt:13) ~[DreamCore-reobf.jar:?] + at net.perfectdreams.dreamcore.utils.extensions.EntityExtensionsKt.teleportToServerSpawnWithEffects(EntityExtensions.kt:20) ~[DreamCore-reobf.jar:?] + at net.perfectdreams.dreamcore.utils.extensions.EntityExtensionsKt.teleportToServerSpawnWithEffects$default(EntityExtensions.kt:19) ~[DreamCore-reobf.jar:?] + at net.perfectdreams.dreamxizum.utils.ArenaXizum.finishArena(ArenaXizum.kt:185) ~[DreamXizum-reobf.jar:?] + at net.perfectdreams.dreamxizum.listeners.XizumListener.onDeath(XizumListener.kt:55) ~[DreamXizum-reobf.jar:?] + at com.destroystokyo.paper.event.executor.asm.generated.GeneratedEventExecutor732.execute(Unknown Source) ~[?:?] + at org.bukkit.plugin.EventExecutor$2.execute(EventExecutor.java:77) ~[sparklypaper-api-1.20.2-R0.1-SNAPSHOT.jar:?] + at co.aikar.timings.TimedEventExecutor.execute(TimedEventExecutor.java:77) ~[sparklypaper-api-1.20.2-R0.1-SNAPSHOT.jar:git-SparklyPaper-"049a8e5"] + at org.bukkit.plugin.RegisteredListener.callEvent(RegisteredListener.java:70) ~[sparklypaper-api-1.20.2-R0.1-SNAPSHOT.jar:?] + at io.papermc.paper.plugin.manager.PaperEventManager.callEvent(PaperEventManager.java:54) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at io.papermc.paper.plugin.manager.PaperPluginManagerImpl.callEvent(PaperPluginManagerImpl.java:126) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at org.bukkit.plugin.SimplePluginManager.callEvent(SimplePluginManager.java:615) ~[sparklypaper-api-1.20.2-R0.1-SNAPSHOT.jar:?] + at org.bukkit.craftbukkit.v1_20_R2.event.CraftEventFactory.callPlayerDeathEvent(CraftEventFactory.java:984) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at net.minecraft.server.level.ServerPlayer.die(ServerPlayer.java:961) ~[?:?] + at net.minecraft.world.entity.LivingEntity.hurt(LivingEntity.java:1548) ~[?:?] + at net.minecraft.world.entity.player.Player.hurt(Player.java:973) ~[?:?] + at net.minecraft.server.level.ServerPlayer.hurt(ServerPlayer.java:1130) ~[?:?] + at net.minecraft.world.entity.projectile.AbstractArrow.onHitEntity(AbstractArrow.java:402) ~[?:?] + at net.minecraft.world.entity.projectile.Projectile.onHit(Projectile.java:206) ~[?:?] + at net.minecraft.world.entity.projectile.Projectile.preOnHit(Projectile.java:197) ~[?:?] + at net.minecraft.world.entity.projectile.AbstractArrow.preOnHit(AbstractArrow.java:296) ~[?:?] + at net.minecraft.world.entity.projectile.AbstractArrow.tick(AbstractArrow.java:232) ~[?:?] + at net.minecraft.world.entity.projectile.Arrow.tick(Arrow.java:112) ~[?:?] + at net.minecraft.server.level.ServerLevel.tickNonPassenger(ServerLevel.java:1392) ~[?:?] + at net.minecraft.world.level.Level.guardEntityTick(Level.java:1314) ~[?:?] + at net.minecraft.server.level.ServerLevel.lambda$tick$8(ServerLevel.java:906) ~[?:?] + at net.minecraft.world.level.entity.EntityTickList.forEach(EntityTickList.java:49) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at net.minecraft.server.level.ServerLevel.tick(ServerLevel.java:886) ~[?:?] + at net.minecraft.server.MinecraftServer.lambda$tickChildren$16(MinecraftServer.java:1600) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) ~[?:?] + at java.util.concurrent.FutureTask.run(FutureTask.java:317) ~[?:?] + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[?:?] + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[?:?] + at java.lang.Thread.run(Thread.java:1583) ~[?:?] +``` + +The Change Tracker also has a concurrency bug. NoCheatPlus uses `setAccess` to control which world is being used in `BlockCacheBukkit`. This is fine in a sequential ticking server, but in a parallel ticking server, there is a race condition where one server level tick thread changes the world to `World1`, then another thread changes the world to `World2`, then whoops, a crash happens! + +This also breaks the movement checks above: While the movement checks do have `synchronized` in their methods, the falling block checks don't, and both use the same BlockCache instance (handled by `setAccess`). + +https://github.com/Updated-NoCheatPlus/NoCheatPlus/blob/64bf374fd39297bab5b7adb646063debbd12643e/NCPCompatBukkit/src/main/java/fr/neatmonster/nocheatplus/compat/bukkit/BlockCacheBukkit.java#L52 + +To work around this, disable `changetracker.active` and `changetracker.pistons`. + +I think that NoCheatPlus may have other concurrency issues too due to its use of `setAccess` around the code. In fact, I think that NoCheatPlus has the same issues in Folia too, even tho NoCheatPlus is marked as "Folia supported". + +```javastacktrace +[03:35:04] [serverlevel-tick-worker-6/ERROR]: THE SERVER IS GOING TO CRASH! - Thread serverlevel-tick-worker-6 failed main thread check: Cannot query another world's (world) chunk (-817, -45) in a ServerLevelTickThread - Is tick thread? true; Is server level tick thread? true; Currently ticking level: Resources; Is iterating over levels? true; Are we going to hard throw? false +java.lang.Throwable: null + at net.minecraft.server.level.ServerChunkCache.getChunk(ServerChunkCache.java:272) ~[?:?] + at net.minecraft.world.level.Level.getChunk(Level.java:900) ~[?:?] + at net.minecraft.world.level.Level.getBlockState(Level.java:1175) ~[?:?] + at org.bukkit.craftbukkit.v1_20_R2.block.CraftBlock.getType(CraftBlock.java:238) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at fr.neatmonster.nocheatplus.compat.bukkit.BlockCacheBukkit.fetchTypeId(BlockCacheBukkit.java:52) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.utilities.map.BlockCache.getOrCreateNode(BlockCache.java:317) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.utilities.map.BlockCache.getOrCreateBlockCacheNode(BlockCache.java:442) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.compat.blocks.changetracker.BlockChangeTracker.addBlock(BlockChangeTracker.java:571) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.compat.blocks.changetracker.BlockChangeTracker.addBlocks(BlockChangeTracker.java:433) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.compat.blocks.changetracker.BlockChangeTracker.addBlocks(BlockChangeTracker.java:394) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.compat.blocks.changetracker.BlockChangeListener.onEntityChangeBlock(BlockChangeListener.java:300) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.compat.blocks.changetracker.BlockChangeListener.access$200(BlockChangeListener.java:56) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.compat.blocks.changetracker.BlockChangeListener$2.onEvent(BlockChangeListener.java:99) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.compat.blocks.changetracker.BlockChangeListener$2.onEvent(BlockChangeListener.java:93) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.event.mini.MiniListenerNode.onEvent(MiniListenerNode.java:157) ~[NoCheatPlus.jar:?] + at fr.neatmonster.nocheatplus.event.mini.EventRegistryBukkit$4.execute(EventRegistryBukkit.java:124) ~[NoCheatPlus.jar:?] + at co.aikar.timings.TimedEventExecutor.execute(TimedEventExecutor.java:77) ~[sparklypaper-api-1.20.2-R0.1-SNAPSHOT.jar:git-SparklyPaper-"049a8e5"] + at org.bukkit.plugin.RegisteredListener.callEvent(RegisteredListener.java:70) ~[sparklypaper-api-1.20.2-R0.1-SNAPSHOT.jar:?] + at io.papermc.paper.plugin.manager.PaperEventManager.callEvent(PaperEventManager.java:54) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at io.papermc.paper.plugin.manager.PaperPluginManagerImpl.callEvent(PaperPluginManagerImpl.java:126) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at org.bukkit.plugin.SimplePluginManager.callEvent(SimplePluginManager.java:615) ~[sparklypaper-api-1.20.2-R0.1-SNAPSHOT.jar:?] + at org.bukkit.craftbukkit.v1_20_R2.event.CraftEventFactory.callEntityChangeBlockEvent(CraftEventFactory.java:1411) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at org.bukkit.craftbukkit.v1_20_R2.event.CraftEventFactory.callEntityChangeBlockEvent(CraftEventFactory.java:1403) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at net.minecraft.world.entity.item.FallingBlockEntity.fall(FallingBlockEntity.java:98) ~[?:?] + at net.minecraft.world.entity.item.FallingBlockEntity.fall(FallingBlockEntity.java:92) ~[?:?] + at net.minecraft.world.level.block.FallingBlock.tick(FallingBlock.java:37) ~[?:?] + at net.minecraft.world.level.block.state.BlockBehaviour$BlockStateBase.tick(BlockBehaviour.java:1205) ~[?:?] + at net.minecraft.server.level.ServerLevel.tickBlock(ServerLevel.java:1340) ~[?:?] + at net.minecraft.world.ticks.LevelTicks.runCollectedTicks(LevelTicks.java:197) ~[?:?] + at net.minecraft.world.ticks.LevelTicks.tick(LevelTicks.java:94) ~[?:?] + at net.minecraft.server.level.ServerLevel.tick(ServerLevel.java:848) ~[?:?] + at net.minecraft.server.MinecraftServer.lambda$tickChildren$16(MinecraftServer.java:1600) ~[sparklypaper-1.20.2.jar:git-SparklyPaper-"049a8e5"] + at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) ~[?:?] + at java.util.concurrent.FutureTask.run(FutureTask.java:317) ~[?:?] + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[?:?] + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[?:?] + at java.lang.Thread.run(Thread.java:1583) ~[?:?] +``` + +## MyPet + +If a player mounted on a Warden teleports to another world, the server crashes. + +This is caused by https://github.com/MyPetORG/MyPet/issues/1647 and can even cause issues in vanilla Paper. In vanilla Paper, instead of crashing the server, the player is teleported back to the Warden's location. + +Fork that removes the affecting code: https://github.com/SparklyPower/MyPet \ No newline at end of file diff --git a/PARALLEL_NOTES.md b/PARALLEL_NOTES.md new file mode 100644 index 0000000..ac83af6 --- /dev/null +++ b/PARALLEL_NOTES.md @@ -0,0 +1,45 @@ +# Parallel World Ticking Notes + +Notes about the Parallel World Ticking implementation. + +i'm stoopid so this may have a lot of dumb incorrect stuff pls don't hurt me spottedleaf :( + +## Opening an inventory after a world switch + +If you have an event that teleports the player, and somehow that event also opens an BlockEntity inventory, the server will lock up waiting for chunks on another world. + +Example: +```kotlin +@EventHandler +fun onInteract(e: PlayerInteractEvent) { + e.player.teleport(Location(Bukkit.getWorld("..."), 0.0, 0.0, 0.0)) +} +``` + +If you right-click on a chest, the player will be teleported, the chest will open... and the server will lock up! + +This happens because the player is teleported BEFORE the inventory has been opened, the inventory is only opened AFTER the player has been teleported. + +In a normal Paper server, this ain't a huge deal: The inventory is closed when the player is ticked in the new world, since it will fail the `stillValid` check. + +In a parallel environment however, the `stillValid` and plugins listening for `InventoryCloseEvent` while loading the `holder` may load chunks from other worlds in a different ServerLevel Tick Thread! This locks up since we CANNOT load chunks from other worlds, because well, you know, it attempts to load on the main thread, but because the main thread is blocked... + +To work around this issue... +* `openMenu` will ignore any open menu requests until player `hasTickedAtLeastOnceInNewWorld`. +* There are additional checks in the `BaseContainerBlockEntity#canUnlock`, to reject containers that were attempted to be opened after the player switched worlds. + +In the future, it would be nice if `openMenu` could check if the container is still valid in the new world and then decide if the container should be closed. Currently, to open inventories after a teleport, wait 1 tick. + +## TickThread Checks in NMS Level `setBlockEntity` and `getBlockEntity` + +I attempted to add TickThread checks to these two methods, however, it seems like StarLight DOES access these block entities in a different "Tuinity Chunk System Worker" thread. Heck, even CoreProtect accesses these block entities from an async thread! + +I looked up what Folia does, and it seems that they do have thread checks there, but it seems that they only check if it is a tick thread instead of checking if the current thread is related to the current world. + +To be honest, it seems that `getBlockEntity` is thread safe. EXCEPT if you are accessing block entities from a separate `ServerLevelTickThread`! This will cause a main thread chunk load, and that will freeze the server. + +The `capturedTileEntities` is also sus, but the map itself doesn't seem to be ever iterated, the only time it is iterated is via `entrySet()`. Maybe just to be extra sure, synchronize it? (Folia doesn't do that) + +So, instead of doing what Folia does, we check `getBlockEntity` it via `ensureTickThreadOrAsyncThread`. + +However, `setBlockEntity` still has the `ensureTickThread` check. \ No newline at end of file diff --git a/PARALLEL_WORLD_TICKING.md b/PARALLEL_WORLD_TICKING.md new file mode 100644 index 0000000..77849d2 --- /dev/null +++ b/PARALLEL_WORLD_TICKING.md @@ -0,0 +1,101 @@ +# Parallel World Ticking + +"mom can we have folia?" + +"we already have folia at home" + +folia at home: *this* + +## ⚠️⚠️⚠️ THIS IS EXPERIMENTAL ⚠️⚠️⚠️ + +DON'T COMPLAIN IF YOUR SERVER EXPLODES. AFTER ALL, SPARKLYPAPER WAS MADE FOR SPARKLYPOWER, ABSOUTELY NO SUPPORT FOR YOU!!! + +In the Minecraft server world, there are various ways of implementing concurrent ticking, such as... + +* The Vanilla Way™: All worlds are ticked sequentially. +* Parallel World Ticking: All worlds are ticked in parallel, but each tick waits for all worlds to be processed before proceeding. +* Asynchronous World Ticking: All worlds are ticked asynchronously, each world with its own tick rate. +* Asynchronous Region Ticking: Chunks are split up in regions, and each region are ticked asynchronously. This is what [Folia](https://github.com/PaperMC/Folia) does. +* Truly independent servers: Each server has its own world, so each world is completely isolated from each other. + +SparklyPaper moves world ticking to its first logical step after The Vanilla Way™, moving Vanilla's world ticking into separate threads, allowing worlds to be ticked in parallel. Every tick waits until all worlds finishes ticking. This means that your TPS is _mostly_ based off the MSPT of your heaviest world. Useful to spread out players to multiple worlds! (Example: Multiple Survival worlds) + +We do run Parallel World Ticking in production @ [SparklyPower](https://sparklypower.net/). Our server was being bottlenecked by all the things our Survival world needed to tick (such as Villagers, blocks like farmland, those pesky Axolotls, etc) that we needed a solution. We first tried to go with the "one server per world" but after looking at so much complexity we would need to handle, such as... + +* How are you going to do Inventory syncing? +* How are you going to maintain multiple servers? +* How are you going to query how many players are connected to *all* servers? +* How are you going to implement player name autocomplete, if players may be on different servers? +* Are you ready to reimplement vanilla commands such as `/tp` and `/give`? + +That we decided that maybe it was time to pull off a crazy patch to do parallel world ticking instead. + +Synchronization issues *are expected to happen*. Thread checks are still present and only the `ServerLevelTickThread` that is bound to the modified world, or the main `TickThread`, can modify world data. Plugins will break with Parallel World Ticking if they attempt to modify other worlds in a thread that isn't theirs! Plugins can work around this by scheduling a main thread task. + +## Well, if Folia has a superior ticking system, why not use Folia? + +Folia is amazing, in fact, a lot of the code used for parallel world ticking was heavily inspired by... and, uh, copied from... Folia. + +However, due to the way Folia works, a lot of plugins *will* break and require updates from their maintainers to make them work in Folia. Which is why Folia, by default, does not allow plugins not marked as Folia compatible to work. + +With Parallel World Ticking, not a lot of plugins *should* break since plugins mostly do stuff on the same world that the event has been triggered, and player actions and a bunch of other stuff are still processed on the main thread, so a lot of plugins should, hopefully, work out of the box. Of course, the downside is that Parallel World Ticking does not provide all the performance advantages that Folia has, and you are forced to break down your players into multiple worlds to get those sweet TPS improvements, while with Folia you only need to get players to be far from each other in the same world. + +However, there are things that WILL BREAK, such as teleporting players/entities to another world on events called on the server level tick thread. You can work around these issues by scheduling these API calls to be run after all worlds are ticked. + +Because Minecraft's vanilla mechanics do not interfer with other worlds, aside from portal/end portal respawn, maintaining the vanilla behavior for items is easier compared to Folia. + +So this is a stopgap solution while Folia isn't ready for prime time yet, without requiring you to do the whole "one servers for each world" approach, which is way harder to develop, handle, and maintain. + +Besides, it is fun! + +## If this is possible, why Paper doesn't have it built-in? + +Plugins, CraftBukkit, and the Minecraft Server itself, weren't really made with parallel world ticking in mind. + +Adding this to Paper would ensure that a lot of angry users would complain to Paper that plugin xyz isn't working. This is also the reason about why Folia only allows loading plugins marked as Folia compatible. + +## I've heard that anything async related in the Minecraft code is bad + +Yes, attempts to do ✨ async magic ✨ in the Minecraft server aren't a new thing. This has been done in the past in Akarin, Yatopia, and other forks ([patch](https://github.com/YatopiaMC/Yatopia/blob/1a54ef2f995f049d4fcf1f2bd084691126f10046/patches/server/0046-Option-for-async-world-ticking.patch)). + +However, the previous attempts were based on "`synchronize` and pray", which is why they were unstable and not recommended for production. + +MrPowerGamerBR from 2018 had even made a meme making fun of these patches. + +![https://cdn.discordapp.com/attachments/289587909051416579/482922902283485184/async_forks.png?ex=6558cd80&is=65465880&hm=28743988187da5dfa050c417ca9fa575c6924b6631c549f93e3186522a376c82&](https://cdn.discordapp.com/attachments/289587909051416579/482922902283485184/async_forks.png?ex=6558cd80&is=65465880&hm=28743988187da5dfa050c417ca9fa575c6924b6631c549f93e3186522a376c82&) + +SparklyPaper follows Folia's footsteps and keeps everything in check, keeping all tick thread checks in the code. Most of the groundwork had already been done by Spottedleaf and the Paper team. thx leaf *pets the leaf* + +But of course, that doesn't mean that SparklyPaper is perfect! If your server crashes, that ain't gonna be my fault xd. + +## I live on the edge and I don't want random "not on main thread" throws + +Off-main thread throws can be disabled with `-Dsparklypaper.disableHardThrow=true`, but THIS IS NOT RECOMMENDED because you SHOULD FIX THE ISSUES THEMSELVES instead of RISKING DATA CORRUPTION!!! The server will still log the stacktrace of where the exception happened. + +In fact, disabling throws is not an easy way out: Yes, you avoid some functions borking out. But, if the tick thread check has failed, your server is probably going to crash anyway. Example: If a plugin is attempting to teleport a player to world X while they are in a TickThread of world Y, the server will lock up because loading chunks outside of the world's tick thread or from an async thread is not allowed. In fact, if you had kept hard throws enabled, your server wouldn't have crashed because the request would've been denied! Fix the dang issues instead!!! + +## Profiling with Spark + +By default, Spark will profile the `Server thread` thread, which ain't good for us if we want to profile what is being used in our worlds. + +Spark has an undocumented configuration setting to configure what threads the background profiler will track. + +In Spark's `config.json`, add `"backgroundProfilerThreadDumper": "Server thread,serverlevel-tick-worker-1,serverlevel-tick-worker-2,serverlevel-tick-worker-3,serverlevel-tick-worker-4,serverlevel-tick-worker-5,serverlevel-tick-worker-6,serverlevel-tick-worker-7,serverlevel-tick-worker-8"` (the thread list may vary if you changed your thread count) to dump the Server thread and the ServerLevel ticking worker threads. + +Because Spark queries the thread list on startup, we prestart all the threads in the thread pool with `Util.SERVERLEVEL_TICK_EXECUTOR.prestartAllCoreThreads()`. + +## Can I disable this? + +SparklyPaper was tailor-made for SparklyPower with features that we need, so... no. + +I'm not even sure why this question is even here considering that the only real SparklyPaper user is myself. :3 + +*Theorically* if you really want to, you can set `parallel-world-ticking.threads` to 1, and it would work just like the good old days. None of the additional checks are removed, however. + +## Plugin Incompatibilities + +[Here's a list of plugins that have issues with parallel world ticking](PARALLEL_INCOMPATIBLE_PLUGINS.md) + +## Implementation Notes + +If you are curious about things that I've learned while making this, I wrote [some notes about why some things were implemented the way that they are](PARALLEL_NOTES.md). diff --git a/README.md b/README.md index 175075e..95c6d07 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,15 @@ SparklyPower's Paper fork, with a mix of weird & crazy patches from other forks! While our fork is mostly cherry-picked patches from other forks, we do have some handmade patches too to add and optimize some of the things that we have in our server! +## Features + +SparklyPaper's config file is `sparklypaper.yml`, the file is, by default, placed on the root of your server. + * Configurable Farm Land moisture tick rate when the block is already moisturised - * The isNearWater check is costly, especially if you have a lot of farm lands. If the block is already moistured, we can change the tick rate of it to avoid these expensive isNearWater checks. + * The `isNearWater` check is costly, especially if you have a lot of farm lands. If the block is already moistured, we can change the tick rate of it to avoid these expensive `isNearWater` checks. +* Check how much MSPT (milliseconds per tick) each world is using in `/mspt` + * Useful to figure out which worlds are lagging your server. +* Parallel World Ticking + * "mom can we have folia?" "we already have folia at home" folia at home: [Parallel World Ticking](PARALLEL_WORLD_TICKING.md) We don't cherry-pick *everything* from other forks, only patches that I can see and think "yeah, I can see how this would improve performance" or patches that target specific performance/feature pain points in our server are cherry-picked! In fact, some patches that are used in other forks [may be actually borked](BORKED_PATCHES.md)... \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 8e8b08d..e5cdb97 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ group=net.sparklypower.sparklypaper version=1.20.2-R0.1-SNAPSHOT mcVersion=1.20.2 -paperRef=4675152f4908431e0f944a7bf9fa3b2181a2dfd6 +paperRef=f186318a91cbd3b2a2259d39cb88576989a496b8 org.gradle.caching=true org.gradle.parallel=true diff --git a/patches/server/0001-new-fork-who-dis-Rebrand-to-SparklyPaper-and-Build-C.patch b/patches/server/0001-new-fork-who-dis-Rebrand-to-SparklyPaper-and-Build-C.patch index 6c7607a..b06d111 100644 --- a/patches/server/0001-new-fork-who-dis-Rebrand-to-SparklyPaper-and-Build-C.patch +++ b/patches/server/0001-new-fork-who-dis-Rebrand-to-SparklyPaper-and-Build-C.patch @@ -5,7 +5,7 @@ Subject: [PATCH] new fork who dis - Rebrand to SparklyPaper and Build Changes diff --git a/build.gradle.kts b/build.gradle.kts -index a79461457ea19339f47572c70705d655ebc55276..d43c3f2e17f9ef48ec458f9c94478cfd02db6edb 100644 +index 79beac737c17412913983614bd478d33e3c6ed58..8adfd75d66cbd3e3afafc0ea167d1e6568c4adbe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,8 @@ import io.papermc.paperweight.util.* @@ -54,7 +54,7 @@ index a79461457ea19339f47572c70705d655ebc55276..d43c3f2e17f9ef48ec458f9c94478cfd standardInput = System.`in` workingDir = rootProject.layout.projectDirectory diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 97745f0bab8d82d397c6c2a5775aed92bca0a034..0dfd9a2f9195ec018ed5069f43908b8c0e09edbd 100644 +index 8f31413c939cc2b0454ad3d9a1b618dbae449d00..25367df06a8a6e8b0b3a56652a5fb1c70a15632d 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1697,7 +1697,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop -+) ++) { ++ @Serializable ++ class ParallelWorldTicking( ++ val threads: Int ++ ) ++} \ No newline at end of file diff --git a/src/main/kotlin/net/sparklypower/sparklypaper/configs/SparklyPaperConfigUtils.kt b/src/main/kotlin/net/sparklypower/sparklypaper/configs/SparklyPaperConfigUtils.kt new file mode 100644 -index 0000000000000000000000000000000000000000..82a29b23429e31d78e09fa23e8c87cec76ba63bd +index 0000000000000000000000000000000000000000..fbbb11c1a62a28a251c35261fb29e6267a08c1a3 --- /dev/null +++ b/src/main/kotlin/net/sparklypower/sparklypaper/configs/SparklyPaperConfigUtils.kt -@@ -0,0 +1,46 @@ +@@ -0,0 +1,49 @@ +package net.sparklypower.sparklypaper.configs + +import com.charleskorn.kaml.Yaml @@ -243,6 +250,9 @@ index 0000000000000000000000000000000000000000..82a29b23429e31d78e09fa23e8c87cec + configFile.writeText( + yaml.encodeToString( + SparklyPaperConfig( ++ SparklyPaperConfig.ParallelWorldTicking( ++ threads = 8 ++ ), + mapOf( + "default" to SparklyPaperWorldConfig( + SparklyPaperWorldConfig.TickRates( diff --git a/patches/server/0005-Parallel-world-ticking.patch b/patches/server/0005-Parallel-world-ticking.patch new file mode 100644 index 0000000..a09a631 --- /dev/null +++ b/patches/server/0005-Parallel-world-ticking.patch @@ -0,0 +1,2162 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrPowerGamerBR +Date: Tue, 7 Nov 2023 01:34:14 -0300 +Subject: [PATCH] Parallel world ticking + +"mom can we have folia?" "we already have folia at home" folia at home: + +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +index abd0217cf0bff183c8e262edc173a53403797c1a..1ef797d2c743077c40c7e1796d4afe324e162970 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +@@ -1023,7 +1023,7 @@ public final class ChunkHolderManager { + if (changedFullStatus.isEmpty()) { + return; + } +- if (!TickThread.isTickThread()) { ++ if (!TickThread.isTickThreadFor(world)) { // SparklyPaper - parallel world ticking + this.taskScheduler.scheduleChunkTask(() -> { + final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate; + for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { +@@ -1052,7 +1052,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"); +@@ -1327,14 +1327,14 @@ public final class ChunkHolderManager { + } + + private boolean processTicketUpdates(final boolean checkLocks, final boolean processFullUpdates, List scheduledTasks) { +- TickThread.ensureTickThread("Cannot process ticket levels off-main"); ++ TickThread.ensureTickThread(world, "Cannot process ticket levels off-main"); // SparklyPaper - parallel world ticking + if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { + throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager"); + } + + List changedFullStatus = null; + +- final boolean isTickThread = TickThread.isTickThread(); ++ final boolean isTickThread = TickThread.isTickThreadFor(world); + + boolean ret = false; + final boolean canProcessFullUpdates = processFullUpdates & isTickThread; +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java +index f975cb93716e137d973ff2f9011acdbef58859a2..cc510eea4b872e1238f97846db638b2a7028a66d 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java +@@ -327,7 +327,7 @@ public final class ChunkTaskScheduler { + public void scheduleTickingState(final int chunkX, final int chunkZ, final FullChunkStatus toStatus, + final boolean addTicket, final PrioritisedExecutor.Priority priority, + final Consumer onComplete) { +- if (!TickThread.isTickThread()) { ++ if (!TickThread.isTickThreadFor(world, chunkX, chunkZ)) { // SparklyPaper - parallel world ticking (other threads may call this method, such as the container stillValid check, which may trigger a chunk load in a different thread) + this.scheduleChunkTask(chunkX, chunkZ, () -> { + ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + }, priority); +@@ -483,7 +483,7 @@ public final class ChunkTaskScheduler { + + public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket, + final PrioritisedExecutor.Priority priority, final Consumer onComplete) { +- if (!TickThread.isTickThread()) { ++ if (!TickThread.isTickThreadFor(world, chunkX, chunkZ)) { // SparklyPaper - parallel world ticking + this.scheduleChunkTask(chunkX, chunkZ, () -> { + ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + }, priority); +diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java +index f9063e2282f89e97a378f06822cde0a64ab03f9a..98abe711f63c0a112ef969bd74bda81f2a72ed82 100644 +--- a/src/main/java/io/papermc/paper/util/TickThread.java ++++ b/src/main/java/io/papermc/paper/util/TickThread.java +@@ -17,6 +17,7 @@ import java.util.concurrent.atomic.AtomicInteger; + public class TickThread extends Thread { + + public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.strict-thread-checks"); ++ 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 + + static { + if (STRICT_THREAD_CHECKS) { +@@ -41,50 +42,93 @@ public class TickThread extends Thread { + @Deprecated + public static void ensureTickThread(final String reason) { + if (!isTickThread()) { +- MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); +- throw new IllegalStateException(reason); ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason + " - " + getTickThreadInformation(), new Throwable()); ++ if (HARD_THROW) ++ throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final ServerLevel world, final BlockPos pos, final String reason) { + if (!isTickThreadFor(world, pos)) { +- MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); +- throw new IllegalStateException(reason); ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason + " @ world " + world.getWorld().getName() + " blockPos: " + pos + " - " + getTickThreadInformation(), new Throwable()); ++ if (HARD_THROW) ++ throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final ServerLevel world, final ChunkPos pos, final String reason) { + if (!isTickThreadFor(world, pos)) { +- MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); +- throw new IllegalStateException(reason); ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason + " @ world " + world.getWorld().getName() + " chunkPos: " + pos + " - " + getTickThreadInformation(), new Throwable()); ++ if (HARD_THROW) ++ throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) { + if (!isTickThreadFor(world, chunkX, chunkZ)) { +- MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); +- throw new IllegalStateException(reason); ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason + " @ world " + world.getWorld().getName() + " chunkX: " + chunkX + " chunkZ: " + chunkZ + " - " + getTickThreadInformation(), new Throwable()); ++ if (HARD_THROW) ++ throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final Entity entity, final String reason) { + if (!isTickThreadFor(entity)) { +- MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); +- throw new IllegalStateException(reason); ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason + " @ entity " + entity.getStringUUID() + " - " + getTickThreadInformation(), new Throwable()); ++ if (HARD_THROW) ++ throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final ServerLevel world, final AABB aabb, final String reason) { + if (!isTickThreadFor(world, aabb)) { +- MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); +- throw new IllegalStateException(reason); ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason + " @ world " + world.getWorld().getName() + " aabb: " + aabb + " - " + getTickThreadInformation(), new Throwable()); ++ if (HARD_THROW) ++ throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final ServerLevel world, final double blockX, final double blockZ, final String reason) { + if (!isTickThreadFor(world, blockX, blockZ)) { +- MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); +- throw new IllegalStateException(reason); ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason + " @ world " + world.getWorld().getName() + " blockX: " + blockX + " blockZ: " + blockZ + " - " + getTickThreadInformation(), 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 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 ServerLevel world, final String reason) { ++ if (!isTickThreadFor(world)) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason + " @ world " + world.getWorld().getName() + " - " + getTickThreadInformation(), 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) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread ONLY tick thread check: " + reason + " - " + getTickThreadInformation(), 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 ServerLevel world, final String reason) { ++ boolean isValidTickThread = isTickThreadFor(world); ++ boolean isAsyncThread = !isTickThread(); ++ boolean isValid = isAsyncThread || isValidTickThread; ++ if (!isValid) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread or async thread check: " + reason + " @ world " + world.getWorld().getName() + " - " + getTickThreadInformation(), new Throwable()); ++ if (HARD_THROW) ++ throw new IllegalStateException(reason); + } + } + +@@ -109,6 +153,32 @@ public class TickThread extends Thread { + return (TickThread)Thread.currentThread(); + } + ++ public static String getTickThreadInformation() { ++ 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.getServer().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 static boolean isTickThread() { + return Thread.currentThread() instanceof TickThread; + } +@@ -118,42 +188,111 @@ public class TickThread extends Thread { + } + + public static boolean isTickThreadFor(final ServerLevel 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 ServerLevel 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 ServerLevel 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 ServerLevel 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 ServerLevel 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 ServerLevel 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 ServerLevel 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 ServerLevel 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 ServerLevel 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 ServerLevel 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 ServerLevel currentlyTickingServerLevel; + } ++ // SparklyPaper end + } +diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java +index 5c1503f5b173138fc9e918d5562a981ca8b66d06..5f9249526c88970cb18d45fd917ebabad2c77809 100644 +--- a/src/main/java/net/minecraft/Util.java ++++ b/src/main/java/net/minecraft/Util.java +@@ -98,6 +98,10 @@ public class Util { + } + }); + // Paper end - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread ++ // SparklyPaper start - parallel world ticking ++ @Nullable ++ public static java.util.concurrent.ThreadPoolExecutor SERVERLEVEL_TICK_EXECUTOR = null; ++ // SparklyPaper end + private static final ExecutorService IO_POOL = makeIoExecutor(); + private static final DateTimeFormatter FILENAME_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss", Locale.ROOT); + public static final long NANOS_PER_MILLI = 1000000L; +@@ -219,6 +223,10 @@ public class Util { + public static void shutdownExecutors() { + shutdownExecutor(BACKGROUND_EXECUTOR); + shutdownExecutor(IO_POOL); ++ // SparklyPaper start - parallel world ticking ++ if (SERVERLEVEL_TICK_EXECUTOR != null) ++ shutdownExecutor(SERVERLEVEL_TICK_EXECUTOR); ++ // SparklyPaper end + } + + private static void shutdownExecutor(ExecutorService service) { +diff --git a/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java +index 155bd3d6d9c7d3cac7fd04de8210301251d1e17a..f51d90f54ae693e7e9c8aa0ea14af9fdd26351f9 100644 +--- a/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/AbstractProjectileDispenseBehavior.java +@@ -32,7 +32,7 @@ public abstract class AbstractProjectileDispenseBehavior extends DefaultDispense + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) enumdirection.getStepX(), (double) ((float) enumdirection.getStepY() + 0.1F), (double) enumdirection.getStepZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java +index 80dbeb0a988c749feaaba26ce5ad93c181d88a5d..4e08ae0af6e357347486d5c994f7dd413236c2b1 100644 +--- a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java +@@ -62,7 +62,7 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d1, d2 + d4, d3)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java +index 379890ae05b2fb4bd81b2fa907413d3736ba1169..e9a21ce76cca0f6e615a447451048df9cf6b637e 100644 +--- a/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java +@@ -74,7 +74,7 @@ public class DefaultDispenseItemBehavior implements DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), CraftVector.toBukkit(entityitem.getDeltaMovement())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + world.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +index a0c7c6208314d981e8577ad69ef1c5193290a085..5be8039092cd6a2a787fdfaf9d516a18a60f3d53 100644 +--- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +@@ -222,7 +222,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -277,7 +277,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -333,7 +333,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) list.get(0).getBukkitEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + world.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -389,7 +389,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityhorseabstract.getBukkitEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + world.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -463,7 +463,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityhorsechestedabstract.getBukkitEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + world.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -502,7 +502,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(enumdirection.getStepX(), enumdirection.getStepY(), enumdirection.getStepZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -560,7 +560,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d3, d4, d5)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -633,7 +633,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -707,7 +707,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -754,7 +754,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -815,7 +815,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -844,8 +844,8 @@ public interface DispenseItemBehavior { + // CraftBukkit start + worldserver.captureTreeGeneration = false; + if (worldserver.capturedBlockStates.size() > 0) { +- TreeType treeType = SaplingBlock.treeType; +- SaplingBlock.treeType = null; ++ TreeType treeType = SaplingBlock.treeTypeRT.get(); // SparklyPaper - parallel world ticking ++ SaplingBlock.treeTypeRT.set(null); // SparklyPaper - parallel world ticking + Location location = CraftLocation.toBukkit(blockposition, worldserver.getWorld()); + List blocks = new java.util.ArrayList<>(worldserver.capturedBlockStates.values()); + worldserver.capturedBlockStates.clear(); +@@ -884,7 +884,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -941,7 +941,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -990,7 +990,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -1063,7 +1063,7 @@ public interface DispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - only single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +index e17090003988ad2c890d48666c2234b14d511345..6b82ce7423fe08238bd9b44b442ca9b05a3101d8 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +@@ -40,7 +40,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java +index 6f2adf2334e35e8a617a4ced0c1af2abf32bbd8d..a5ea9df0a021ed820c0c1ccb612caebd582878e2 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java +@@ -37,7 +37,7 @@ public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event + + BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + pointer.level().getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 81b47185a939dc2d7e9e0bda16bb910ef9424d23..12ff402c72e45afc06557e3f31ef5002eec5eee4 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -160,6 +160,7 @@ import net.minecraft.world.level.storage.PrimaryLevelData; + import net.minecraft.world.level.storage.ServerLevelData; + import net.minecraft.world.level.storage.WorldData; + import net.minecraft.world.level.storage.loot.LootDataManager; ++import org.bukkit.craftbukkit.event.CraftEventFactory; + import org.slf4j.Logger; + + // CraftBukkit start +@@ -1523,6 +1524,65 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop> tasks = new java.util.ArrayDeque<>(); ++ // while (iterator.hasNext()) { // SparklyPaper - commented out to cause diff when upstream changes this code ++ // ServerLevel worldserver = (ServerLevel) iterator.next(); ++ // worldserver.updateLagCompensationTick(); // Paper - lag compensation ++ // worldserver.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper ++ // net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper ++ // worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper ++ // ++ // this.profiler.push(() -> { ++ // return worldserver + " " + worldserver.dimension().location(); ++ // }); ++ // /* Drop global time updates ++ // if (this.tickCount % 20 == 0) { ++ // this.profiler.push("timeSync"); ++ // this.synchronizeTime(worldserver); ++ // this.profiler.pop(); ++ // } ++ // // CraftBukkit end */ ++ // ++ // this.profiler.push("tick"); ++ // ++ // try { ++ // worldserver.timings.doTick.startTiming(); // Spigot ++ // long i = Util.getNanos(); // SparklyPaper - track world's MSPT ++ // worldserver.tick(shouldKeepTicking); ++ // // SparklyPaper start - track world's MSPT ++ // long j = this.tickTimes[this.tickCount % 100] = Util.getNanos() - i; ++ // ++ // // These are from the "tickServer" function ++ // worldserver.tickTimes5s.add(this.tickCount, j); ++ // worldserver.tickTimes10s.add(this.tickCount, j); ++ // worldserver.tickTimes60s.add(this.tickCount, j); ++ // // SparklyPaper end ++ // // Paper start ++ // for (final io.papermc.paper.chunk.SingleThreadChunkRegionManager regionManager : worldserver.getChunkSource().chunkMap.regionManagers) { ++ // regionManager.recalculateRegions(); ++ // } ++ // // Paper end ++ // worldserver.timings.doTick.stopTiming(); // Spigot ++ // } catch (Throwable throwable) { ++ // // Spigot Start ++ // CrashReport crashreport; ++ // try { ++ // crashreport = CrashReport.forThrowable(throwable, "Exception ticking world"); ++ // } catch (Throwable t) { ++ // if (throwable instanceof ThreadDeath) { throw (ThreadDeath)throwable; } // Paper ++ // throw new RuntimeException("Error generating crash report", t); ++ // } ++ // // Spigot End ++ // ++ // worldserver.fillReportDetails(crashreport); ++ // throw new ReportedException(crashreport); ++ // } ++ // ++ // this.profiler.pop(); ++ // this.profiler.pop(); ++ // worldserver.explosionDensityCache.clear(); // Paper - Optimize explosions ++ // } + while (iterator.hasNext()) { + ServerLevel worldserver = (ServerLevel) iterator.next(); + worldserver.updateLagCompensationTick(); // Paper - lag compensation +@@ -1530,56 +1590,55 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper + +- this.profiler.push(() -> { +- return worldserver + " " + worldserver.dimension().location(); +- }); +- /* Drop global time updates +- if (this.tickCount % 20 == 0) { +- this.profiler.push("timeSync"); +- this.synchronizeTime(worldserver); +- this.profiler.pop(); +- } +- // CraftBukkit end */ ++ tasks.add( ++ net.minecraft.Util.SERVERLEVEL_TICK_EXECUTOR.submit(() -> { ++ try { ++ io.papermc.paper.util.TickThread.ServerLevelTickThread currentThread = (io.papermc.paper.util.TickThread.ServerLevelTickThread) Thread.currentThread(); ++ currentThread.setName("serverlevel-tick-worker [" + worldData.getLevelName() + "]"); ++ currentThread.currentlyTickingServerLevel = worldserver; ++ ++ long i = Util.getNanos(); // SparklyPaper - track world's MSPT ++ worldserver.tick(shouldKeepTicking); ++ worldserver.explosionDensityCache.clear(); // Paper - Optimize explosions + +- this.profiler.push("tick"); ++ // SparklyPaper start - track world's MSPT ++ long j = this.tickTimes[this.tickCount % 100] = Util.getNanos() - i; ++ ++ // These are from the "tickServer" function ++ worldserver.tickTimes5s.add(this.tickCount, j); ++ worldserver.tickTimes10s.add(this.tickCount, j); ++ worldserver.tickTimes60s.add(this.tickCount, j); ++ // SparklyPaper end ++ ++ currentThread.currentlyTickingServerLevel = null; // Reset current ticking level ++ } catch (Throwable throwable) { ++ // Spigot Start ++ CrashReport crashreport; ++ try { ++ crashreport = CrashReport.forThrowable(throwable, "Exception ticking world"); ++ } catch (Throwable t) { ++ if (throwable instanceof ThreadDeath) { throw (ThreadDeath)throwable; } // Paper ++ throw new RuntimeException("Error generating crash report", t); ++ } ++ // Spigot End + ++ worldserver.fillReportDetails(crashreport); ++ throw new ReportedException(crashreport); ++ } ++ }, worldserver) ++ ); ++ } ++ while (!tasks.isEmpty()) { + try { +- worldserver.timings.doTick.startTiming(); // Spigot +- long i = Util.getNanos(); // SparklyPaper - track world's MSPT +- worldserver.tick(shouldKeepTicking); +- // SparklyPaper start - track world's MSPT +- long j = this.tickTimes[this.tickCount % 100] = Util.getNanos() - i; +- +- // These are from the "tickServer" function +- worldserver.tickTimes5s.add(this.tickCount, j); +- worldserver.tickTimes10s.add(this.tickCount, j); +- worldserver.tickTimes60s.add(this.tickCount, j); +- // SparklyPaper end +- // Paper start ++ ServerLevel worldserver = tasks.pop().get(); + for (final io.papermc.paper.chunk.SingleThreadChunkRegionManager regionManager : worldserver.getChunkSource().chunkMap.regionManagers) { + regionManager.recalculateRegions(); + } +- // Paper end +- worldserver.timings.doTick.stopTiming(); // Spigot +- } catch (Throwable throwable) { +- // Spigot Start +- CrashReport crashreport; +- try { +- crashreport = CrashReport.forThrowable(throwable, "Exception ticking world"); +- } catch (Throwable t) { +- if (throwable instanceof ThreadDeath) { throw (ThreadDeath)throwable; } // Paper +- throw new RuntimeException("Error generating crash report", t); +- } +- // Spigot End +- +- worldserver.fillReportDetails(crashreport); +- throw new ReportedException(crashreport); ++ } catch (java.lang.InterruptedException | java.util.concurrent.ExecutionException e) { ++ throw new RuntimeException(e); // Propagate exception + } +- +- this.profiler.pop(); +- this.profiler.pop(); +- worldserver.explosionDensityCache.clear(); // Paper - Optimize explosions + } ++ // SparklyPaper end + this.isIteratingOverLevels = false; // Paper + + this.profiler.popPush("connection"); +diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +index 8c20221736419bcb9c3e570af624eef8e6fc3b09..bcd04c04c27a83ca56f05d5aa25f8a103b0c072d 100644 +--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +@@ -17,6 +17,7 @@ import java.util.Collections; + import java.util.List; + import java.util.Locale; + import java.util.Optional; ++import java.util.concurrent.TimeUnit; + import java.util.function.BooleanSupplier; + import javax.annotation.Nullable; + import net.minecraft.DefaultUncaughtExceptionHandler; +@@ -50,6 +51,7 @@ import net.minecraft.world.level.GameRules; + import net.minecraft.world.level.GameType; + import net.minecraft.world.level.block.entity.SkullBlockEntity; + import net.minecraft.world.level.storage.LevelStorageSource; ++import net.sparklypower.sparklypaper.configs.SparklyPaperConfigUtils; + import org.slf4j.Logger; + + // CraftBukkit start +@@ -226,6 +228,9 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + return false; + } + net.sparklypower.sparklypaper.SparklyPaperCommands.INSTANCE.registerCommands(this); ++ Util.SERVERLEVEL_TICK_EXECUTOR = new java.util.concurrent.ThreadPoolExecutor(SparklyPaperConfigUtils.config.getParallelWorldTicking().getThreads(), SparklyPaperConfigUtils.config.getParallelWorldTicking().getThreads(), 0L, TimeUnit.MILLISECONDS, new java.util.concurrent.LinkedBlockingQueue(), new net.sparklypower.sparklypaper.ServerLevelTickExecutorThreadFactory()); // SparklyPaper - parallel world ticking; // SparklyPaper - parallel world ticking ++ int serverLevelTickWorkerCount = Util.SERVERLEVEL_TICK_EXECUTOR.prestartAllCoreThreads(); // SparklyPaper - parallel world ticking ++ DedicatedServer.LOGGER.info("Using " + serverLevelTickWorkerCount + " threads for parallel world ticking"); // SparklyPaper - parallel world ticking + // SparklyPaper end + com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // load version history now + io.papermc.paper.brigadier.PaperBrigadierProviderImpl.INSTANCE.getClass(); // init PaperBrigadierProvider +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index 8c33a12ca879c46893150d6adfb8aa4d397c6b4c..7088c8e8a7eba566fa91f5fa2995cd724705b8c4 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -238,7 +238,7 @@ public class ServerChunkCache extends ChunkSource { + public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) { + long k = ChunkPos.asLong(x, z); + +- if (io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system ++ if (io.papermc.paper.util.TickThread.isTickThreadFor(level, x, z)) { // Paper - rewrite chunk system // SparklyPaper - parallel world ticking + return this.getChunkAtIfLoadedMainThread(x, z); + } + +@@ -265,7 +265,16 @@ public class ServerChunkCache extends ChunkSource { + @Override + public ChunkAccess getChunk(int x, int z, ChunkStatus leastStatus, boolean create) { + final int x1 = x; final int z1 = z; // Paper - conflict on variable change +- if (!io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(level, x, z)) { // Paper - rewrite chunk system // SparklyPaper - parallel world ticking ++ // SparklyPaper start - parallel world ticking ++ if (io.papermc.paper.util.TickThread.isServerLevelTickThread()) { ++ // Welp, we are going to crash, bye ++ net.minecraft.server.MinecraftServer.LOGGER.error("THE SERVER IS GOING TO CRASH! - Thread " + Thread.currentThread().getName() + " failed main thread check: Cannot query another world's (" + level.getWorld().getName() + ") chunk (" + x + ", " + z + ") in a ServerLevelTickThread - " + io.papermc.paper.util.TickThread.getTickThreadInformation(), new Throwable()); ++ if (io.papermc.paper.util.TickThread.HARD_THROW) ++ throw new IllegalStateException("Cannot query another world's (" + level.getWorld().getName() + ") chunk (" + x + ", " + z + ") in a ServerLevelTickThread"); ++ } ++ // SparklyPaper end ++ + return (ChunkAccess) CompletableFuture.supplyAsync(() -> { + return this.getChunk(x, z, leastStatus, create); + }, this.mainThreadProcessor).join(); +@@ -317,7 +326,7 @@ public class ServerChunkCache extends ChunkSource { + @Nullable + @Override + public LevelChunk getChunkNow(int chunkX, int chunkZ) { +- if (!io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system ++ if (!io.papermc.paper.util.TickThread.isTickThreadFor(level, chunkX, chunkZ)) { // Paper - rewrite chunk system // SparklyPaper - parallel world ticking + return null; + } else { + return this.getChunkAtIfLoadedMainThread(chunkX, chunkZ); // Paper - optimise for loaded chunks +@@ -331,7 +340,7 @@ public class ServerChunkCache extends ChunkSource { + } + + public CompletableFuture> getChunkFuture(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { +- boolean flag1 = io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system ++ boolean flag1 = io.papermc.paper.util.TickThread.isTickThreadFor(level, chunkX, chunkZ); // Paper - rewrite chunk system // SparklyPaper - parallel world ticking + CompletableFuture completablefuture; + + if (flag1) { +@@ -650,7 +659,7 @@ public class ServerChunkCache extends ChunkSource { + + if (true || this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { // Paper - optimise chunk tick iteration + this.level.tickChunk(chunk1, k); +- if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper ++ // if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // SparklyPaper - parallel world ticking (only run mid-tick at the end of each tick / fixes concurrency bugs related to executeMidTickTasks) // Paper + } + } + } +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 3fe14f5f135249ea9004589a86ed372aeb4667f8..49109a1c59f6b069ee636d0031754446b0cdc062 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -703,7 +703,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelDirectory.path().toFile()); + // CraftBukkit end + this.players = Lists.newArrayList(); +- this.entityTickList = new EntityTickList(); ++ this.entityTickList = new EntityTickList(this); + this.blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); + this.fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); + this.navigatingMobs = new ObjectOpenHashSet(); +@@ -1329,7 +1329,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (fluid1.is(fluid)) { + fluid1.tick(this, pos); + } +- MinecraftServer.getServer().executeMidTickTasks(); // Paper - exec chunk tasks during world tick ++ // MinecraftServer.getServer().executeMidTickTasks(); // SparklyPaper - parallel world ticking (only run mid-tick at the end of each tick / fixes concurrency bugs related to executeMidTickTasks) // Paper - exec chunk tasks during world tick + + } + +@@ -1339,7 +1339,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (iblockdata.is(block)) { + iblockdata.tick(this, pos, this.random); + } +- MinecraftServer.getServer().executeMidTickTasks(); // Paper - exec chunk tasks during world tick ++ // MinecraftServer.getServer().executeMidTickTasks(); // SparklyPaper - parallel world ticking (only run mid-tick at the end of each tick / fixes concurrency bugs related to executeMidTickTasks) // Paper - exec chunk tasks during world tick + + } + +@@ -1357,7 +1357,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + public void tickNonPassenger(Entity entity) { + // Paper start - log detailed entity tick information +- io.papermc.paper.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); ++ io.papermc.paper.util.TickThread.ensureTickThread(entity, "Cannot tick an entity off-main"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + try { + if (currentlyTickingEntity.get() == null) { + currentlyTickingEntity.lazySet(entity); +@@ -1645,6 +1645,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + private void addPlayer(ServerPlayer player) { ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot add player off-main"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + Entity entity = (Entity) this.getEntities().get(player.getUUID()); + + if (entity != null) { +@@ -1658,7 +1659,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + // CraftBukkit start + private boolean addEntity(Entity entity, CreatureSpawnEvent.SpawnReason spawnReason) { +- org.spigotmc.AsyncCatcher.catchOp("entity add"); // Spigot ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot add entity off-main"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + // Paper start + if (entity.valid) { + MinecraftServer.LOGGER.error("Attempted Double World add on " + entity, new Throwable()); +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index f71a4a8307fb092d33545e12d253e0b80c884168..e44f7734e677226ff8715134c81edad9520b5694 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -18,6 +18,7 @@ import java.util.Optional; + import java.util.OptionalInt; + import java.util.Set; + import javax.annotation.Nullable; ++ + import net.minecraft.BlockUtil; + import net.minecraft.ChatFormatting; + import net.minecraft.CrashReport; +@@ -95,7 +96,6 @@ import net.minecraft.util.Mth; + import net.minecraft.util.RandomSource; + import net.minecraft.util.Unit; + import net.minecraft.world.damagesource.DamageSource; +-import net.minecraft.world.damagesource.DamageSources; + import net.minecraft.world.effect.MobEffectInstance; + import net.minecraft.world.effect.MobEffects; + import net.minecraft.world.entity.Entity; +@@ -114,12 +114,7 @@ import net.minecraft.world.entity.player.Inventory; + import net.minecraft.world.entity.player.Player; + import net.minecraft.world.entity.projectile.AbstractArrow; + import net.minecraft.world.food.FoodData; +-import net.minecraft.world.inventory.AbstractContainerMenu; +-import net.minecraft.world.inventory.ContainerListener; +-import net.minecraft.world.inventory.ContainerSynchronizer; +-import net.minecraft.world.inventory.HorseInventoryMenu; +-import net.minecraft.world.inventory.ResultSlot; +-import net.minecraft.world.inventory.Slot; ++import net.minecraft.world.inventory.*; + import net.minecraft.world.item.ComplexItem; + import net.minecraft.world.item.ItemCooldowns; + import net.minecraft.world.item.ItemStack; +@@ -321,6 +316,7 @@ public class ServerPlayer extends Player { + // Paper start - optimise chunk tick iteration + public double lastEntitySpawnRadiusSquared = -1.0; + // Paper end - optimise chunk tick iteration ++ 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 world, GameProfile profile, ClientInformation clientOptions) { + super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile); +@@ -710,6 +706,7 @@ public class ServerPlayer extends Player { + + @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; +@@ -728,6 +725,7 @@ public class ServerPlayer extends Player { + containerUpdateDelay = this.level().paperConfig().tickRates.containerUpdate; + } + // Paper end ++ // SparklyPaper - parallel world ticking (debugging) + if (!this.level().isClientSide && this.containerMenu != this.inventoryMenu && (this.isImmobile() || !this.containerMenu.stillValid(this))) { // Paper - auto close while frozen + this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.CANT_USE); // Paper + this.containerMenu = this.inventoryMenu; +@@ -1182,6 +1180,7 @@ public class ServerPlayer extends Player { + ResourceKey resourcekey = worldserver1.getTypeKey(); // CraftBukkit + + if (resourcekey == LevelStem.END && worldserver != null && worldserver.getTypeKey() == LevelStem.OVERWORLD) { // CraftBukkit ++ io.papermc.paper.util.TickThread.ensureOnlyTickThread("Cannot change dimension of a player off-main, from world " + serverLevel().getWorld().getName() + " to world " + worldserver.getWorld().getName()); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + this.isChangingDimension = true; // CraftBukkit - Moved down from above + this.unRide(); + this.serverLevel().removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION); +@@ -1194,6 +1193,10 @@ public class ServerPlayer extends Player { + + return this; + } else { ++ if (worldserver != null) ++ io.papermc.paper.util.TickThread.ensureOnlyTickThread("Cannot change dimension of a player off-main, from world " + serverLevel().getWorld().getName() + " to world " + worldserver.getWorld().getName()); // SparklyPaper - parallel world ticking (additional concurrency issues logs) ++ else ++ io.papermc.paper.util.TickThread.ensureOnlyTickThread("Cannot change dimension of a player off-main, from world " + serverLevel().getWorld().getName() + " to unknown world"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + // CraftBukkit start + /* + WorldData worlddata = worldserver.getLevelData(); +@@ -1594,6 +1597,12 @@ public class ServerPlayer extends Player { + 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 " + container + " because we haven't ticked in the current world yet!", new Throwable()); ++ return OptionalInt.empty(); ++ } ++ // SparklyPaper end + this.containerMenu = container; + if (!this.isImmobile()) this.connection.send(new ClientboundOpenScreenPacket(container.containerId, container.getType(), Objects.requireNonNullElseGet(title, container::getTitle))); // Paper + // CraftBukkit end +@@ -1655,6 +1664,7 @@ public class ServerPlayer extends Player { + } + @Override + public void closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) { ++ MinecraftServer.LOGGER.warn("Closing " + this.getBukkitEntity().getName() + " inventory that was created at", this.containerMenu.containerCreationStacktrace); + CraftEventFactory.handleInventoryCloseEvent(this, reason); // CraftBukkit + // Paper end + this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId)); +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 33abcf12b4426572b74ca4c813e4392c823494bc..07198f2f8f7cb082c9e575a5c1e56c14aca31a87 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -111,7 +111,6 @@ import org.bukkit.Location; + import org.bukkit.craftbukkit.CraftServer; + import org.bukkit.craftbukkit.CraftWorld; + import org.bukkit.craftbukkit.entity.CraftPlayer; +-import org.bukkit.craftbukkit.util.CraftChatMessage; + import org.bukkit.craftbukkit.util.CraftLocation; + import org.bukkit.entity.Player; + import org.bukkit.event.player.PlayerChangedWorldEvent; +@@ -120,7 +119,6 @@ import org.bukkit.event.player.PlayerLoginEvent; + import org.bukkit.event.player.PlayerQuitEvent; + import org.bukkit.event.player.PlayerRespawnEvent; + import org.bukkit.event.player.PlayerRespawnEvent.RespawnReason; +-import org.bukkit.event.player.PlayerSpawnChangeEvent; + // CraftBukkit end + + public abstract class PlayerList { +@@ -136,7 +134,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; +@@ -157,7 +155,7 @@ public abstract class PlayerList { + + // CraftBukkit start + private 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 - Team name used for collideRule + + public PlayerList(MinecraftServer server, LayeredRegistryAccess registryManager, PlayerDataStorage saveHandler, int maxPlayers) { +@@ -181,6 +179,7 @@ public abstract class PlayerList { + abstract public void loadAndSaveFiles(); // Paper - moved from DedicatedPlayerList constructor + + public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData) { ++ io.papermc.paper.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 + GameProfile gameprofile = player.getGameProfile(); +@@ -820,6 +819,8 @@ public abstract class PlayerList { + } + + public ServerPlayer respawn(ServerPlayer entityplayer, ServerLevel worldserver, boolean flag, Location location, boolean avoidSuffocation, RespawnReason reason, org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag...respawnFlags) { ++ System.out.println("respawning player - current player container is " + entityplayer.containerMenu + " but their inventory is " + entityplayer.inventoryMenu); ++ io.papermc.paper.util.TickThread.ensureOnlyTickThread("Cannot respawn player off-main, from world " + entityplayer.serverLevel().getWorld().getName() + " to world " + worldserver.getWorld().getName()); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + // Paper end + entityplayer.stopRiding(); // CraftBukkit + this.players.remove(entityplayer); +@@ -844,6 +845,7 @@ public abstract class PlayerList { + ServerPlayer entityplayer1 = entityplayer; + org.bukkit.World fromWorld = entityplayer.getBukkitEntity().getWorld(); + entityplayer.wonGame = false; ++ entityplayer.hasTickedAtLeastOnceInNewWorld = false; // SparklyPaper - parallel world ticking (see: PARALLEL_NOTES.md - Opening an inventory after a world switch) + // CraftBukkit end + + entityplayer1.connection = entityplayer.connection; +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index c655c6fee393c62ba79301f76baa72f9b1154a9a..c0f0f135d1d941c1ab18319ee93db61fa7afa823 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -934,11 +934,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + // This will be called every single tick the entity is in lava, so don't throw an event + this.setSecondsOnFire(15, false); + } +- CraftEventFactory.blockDamage = (this.lastLavaContact) == null ? null : org.bukkit.craftbukkit.block.CraftBlock.at(this.level, this.lastLavaContact); ++ CraftEventFactory.blockDamageRT.set((this.lastLavaContact) == null ? null : org.bukkit.craftbukkit.block.CraftBlock.at(this.level, this.lastLavaContact)); // SparklyPaper - parallel world ticking + if (this.hurt(this.damageSources().lava(), 4.0F)) { + this.playSound(SoundEvents.GENERIC_BURN, 0.4F, 2.0F + this.random.nextFloat() * 0.4F); + } +- CraftEventFactory.blockDamage = null; ++ CraftEventFactory.blockDamageRT.set(null); // SparklyPaper - parallel world ticking + // CraftBukkit end - we also don't throw an event unless the object in lava is living, to save on some event calls + + } +@@ -3390,9 +3390,9 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + if (this.fireImmune()) { + return; + } +- CraftEventFactory.entityDamage = lightning; ++ CraftEventFactory.entityDamageRT.set(lightning); // SparklyPaper - parallel world ticking + if (!this.hurt(this.damageSources().lightningBolt(), 5.0F)) { +- CraftEventFactory.entityDamage = null; ++ CraftEventFactory.entityDamageRT.set(null); // SparklyPaper - parallel world ticking + return; + } + // CraftBukkit end +@@ -3903,6 +3903,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + this.teleportPassengers(); + this.setYHeadRot(yaw); + } else { ++ io.papermc.paper.util.TickThread.ensureTickThread(world, "Cannot teleport entity to another world off-main, from world " + level.getWorld().getName() + " to world " + world.getWorld().getName()); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + this.unRide(); + Entity entity = this.getType().create(world); + +diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +index 490472bb618e9ac07da5883a71dff8920525b1e2..73e4d849d151ec3adec80a96c1e6efbe35b5044a 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +@@ -341,9 +341,9 @@ public class Turtle extends Animal { + + @Override + public void thunderHit(ServerLevel world, LightningBolt lightning) { +- org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = lightning; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(lightning); // CraftBukkit // SparklyPaper - parallel world ticking + this.hurt(this.damageSources().lightningBolt(), Float.MAX_VALUE); +- org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = null; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // SparklyPaper - parallel world ticking + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java +index e6f75a9cac46c8e3ddba664a9d5b27b665a94cb4..07df7ce695452409b4ac132d8fb72bed2415b08e 100644 +--- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java ++++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java +@@ -295,9 +295,9 @@ public class FallingBlockEntity extends Entity { + float f2 = (float) Math.min(Mth.floor((float) i * this.fallDamagePerDistance), this.fallDamageMax); + + this.level().getEntities((Entity) this, this.getBoundingBox(), predicate).forEach((entity) -> { +- CraftEventFactory.entityDamage = this; // CraftBukkit ++ CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // SparklyPaper - parallel world ticking + entity.hurt(damagesource2, f2); +- CraftEventFactory.entityDamage = null; // CraftBukkit ++ CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // SparklyPaper - parallel world ticking + }); + boolean flag = this.blockState.is(BlockTags.ANVIL); + +diff --git a/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java b/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java +index bbdb82b319480b103df463cce3c1b8e3dd5857ec..46093cd5d70ad6a95b2407aa5d749a3790ccd92a 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java +@@ -129,9 +129,9 @@ public class EvokerFangs extends Entity implements TraceableEntity { + + if (target.isAlive() && !target.isInvulnerable() && target != entityliving1) { + if (entityliving1 == null) { +- org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = this; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // SparklyPaper - parallel world ticking + target.hurt(this.damageSources().magic(), 6.0F); +- org.bukkit.craftbukkit.event.CraftEventFactory.entityDamage = null; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // SparklyPaper - parallel world ticking + } else { + if (entityliving1.isAlliedTo((Entity) target)) { + return; +diff --git a/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java b/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java +index b2f08889139dc447f7071f1c81456035bf8de31e..f816f197df8f36c83d5fe5b6d677da91bac2c16f 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java +@@ -240,9 +240,9 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier { + + if (f > 0.0F) { + if (this.attachedToEntity != null) { +- CraftEventFactory.entityDamage = this; // CraftBukkit ++ CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // SparklyPaper - parallel world ticking + this.attachedToEntity.hurt(this.damageSources().fireworks(this, this.getOwner()), 5.0F + (float) (nbttaglist.size() * 2)); +- CraftEventFactory.entityDamage = null; // CraftBukkit ++ CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // SparklyPaper - parallel world ticking + } + + double d0 = 5.0D; +@@ -269,9 +269,9 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier { + if (flag) { + float f1 = f * (float) Math.sqrt((5.0D - (double) this.distanceTo(entityliving)) / 5.0D); + +- CraftEventFactory.entityDamage = this; // CraftBukkit ++ CraftEventFactory.entityDamageRT.set(this); // CraftBukkit // SparklyPaper - parallel world ticking + entityliving.hurt(this.damageSources().fireworks(this, this.getOwner()), f1); +- CraftEventFactory.entityDamage = null; // CraftBukkit ++ CraftEventFactory.entityDamageRT.set(null); // CraftBukkit // SparklyPaper - parallel world ticking + } + } + } +diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +index a90317100d32974e481e14476843f66997a2cf3a..981a73abdef08bf4a2de274abbccff35415f3a95 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +@@ -78,16 +78,19 @@ public abstract class Projectile extends Entity implements TraceableEntity { + } else if (this.ownerUUID != null && this.level() instanceof ServerLevel) { + this.cachedOwner = ((ServerLevel) this.level()).getEntity(this.ownerUUID); + // Paper start - check all worlds +- if (this.cachedOwner == null) { +- for (final ServerLevel level : this.level().getServer().getAllLevels()) { +- if (level == this.level()) continue; +- final Entity entity = level.getEntity(this.ownerUUID); +- if (entity != null) { +- this.cachedOwner = entity; +- break; +- } +- } +- } ++ // SparklyPaper start - parallel world ticking ++ // This is from the "MC-50319: Check other worlds for shooter of projectiles" patch, however, accessing the entities in other worlds will cause concurrency issues ++ // if (this.cachedOwner == null) { ++ // for (final ServerLevel level : this.level().getServer().getAllLevels()) { ++ // if (level == this.level()) continue; ++ // final Entity entity = level.getEntity(this.ownerUUID); ++ // if (entity != null) { ++ // this.cachedOwner = entity; ++ // break; ++ // } ++ // } ++ // } ++ // SparklyPaper end + // Paper end + this.refreshProjectileSource(false); // Paper + return this.cachedOwner; +diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +index f5db60cbecbe69941873e064315931089fe0e48a..6c4a1de4f2606439348dbdb620a1aff63b848028 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +@@ -82,9 +82,9 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + + entityplayer.connection.teleport(teleEvent.getTo()); + entity.resetFallDistance(); +- CraftEventFactory.entityDamage = this; ++ CraftEventFactory.entityDamageRT.set(this); // SparklyPaper - parallel world ticking + entity.hurt(this.damageSources().fall(), 5.0F); +- CraftEventFactory.entityDamage = null; ++ CraftEventFactory.entityDamageRT.set(null); // SparklyPaper - parallel world ticking + } + // CraftBukkit end + } +diff --git a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java +index f664da5a8413bb13cc95d2cf1604f11a5d285dae..42da66ca4fbb37cac1d8db20f8f4f1c83f8f5fd7 100644 +--- a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java +@@ -102,6 +102,7 @@ public abstract class AbstractContainerMenu { + this.title = title; + } + // CraftBukkit end ++ public Throwable containerCreationStacktrace = new Throwable(); // SparklyPaper - parallel world ticking (debugging) + + protected AbstractContainerMenu(@Nullable MenuType type, int syncId) { + this.carried = ItemStack.EMPTY; +diff --git a/src/main/java/net/minecraft/world/item/ArmorItem.java b/src/main/java/net/minecraft/world/item/ArmorItem.java +index 42d87800a328f71c5127ce5599ca4c71cc9bb1cd..466526dfe8f81379bccf640f2c3a70640c353540 100644 +--- a/src/main/java/net/minecraft/world/item/ArmorItem.java ++++ b/src/main/java/net/minecraft/world/item/ArmorItem.java +@@ -69,7 +69,7 @@ public class ArmorItem extends Item implements Equipable { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityliving.getBukkitEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + world.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java +index 4697df75fdee2023c41260bed211e3e3d90d2b9b..618a0a4d479c73fd0ca7e9d770ea01deb10c004a 100644 +--- a/src/main/java/net/minecraft/world/item/ItemStack.java ++++ b/src/main/java/net/minecraft/world/item/ItemStack.java +@@ -378,8 +378,8 @@ public final class ItemStack { + if (enuminteractionresult.consumesAction() && world.captureTreeGeneration && world.capturedBlockStates.size() > 0) { + world.captureTreeGeneration = false; + Location location = CraftLocation.toBukkit(blockposition, world.getWorld()); +- 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 blocks = new java.util.ArrayList<>(world.capturedBlockStates.values()); + world.capturedBlockStates.clear(); + StructureGrowEvent structureEvent = null; +diff --git a/src/main/java/net/minecraft/world/item/MinecartItem.java b/src/main/java/net/minecraft/world/item/MinecartItem.java +index a33395dc5a94d89b5ab273c7832813b6ff9ea3b7..31abddcd78672814de8d1e6289da67782675081a 100644 +--- a/src/main/java/net/minecraft/world/item/MinecartItem.java ++++ b/src/main/java/net/minecraft/world/item/MinecartItem.java +@@ -69,7 +69,7 @@ public class MinecartItem extends Item { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); + + BlockDispenseEvent event = new BlockDispenseEvent(block2, craftItem.clone(), new org.bukkit.util.Vector(d0, d1 + d3, d2)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + worldserver.getCraftServer().getPluginManager().callEvent(event); + } + +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index 45243249a561440512ef2a620c60b02e159c80e2..849b9b4336d2ac99324dacf6ad8a2e34e7e65797 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -566,7 +566,7 @@ public class Explosion { + continue; + } + +- CraftEventFactory.entityDamage = this.source; ++ CraftEventFactory.entityDamageRT.set(this.source); // SparklyPaper - parallel world ticking + entity.lastDamageCancelled = false; + + if (entity instanceof EnderDragon) { +@@ -582,7 +582,7 @@ public class Explosion { + entity.hurt(this.getDamageSource(), (float) ((int) ((d13 * d13 + d13) / 2.0D * 7.0D * (double) f2 + 1.0D))); + } + +- CraftEventFactory.entityDamage = null; ++ CraftEventFactory.entityDamageRT.set(null); // SparklyPaper - parallel world ticking + if (entity.lastDamageCancelled) { // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Skip entity if damage event was cancelled + continue; + } +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index b9e0822638a3979bd43392efdb595153e6f34675..28caabc36e7af64d3f546fc0e3c9c06bc5c8b78d 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -15,6 +15,8 @@ import java.util.function.Consumer; + import java.util.function.Predicate; + import java.util.function.Supplier; + import javax.annotation.Nullable; ++ ++import io.papermc.paper.util.TickThread; + import net.minecraft.CrashReport; + import net.minecraft.CrashReportCategory; + import net.minecraft.ReportedException; +@@ -827,7 +829,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public final LevelChunk getChunk(int chunkX, int chunkZ) { // Paper - final to help inline + // Paper start - make sure loaded chunks get the inlined variant of this function + net.minecraft.server.level.ServerChunkCache cps = ((ServerLevel)this).getChunkSource(); +- if (cps.mainThread == Thread.currentThread()) { ++ if (TickThread.isTickThreadFor((ServerLevel) this, chunkX, chunkZ)) { // SparklyPaper - parallel world ticking, let tick threads load chunks via the main thread + LevelChunk ifLoaded = cps.getChunkAtIfLoadedMainThread(chunkX, chunkZ); + if (ifLoaded != null) { + return ifLoaded; +@@ -911,6 +913,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + @Override + public boolean setBlock(BlockPos pos, BlockState state, int flags, int maxUpdateDepth) { ++ io.papermc.paper.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 +@@ -1292,7 +1295,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + tickingblockentity.tick(); + // Paper start - execute chunk tasks during tick + if ((this.tileTickPosition & 7) == 0) { +- MinecraftServer.getServer().executeMidTickTasks(); ++ // MinecraftServer.getServer().executeMidTickTasks(); // SparklyPaper - parallel world ticking (only run mid-tick at the end of each tick / fixes concurrency bugs related to executeMidTickTasks) + } + // Paper end - execute chunk tasks during tick + } +@@ -1309,7 +1312,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public void guardEntityTick(Consumer tickConsumer, T entity) { + try { + tickConsumer.accept(entity); +- MinecraftServer.getServer().executeMidTickTasks(); // Paper - execute chunk tasks mid tick ++ // MinecraftServer.getServer().executeMidTickTasks(); // SparklyPaper - parallel world ticking (only run mid-tick at the end of each tick / fixes concurrency bugs related to executeMidTickTasks) // Paper - execute chunk tasks mid tick + } catch (Throwable throwable) { + if (throwable instanceof ThreadDeath) throw throwable; // Paper + // Paper start - Prevent tile entity and entity crashes +@@ -1409,6 +1412,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + @Nullable + public BlockEntity getBlockEntity(BlockPos blockposition, boolean validate) { ++ io.papermc.paper.util.TickThread.ensureTickThreadOrAsyncThread((ServerLevel) this, "Cannot read world asynchronously"); // SparklyPaper start - parallel world ticking + // Paper start - Optimize capturedTileEntities lookup + net.minecraft.world.level.block.entity.BlockEntity blockEntity; + if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(blockposition)) != null) { +@@ -1416,10 +1420,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + // Paper end + // CraftBukkit end +- return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && !io.papermc.paper.util.TickThread.isTickThread() ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); // Paper - rewrite chunk system ++ return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && !io.papermc.paper.util.TickThread.isTickThread() ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); // Paper - rewrite chunk system // SparklyPaper - parallel world ticking + } + + public void setBlockEntity(BlockEntity blockEntity) { ++ io.papermc.paper.util.TickThread.ensureTickThread((ServerLevel) this, "Cannot modify world asynchronously"); // SparklyPaper start - parallel world ticking + BlockPos blockposition = blockEntity.getBlockPos(); + + if (!this.isOutsideBuildHeight(blockposition)) { +@@ -1505,6 +1510,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + @Override + public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { ++ io.papermc.paper.util.TickThread.ensureTickThread((ServerLevel)this, box, "Cannot getEntities asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + this.getProfiler().incrementCounter("getEntities"); + List list = Lists.newArrayList(); + ((ServerLevel)this).getEntityLookup().getEntities(except, box, list, predicate); // Paper - optimise this call +@@ -1769,8 +1775,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + public final BlockPos.MutableBlockPos getRandomBlockPosition(int x, int y, int z, int l, BlockPos.MutableBlockPos out) { + // Paper end +- this.randValue = this.randValue * 3 + 1013904223; +- int i1 = this.randValue >> 2; ++ int i1 = this.random.nextInt() >> 2; // SparklyPaper - parallel world ticking + + out.set(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); // Paper - change to setValues call + return out; // Paper +diff --git a/src/main/java/net/minecraft/world/level/block/CactusBlock.java b/src/main/java/net/minecraft/world/level/block/CactusBlock.java +index 0003fb51ae3a6575575e10b4c86719f3061e2577..0c5e0634852fd929f7f1c4373b0c7baebb07bc96 100644 +--- a/src/main/java/net/minecraft/world/level/block/CactusBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CactusBlock.java +@@ -115,9 +115,9 @@ public class CactusBlock extends Block { + @Override + public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper +- CraftEventFactory.blockDamage = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()); // CraftBukkit ++ CraftEventFactory.blockDamageRT.set(world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ())); // CraftBukkit // SparklyPaper - parallel world ticking + entity.hurt(world.damageSources().cactus(), 1.0F); +- CraftEventFactory.blockDamage = null; // CraftBukkit ++ CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // SparklyPaper - parallel world ticking + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java +index 7700461b8cd0bde1bf6c0d5e4b73184bed1adc4e..e55dd626d3ead2655894ddb9a25a31b661810533 100644 +--- a/src/main/java/net/minecraft/world/level/block/CampfireBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/CampfireBlock.java +@@ -95,9 +95,9 @@ public class CampfireBlock extends BaseEntityBlock implements SimpleWaterloggedB + public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + if (!new io.papermc.paper.event.entity.EntityInsideBlockEvent(entity.getBukkitEntity(), org.bukkit.craftbukkit.block.CraftBlock.at(world, pos)).callEvent()) { return; } // Paper + if ((Boolean) state.getValue(CampfireBlock.LIT) && entity instanceof LivingEntity && !EnchantmentHelper.hasFrostWalker((LivingEntity) entity)) { +- org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = CraftBlock.at(world, pos); // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // CraftBukkit // SparklyPaper - parallel world ticking + entity.hurt(world.damageSources().inFire(), (float) this.fireDamage); +- org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = null; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // SparklyPaper - parallel world ticking + } + + super.entityInside(state, world, pos, entity); +diff --git a/src/main/java/net/minecraft/world/level/block/DispenserBlock.java b/src/main/java/net/minecraft/world/level/block/DispenserBlock.java +index 9b1e51c1d95da885c80c6d05000d83436b7bcfb4..720b86021fd94cb68440eafebab4ea5d6e380c5f 100644 +--- a/src/main/java/net/minecraft/world/level/block/DispenserBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/DispenserBlock.java +@@ -48,7 +48,8 @@ public class DispenserBlock extends BaseEntityBlock { + object2objectopenhashmap.defaultReturnValue(new DefaultDispenseItemBehavior()); + }); + private static final int TRIGGER_DURATION = 4; +- public static boolean eventFired = false; // CraftBukkit ++ // public static boolean eventFired = false; // CraftBukkit // SparklyPaper - parallel world ticking ++ public static ThreadLocal eventFired = ThreadLocal.withInitial(() -> Boolean.FALSE); // SparklyPaper - parallel world ticking + + public static void registerBehavior(ItemLike provider, DispenseItemBehavior behavior) { + DispenserBlock.DISPENSER_REGISTRY.put(provider.asItem(), behavior); +@@ -99,7 +100,7 @@ public class DispenserBlock extends BaseEntityBlock { + + if (idispensebehavior != DispenseItemBehavior.NOOP) { + if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockPreDispenseEvent(world, pos, itemstack, i)) return; // Paper - BlockPreDispenseEvent is called here +- DispenserBlock.eventFired = false; // CraftBukkit - reset event status ++ DispenserBlock.eventFired.set(Boolean.FALSE); // CraftBukkit - reset event status // SparklyPaper - parallel world ticking + tileentitydispenser.setItem(i, idispensebehavior.dispense(sourceblock, itemstack)); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/FungusBlock.java b/src/main/java/net/minecraft/world/level/block/FungusBlock.java +index 17e9e2efc78cfe8577dbf4e1d6096543ad8b8ac4..0d56dff0ccb86bc62b7752489f4b5d3fa4f1b913 100644 +--- a/src/main/java/net/minecraft/world/level/block/FungusBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/FungusBlock.java +@@ -61,9 +61,9 @@ public class FungusBlock extends BushBlock implements BonemealableBlock { + this.getFeature(world).ifPresent((holder) -> { + // CraftBukkit start + 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 + } + // CraftBukkit end + ((ConfiguredFeature) holder.value()).place(world, world.getChunkSource().getGenerator(), random, pos); +diff --git a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java +index 1b766045687e4dcded5cbcc50b746c55b9a34e22..50e3ee93ed989a9a16f6d652a825abad488bb0b0 100644 +--- a/src/main/java/net/minecraft/world/level/block/MagmaBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/MagmaBlock.java +@@ -23,9 +23,9 @@ public class MagmaBlock extends Block { + @Override + public void stepOn(Level world, BlockPos pos, BlockState state, Entity entity) { + if (!entity.isSteppingCarefully() && entity instanceof LivingEntity && !EnchantmentHelper.hasFrostWalker((LivingEntity) entity)) { +- org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ()); // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ())); // CraftBukkit // SparklyPaper - parallel world ticking + entity.hurt(world.damageSources().hotFloor(), 1.0F); +- org.bukkit.craftbukkit.event.CraftEventFactory.blockDamage = null; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // SparklyPaper - parallel world ticking + } + + super.stepOn(world, pos, state, entity); +diff --git a/src/main/java/net/minecraft/world/level/block/MushroomBlock.java b/src/main/java/net/minecraft/world/level/block/MushroomBlock.java +index 302c5a6401facf192677b89cc0e9190bb35b1229..6d4f5383d30893803945803384d4fdd7e1d17a51 100644 +--- a/src/main/java/net/minecraft/world/level/block/MushroomBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/MushroomBlock.java +@@ -93,7 +93,7 @@ public class MushroomBlock extends BushBlock implements BonemealableBlock { + return false; + } else { + world.removeBlock(pos, false); +- SaplingBlock.treeType = (this == Blocks.BROWN_MUSHROOM) ? TreeType.BROWN_MUSHROOM : TreeType.RED_MUSHROOM; // CraftBukkit // Paper ++ SaplingBlock.treeTypeRT.set((this == Blocks.BROWN_MUSHROOM) ? TreeType.BROWN_MUSHROOM : TreeType.RED_MUSHROOM); // CraftBukkit // Paper // SparklyPaper - parallel world ticking + if (((ConfiguredFeature) ((Holder) optional.get()).value()).place(world, world.getChunkSource().getGenerator(), random, pos)) { + return true; + } else { +diff --git a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java +index cd943997f11f5ea5c600fdc6db96043fb0fa713c..fdfdac600fab86822cb9d1196b43424ad3fcf92a 100644 +--- a/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/PointedDripstoneBlock.java +@@ -141,9 +141,9 @@ public class PointedDripstoneBlock extends Block implements Fallable, SimpleWate + @Override + public void fallOn(Level world, BlockState state, BlockPos pos, Entity entity, float fallDistance) { + if (state.getValue(PointedDripstoneBlock.TIP_DIRECTION) == Direction.UP && state.getValue(PointedDripstoneBlock.THICKNESS) == DripstoneThickness.TIP) { +- CraftEventFactory.blockDamage = CraftBlock.at(world, pos); // CraftBukkit ++ CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // CraftBukkit // SparklyPaper - parallel world ticking + entity.causeFallDamage(fallDistance + 2.0F, 2.0F, world.damageSources().stalagmite()); +- CraftEventFactory.blockDamage = null; // CraftBukkit ++ CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // SparklyPaper - parallel world ticking + } else { + super.fallOn(world, state, pos, entity, fallDistance); + } +diff --git a/src/main/java/net/minecraft/world/level/block/SaplingBlock.java b/src/main/java/net/minecraft/world/level/block/SaplingBlock.java +index 53ac4e618fec3fe384d8a106c521f3eace0b5b35..b29df252c1b039004f40a8cf9aefb8387f29d7e3 100644 +--- a/src/main/java/net/minecraft/world/level/block/SaplingBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SaplingBlock.java +@@ -27,7 +27,7 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock { + protected static final float AABB_OFFSET = 6.0F; + protected static final VoxelShape SHAPE = Block.box(2.0D, 0.0D, 2.0D, 14.0D, 12.0D, 14.0D); + private final AbstractTreeGrower treeGrower; +- public static TreeType treeType; // CraftBukkit ++ public static final ThreadLocal treeTypeRT = new ThreadLocal<>(); // CraftBukkit // SparklyPaper - parallel world ticking (from Folia) + + protected SaplingBlock(AbstractTreeGrower generator, BlockBehaviour.Properties settings) { + super(settings); +@@ -60,8 +60,8 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock { + this.treeGrower.growTree(world, world.getChunkSource().getGenerator(), pos, state, random); + world.captureTreeGeneration = false; + if (world.capturedBlockStates.size() > 0) { +- TreeType treeType = SaplingBlock.treeType; +- SaplingBlock.treeType = null; ++ TreeType treeType = SaplingBlock.treeTypeRT.get(); // SparklyPaper - parallel world ticking ++ SaplingBlock.treeTypeRT.set(null); // SparklyPaper - parallel world ticking + Location location = CraftLocation.toBukkit(pos, world.getWorld()); + java.util.List blocks = new java.util.ArrayList<>(world.capturedBlockStates.values()); + world.capturedBlockStates.clear(); +diff --git a/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java b/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java +index 34eb7ba1adb51e394bf46a6f643db3529626d9ec..d48945137f717062e5233c10e978e5134edebf19 100644 +--- a/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SweetBerryBushBlock.java +@@ -85,9 +85,9 @@ public class SweetBerryBushBlock extends BushBlock implements BonemealableBlock + double d1 = Math.abs(entity.getZ() - entity.zOld); + + if (d0 >= 0.003000000026077032D || d1 >= 0.003000000026077032D) { +- CraftEventFactory.blockDamage = CraftBlock.at(world, pos); // CraftBukkit ++ CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // CraftBukkit // SparklyPaper - parallel world ticking + entity.hurt(world.damageSources().sweetBerryBush(), 1.0F); +- CraftEventFactory.blockDamage = null; // CraftBukkit ++ CraftEventFactory.blockDamageRT.set(null); // CraftBukkit // SparklyPaper - parallel world ticking + } + } + +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java +index c134d089e55ea2ffb180f92aea020bd7647259c9..a7e1e05fdcb380de71b1bac5f7b5daca3dee2ac9 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java +@@ -78,6 +78,12 @@ public abstract class BaseContainerBlockEntity extends BlockEntity implements Co + return canUnlock(player, lock, containerName, null); + } + public static boolean canUnlock(Player player, LockCode lock, Component containerName, @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.serverLevel()) { ++ 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(containerName)); +diff --git a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +index 963a596154091b79ca139af6274aa323518ad1ad..2817225e5efc97d24e54800689cdda0f53baa616 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +@@ -235,11 +235,11 @@ public class ConduitBlockEntity extends BlockEntity { + + if (blockEntity.destroyTarget != null) { + // CraftBukkit start +- CraftEventFactory.blockDamage = CraftBlock.at(world, pos); ++ CraftEventFactory.blockDamageRT.set(CraftBlock.at(world, pos)); // SparklyPaper - parallel world ticking + if (blockEntity.destroyTarget.hurt(world.damageSources().magic(), 4.0F)) { + world.playSound((Player) null, blockEntity.destroyTarget.getX(), blockEntity.destroyTarget.getY(), blockEntity.destroyTarget.getZ(), SoundEvents.CONDUIT_ATTACK_TARGET, SoundSource.BLOCKS, 1.0F, 1.0F); + } +- CraftEventFactory.blockDamage = null; ++ CraftEventFactory.blockDamageRT.set(null); // SparklyPaper - parallel world ticking + // CraftBukkit end + } + +diff --git a/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +index 65112ec3a6ea1c27f032477720ae74395523012b..41ed21bbb7762e52cd800846f546277c19ecd67e 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +@@ -43,9 +43,9 @@ public class SculkCatalystBlockEntity extends BlockEntity implements GameEventLi + // Paper end + + public static void serverTick(Level world, BlockPos pos, BlockState state, SculkCatalystBlockEntity blockEntity) { +- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = blockEntity.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. ++ org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(blockEntity.getBlockPos()); // SparklyPaper - parallel world ticking // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. + blockEntity.catalystListener.getSculkSpreader().updateCursors(world, pos, world.getRandom(), true); +- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = null; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(null); // SparklyPaper - parallel world ticking // CraftBukkit + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/grower/AbstractTreeGrower.java b/src/main/java/net/minecraft/world/level/block/grower/AbstractTreeGrower.java +index a743f36f2682a6b72ffa6644782fc081d1479eb7..85e7da9884f48989a62a789306b701a3dc1a3b0d 100644 +--- a/src/main/java/net/minecraft/world/level/block/grower/AbstractTreeGrower.java ++++ b/src/main/java/net/minecraft/world/level/block/grower/AbstractTreeGrower.java +@@ -75,51 +75,53 @@ public abstract class AbstractTreeGrower { + // CraftBukkit start + protected void setTreeType(Holder> holder) { + ResourceKey> worldgentreeabstract = holder.unwrapKey().get(); ++ TreeType treeType; // SparklyPaper - parallel world ticking + if (worldgentreeabstract == TreeFeatures.OAK || worldgentreeabstract == TreeFeatures.OAK_BEES_005) { +- SaplingBlock.treeType = TreeType.TREE; ++ treeType = TreeType.TREE; + } else if (worldgentreeabstract == TreeFeatures.HUGE_RED_MUSHROOM) { +- SaplingBlock.treeType = TreeType.RED_MUSHROOM; ++ treeType = TreeType.RED_MUSHROOM; + } else if (worldgentreeabstract == TreeFeatures.HUGE_BROWN_MUSHROOM) { +- SaplingBlock.treeType = TreeType.BROWN_MUSHROOM; ++ treeType = TreeType.BROWN_MUSHROOM; + } else if (worldgentreeabstract == TreeFeatures.JUNGLE_TREE) { +- SaplingBlock.treeType = TreeType.COCOA_TREE; ++ treeType = TreeType.COCOA_TREE; + } else if (worldgentreeabstract == TreeFeatures.JUNGLE_TREE_NO_VINE) { +- SaplingBlock.treeType = TreeType.SMALL_JUNGLE; ++ treeType = TreeType.SMALL_JUNGLE; + } else if (worldgentreeabstract == TreeFeatures.PINE) { +- SaplingBlock.treeType = TreeType.TALL_REDWOOD; ++ treeType = TreeType.TALL_REDWOOD; + } else if (worldgentreeabstract == TreeFeatures.SPRUCE) { +- SaplingBlock.treeType = TreeType.REDWOOD; ++ treeType = TreeType.REDWOOD; + } else if (worldgentreeabstract == TreeFeatures.ACACIA) { +- SaplingBlock.treeType = TreeType.ACACIA; ++ treeType = TreeType.ACACIA; + } else if (worldgentreeabstract == TreeFeatures.BIRCH || worldgentreeabstract == TreeFeatures.BIRCH_BEES_005) { +- SaplingBlock.treeType = TreeType.BIRCH; ++ treeType = TreeType.BIRCH; + } else if (worldgentreeabstract == TreeFeatures.SUPER_BIRCH_BEES_0002) { +- SaplingBlock.treeType = TreeType.TALL_BIRCH; ++ treeType = TreeType.TALL_BIRCH; + } else if (worldgentreeabstract == TreeFeatures.SWAMP_OAK) { +- SaplingBlock.treeType = TreeType.SWAMP; ++ treeType = TreeType.SWAMP; + } else if (worldgentreeabstract == TreeFeatures.FANCY_OAK || worldgentreeabstract == TreeFeatures.FANCY_OAK_BEES_005) { +- SaplingBlock.treeType = TreeType.BIG_TREE; ++ treeType = TreeType.BIG_TREE; + } else if (worldgentreeabstract == TreeFeatures.JUNGLE_BUSH) { +- SaplingBlock.treeType = TreeType.JUNGLE_BUSH; ++ treeType = TreeType.JUNGLE_BUSH; + } else if (worldgentreeabstract == TreeFeatures.DARK_OAK) { +- SaplingBlock.treeType = TreeType.DARK_OAK; ++ treeType = TreeType.DARK_OAK; + } else if (worldgentreeabstract == TreeFeatures.MEGA_SPRUCE) { +- SaplingBlock.treeType = TreeType.MEGA_REDWOOD; ++ treeType = TreeType.MEGA_REDWOOD; + } else if (worldgentreeabstract == TreeFeatures.MEGA_PINE) { +- SaplingBlock.treeType = TreeType.MEGA_REDWOOD; ++ treeType = TreeType.MEGA_REDWOOD; + } else if (worldgentreeabstract == TreeFeatures.MEGA_JUNGLE_TREE) { +- SaplingBlock.treeType = TreeType.JUNGLE; ++ treeType = TreeType.JUNGLE; + } else if (worldgentreeabstract == TreeFeatures.AZALEA_TREE) { +- SaplingBlock.treeType = TreeType.AZALEA; ++ treeType = TreeType.AZALEA; + } else if (worldgentreeabstract == TreeFeatures.MANGROVE) { +- SaplingBlock.treeType = TreeType.MANGROVE; ++ treeType = TreeType.MANGROVE; + } else if (worldgentreeabstract == TreeFeatures.TALL_MANGROVE) { +- SaplingBlock.treeType = TreeType.TALL_MANGROVE; ++ treeType = TreeType.TALL_MANGROVE; + } else if (worldgentreeabstract == TreeFeatures.CHERRY || worldgentreeabstract == TreeFeatures.CHERRY_BEES_005) { +- SaplingBlock.treeType = TreeType.CHERRY; ++ treeType = TreeType.CHERRY; + } else { + throw new IllegalArgumentException("Unknown tree generator " + worldgentreeabstract); + } ++ SaplingBlock.treeTypeRT.set(treeType); // SparklyPaper - parallel world ticking + } + // CraftBukkit end + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index fa170cc1ce7011d201295b89718292d696c7fc24..59045493537533f60bb5e3b80633c71bafb25dc0 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -412,6 +412,7 @@ public class LevelChunk extends ChunkAccess { + + @Nullable + public BlockState setBlockState(BlockPos blockposition, BlockState iblockdata, boolean flag, boolean doPlace) { ++ io.papermc.paper.util.TickThread.ensureTickThread(this.level, blockposition, "Updating block asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + // CraftBukkit end + int i = blockposition.getY(); + LevelChunkSection chunksection = this.getSection(this.getSectionIndex(i)); +diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java +index 4cdfc433df67afcd455422e9baf56f167dd712ae..f52b3740bd48f8527a36d48a0454e7d601763985 100644 +--- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java ++++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java +@@ -9,6 +9,13 @@ import net.minecraft.world.entity.Entity; + + public class EntityTickList { + private final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet entities = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(true); // Paper - rewrite this, always keep this updated - why would we EVER tick an entity that's not ticking? ++ // 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 - replace with better logic, do not delay removals +@@ -16,13 +23,13 @@ public class EntityTickList { + } + + public void add(Entity entity) { +- io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist addition"); // Paper ++ io.papermc.paper.util.TickThread.ensureTickThread(entity, "Asynchronous entity ticklist addition"); // Paper // SparklyPaper - parallel world ticking (additional concurrency issues logs) + this.ensureActiveIsNotIterated(); + this.entities.add(entity); // Paper - replace with better logic, do not delay removals/additions + } + + public void remove(Entity entity) { +- io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist removal"); // Paper ++ io.papermc.paper.util.TickThread.ensureTickThread(entity, "Asynchronous entity ticklist removal"); // Paper // SparklyPaper - parallel world ticking (additional concurrency issues logs) + this.ensureActiveIsNotIterated(); + this.entities.remove(entity); // Paper - replace with better logic, do not delay removals/additions + } +@@ -32,7 +39,7 @@ public class EntityTickList { + } + + public void forEach(Consumer action) { +- io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist iteration"); // Paper ++ io.papermc.paper.util.TickThread.ensureTickThread(serverLevel, "Asynchronous entity ticklist iteration"); // Paper // SparklyPaper - parallel world ticking (additional concurrency issues logs) + // Paper start - replace with better logic, do not delay removals/additions + // 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/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index c3060d1d4d0caf369c6ab516cb424f45eb851019..80cc42ea129a796a3e1189d9f840ec8180b92229 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -426,7 +426,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + } + + private boolean unloadChunk0(int x, int z, boolean save) { +- org.spigotmc.AsyncCatcher.catchOp("chunk unload"); // Spigot ++ io.papermc.paper.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; + } +@@ -441,7 +441,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean regenerateChunk(int x, int z) { +- org.spigotmc.AsyncCatcher.catchOp("chunk regenerate"); // Spigot ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, x, z, "Cannot regenerate chunk asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + warnUnsafeChunk("regenerating a faraway chunk", x, z); // Paper + // Paper start - implement regenerateChunk method + final ServerLevel serverLevel = this.world; +@@ -502,6 +502,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean refreshChunk(int x, int z) { ++ io.papermc.paper.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; + +@@ -537,7 +538,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean loadChunk(int x, int z, boolean generate) { +- org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot ++ io.papermc.paper.util.TickThread.ensureTickThread(this.getHandle(), x, z, "May not sync load chunks asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + warnUnsafeChunk("loading a faraway chunk", x, z); // Paper + // Paper start - Optimize this method + ChunkPos chunkPos = new ChunkPos(x, z); +@@ -800,6 +801,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean generateTree(Location loc, TreeType type, BlockChangeDelegate delegate) { ++ io.papermc.paper.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); +@@ -910,11 +912,13 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean createExplosion(double x, double y, double z, float power, boolean setFire, boolean breakBlocks, Entity source) { ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, x, z, "Cannot create explosion asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + return !this.world.explode(source == null ? null : ((CraftEntity) source).getHandle(), x, y, z, power, setFire, breakBlocks ? net.minecraft.world.level.Level.ExplosionInteraction.MOB : net.minecraft.world.level.Level.ExplosionInteraction.NONE).wasCanceled; + } + // Paper start + @Override + public boolean createExplosion(Entity source, Location loc, float power, boolean setFire, boolean breakBlocks) { ++ io.papermc.paper.util.TickThread.ensureTickThread(world, loc.getX(), loc.getZ(), "Cannot create explosion asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + return !world.explode(source != null ? ((org.bukkit.craftbukkit.entity.CraftEntity) source).getHandle() : null, loc.getX(), loc.getY(), loc.getZ(), power, setFire, breakBlocks ? net.minecraft.world.level.Level.ExplosionInteraction.MOB : net.minecraft.world.level.Level.ExplosionInteraction.NONE).wasCanceled; + } + // Paper end +@@ -984,6 +988,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public int getHighestBlockYAt(int x, int z, org.bukkit.HeightMap heightMap) { ++ io.papermc.paper.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); +@@ -1014,6 +1019,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + public void setBiome(int x, int y, int z, Holder bb) { + BlockPos pos = new BlockPos(x, 0, z); ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, pos, "Cannot retrieve chunk asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + if (this.world.hasChunkAt(pos)) { + net.minecraft.world.level.chunk.LevelChunk chunk = this.world.getChunkAt(pos); + +@@ -2272,6 +2278,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void sendGameEvent(Entity sourceEntity, org.bukkit.GameEvent gameEvent, Vector position) { ++ io.papermc.paper.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())), org.bukkit.craftbukkit.util.CraftVector.toBlockPos(position)); + } + // Paper end +@@ -2426,7 +2433,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + // Paper start + public java.util.concurrent.CompletableFuture getChunkAtAsync(int x, int z, boolean gen, boolean urgent) { + warnUnsafeChunk("getting a faraway chunk async", x, z); // Paper +- if (Bukkit.isPrimaryThread()) { ++ if (io.papermc.paper.util.TickThread.isTickThreadFor(this.getHandle(), x, z)) { // SparklyPaper - parallel world ticking + net.minecraft.world.level.chunk.LevelChunk immediate = this.world.getChunkSource().getChunkAtIfLoadedImmediately(x, z); + if (immediate != null) { + return java.util.concurrent.CompletableFuture.completedFuture(new CraftChunk(immediate)); +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +index bec8e6b62dba2bd0e4e85a7d1fb51287384f1290..f650163cab8c54b97a7dac7c79320dae733e3411 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +@@ -74,6 +74,11 @@ public class CraftBlock implements Block { + } + + public net.minecraft.world.level.block.state.BlockState getNMS() { ++ // Folia start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // Folia end - parallel world ticking + return this.world.getBlockState(this.position); + } + +@@ -150,6 +155,11 @@ public class CraftBlock implements Block { + } + + private void setData(final byte data, int flag) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + this.world.setBlock(this.position, CraftMagicNumbers.getBlock(this.getType(), data), flag); + } + +@@ -191,6 +201,12 @@ public class CraftBlock implements Block { + } + + public static boolean setTypeAndData(LevelAccessor world, BlockPos position, net.minecraft.world.level.block.state.BlockState old, net.minecraft.world.level.block.state.BlockState blockData, boolean applyPhysics) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "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 tile entity cleanup + if (old.hasBlockEntity() && blockData.getBlock() != old.getBlock()) { // SPIGOT-3725 remove old tile entity if block changes + // SPIGOT-4612: faster - just clear tile +@@ -336,18 +352,33 @@ public class CraftBlock implements Block { + + @Override + public Biome getBiome() { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ io.papermc.paper.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) { ++ io.papermc.paper.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) { ++ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + this.getWorld().setBiome(this.getX(), this.getY(), this.getZ(), bio); + } + +@@ -395,6 +426,11 @@ public class CraftBlock implements Block { + + @Override + public boolean isBlockFaceIndirectlyPowered(BlockFace face) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + int power = this.world.getMinecraftWorld().getSignal(this.position, CraftBlock.blockFaceToNotch(face)); + + Block relative = this.getRelative(face); +@@ -407,6 +443,11 @@ public class CraftBlock implements Block { + + @Override + public int getBlockPower(BlockFace face) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ io.papermc.paper.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(); +@@ -477,6 +518,11 @@ public class CraftBlock implements Block { + + @Override + public boolean breakNaturally() { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + return this.breakNaturally(null); + } + +@@ -536,6 +582,11 @@ public class CraftBlock implements Block { + + @Override + public boolean applyBoneMeal(BlockFace face) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ io.papermc.paper.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(); +@@ -547,8 +598,8 @@ public class CraftBlock implements Block { + world.captureTreeGeneration = false; + + if (world.capturedBlockStates.size() > 0) { +- 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 blocks = new ArrayList<>(world.capturedBlockStates.values()); + world.capturedBlockStates.clear(); + StructureGrowEvent structureEvent = null; +@@ -637,6 +688,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) { ++ io.papermc.paper.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(); +@@ -678,6 +734,11 @@ public class CraftBlock implements Block { + + @Override + public boolean canPlace(BlockData data) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ io.papermc.paper.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(); +@@ -712,6 +773,11 @@ public class CraftBlock implements Block { + + @Override + public void tick() { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + net.minecraft.world.level.block.state.BlockState blockData = this.getNMS(); + net.minecraft.server.level.ServerLevel level = this.world.getMinecraftWorld(); + +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java +index 390e1b7fd2721b99cb3ce268c6bc1bf0a38e08a3..9255e51954bd9a43afc366d8c414dd8af7571525 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java +@@ -210,6 +210,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) { ++ io.papermc.paper.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking ++ + if (block.getType() != this.getType()) { + if (!force) { + return false; +@@ -350,6 +356,7 @@ public class CraftBlockState implements BlockState { + + @Override + public java.util.Collection getDrops(org.bukkit.inventory.ItemStack item, org.bukkit.entity.Entity entity) { ++ io.papermc.paper.util.TickThread.ensureTickThread(world.getHandle(), position, "Cannot modify world asynchronously"); // SparklyPaper - parallel world ticking + net.minecraft.world.item.ItemStack nms = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(item); + + // Modelled off EntityHuman#hasBlock +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index 5dc160b743534665c6b3efb10b10f7c36e2da5ab..8942bb585e1f4a0b747194ef2ad91acc5de82d8b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -246,8 +246,8 @@ import org.bukkit.potion.PotionEffect; + import org.bukkit.util.Vector; + + public class CraftEventFactory { +- public static org.bukkit.block.Block blockDamage; // For use in EntityDamageByBlockEvent +- public static Entity entityDamage; // For use in EntityDamageByEntityEvent ++ public static final ThreadLocal blockDamageRT = new ThreadLocal<>(); // For use in EntityDamageByBlockEvent // SparklyPaper - parallel world ticking (this is from Folia, fixes concurrency bugs / For use in EntityDamageByEntityEvent) ++ public static final ThreadLocal entityDamageRT = new ThreadLocal<>(); // For use in EntityDamageByEntityEvent // SparklyPaper - parallel world ticking (this is from Folia, fixes concurrency bugs / For use in EntityDamageByEntityEvent) + + // helper methods + private static boolean canBuild(ServerLevel world, Player player, int x, int z) { +@@ -920,7 +920,7 @@ public class CraftEventFactory { + return CraftEventFactory.handleBlockSpreadEvent(world, source, target, block, 2); + } + +- public static BlockPos sourceBlockOverride = null; // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition 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 block, int flag) { + // Suppress during worldgen +@@ -932,7 +932,7 @@ public class CraftEventFactory { + CraftBlockState state = CraftBlockStates.getBlockState(world, target, flag); + state.setData(block); + +- BlockSpreadEvent event = new BlockSpreadEvent(state.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverride != null ? CraftEventFactory.sourceBlockOverride : source), state); ++ BlockSpreadEvent event = new BlockSpreadEvent(state.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverrideRT.get() != null ? CraftEventFactory.sourceBlockOverrideRT.get() : source), state); // SparklyPaper - parallel world ticking + Bukkit.getPluginManager().callEvent(event); + + if (!event.isCancelled()) { +@@ -1047,8 +1047,8 @@ public class CraftEventFactory { + private static EntityDamageEvent handleEntityDamageEvent(Entity entity, DamageSource source, Map modifiers, Map> modifierFunctions, boolean cancelled) { + if (source.is(DamageTypeTags.IS_EXPLOSION)) { + DamageCause damageCause; +- Entity damager = CraftEventFactory.entityDamage; +- CraftEventFactory.entityDamage = null; ++ Entity damager = CraftEventFactory.entityDamageRT.get(); // SparklyPaper - parallel world ticking ++ CraftEventFactory.entityDamageRT.set(null); // SparklyPaper - parallel world ticking + EntityDamageEvent event; + if (damager == null) { + event = new EntityDamageByBlockEvent(null, entity.getBukkitEntity(), DamageCause.BLOCK_EXPLOSION, modifiers, modifierFunctions, source.explodedBlockState); // Paper - handle block state in damage +@@ -1109,13 +1109,13 @@ public class CraftEventFactory { + } + return event; + } else if (source.is(DamageTypes.LAVA)) { +- EntityDamageEvent event = (new EntityDamageByBlockEvent(CraftEventFactory.blockDamage, entity.getBukkitEntity(), DamageCause.LAVA, modifiers, modifierFunctions)); ++ EntityDamageEvent event = (new EntityDamageByBlockEvent(CraftEventFactory.blockDamageRT.get(), entity.getBukkitEntity(), DamageCause.LAVA, modifiers, modifierFunctions)); // SparklyPaper - parallel world ticking + event.setCancelled(cancelled); + +- Block damager = CraftEventFactory.blockDamage; +- CraftEventFactory.blockDamage = null; // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call ++ Block damager = CraftEventFactory.blockDamageRT.get(); // SparklyPaper - parallel world ticking ++ CraftEventFactory.blockDamageRT.set(null); // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call // SparklyPaper - parallel world ticking + CraftEventFactory.callEvent(event); +- CraftEventFactory.blockDamage = damager; // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause ++ CraftEventFactory.blockDamageRT.set(damager); // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause // SparklyPaper - parallel world ticking + + if (!event.isCancelled()) { + event.getEntity().setLastDamageCause(event); +@@ -1123,9 +1123,9 @@ public class CraftEventFactory { + entity.lastDamageCancelled = true; // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Keep track if the event was canceled + } + return event; +- } else if (CraftEventFactory.blockDamage != null) { ++ } else if (CraftEventFactory.blockDamageRT.get() != null) { // SparklyPaper - parallel world ticking + DamageCause cause = null; +- Block damager = CraftEventFactory.blockDamage; ++ Block damager = CraftEventFactory.blockDamageRT.get(); // SparklyPaper - parallel world ticking + if (source.is(DamageTypes.CACTUS) || source.is(DamageTypes.SWEET_BERRY_BUSH) || source.is(DamageTypes.STALAGMITE) || source.is(DamageTypes.FALLING_STALACTITE) || source.is(DamageTypes.FALLING_ANVIL)) { + cause = DamageCause.CONTACT; + } else if (source.is(DamageTypes.HOT_FLOOR)) { +@@ -1140,9 +1140,9 @@ public class CraftEventFactory { + EntityDamageEvent event = new EntityDamageByBlockEvent(damager, entity.getBukkitEntity(), cause, modifiers, modifierFunctions); + event.setCancelled(cancelled); + +- CraftEventFactory.blockDamage = null; // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call ++ CraftEventFactory.blockDamageRT.set(null); // SPIGOT-6639: Clear blockDamage to allow other entity damage during event call // SparklyPaper - parallel world ticking + CraftEventFactory.callEvent(event); +- CraftEventFactory.blockDamage = damager; // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause ++ CraftEventFactory.blockDamageRT.set(damager); // SPIGOT-6639: Re-set blockDamage so that other entities which are also getting damaged have the right cause // SparklyPaper - parallel world ticking + + if (!event.isCancelled()) { + event.getEntity().setLastDamageCause(event); +@@ -1150,10 +1150,10 @@ public class CraftEventFactory { + entity.lastDamageCancelled = true; // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Keep track if the event was canceled + } + return event; +- } else if (CraftEventFactory.entityDamage != null) { ++ } else if (CraftEventFactory.entityDamageRT.get() != null) { // SparklyPaper - parallel world ticking + DamageCause cause = null; +- CraftEntity damager = CraftEventFactory.entityDamage.getBukkitEntity(); +- CraftEventFactory.entityDamage = null; ++ CraftEntity damager = CraftEventFactory.entityDamageRT.get().getBukkitEntity(); // SparklyPaper - parallel world ticking ++ CraftEventFactory.entityDamageRT.set(null); // SparklyPaper - parallel world ticking + if (source.is(DamageTypes.FALLING_STALACTITE) || source.is(DamageTypes.FALLING_BLOCK) || source.is(DamageTypes.FALLING_ANVIL)) { + cause = DamageCause.FALLING_BLOCK; + } else if (damager instanceof LightningStrike) { +@@ -2123,7 +2123,7 @@ public class CraftEventFactory { + CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemStack.copyWithCount(1)); + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(to.getX(), to.getY(), to.getZ())); +- if (!net.minecraft.world.level.block.DispenserBlock.eventFired) { ++ if (!net.minecraft.world.level.block.DispenserBlock.eventFired.get()) { // SparklyPaper - parallel world ticking + if (!event.callEvent()) { + return itemStack; + } +diff --git a/src/main/kotlin/net/sparklypower/sparklypaper/ServerLevelTickExecutorThreadFactory.kt b/src/main/kotlin/net/sparklypower/sparklypaper/ServerLevelTickExecutorThreadFactory.kt +new file mode 100644 +index 0000000000000000000000000000000000000000..7d8b995f8bb7ecf2e1c9a638dc7d7a630702243b +--- /dev/null ++++ b/src/main/kotlin/net/sparklypower/sparklypaper/ServerLevelTickExecutorThreadFactory.kt +@@ -0,0 +1,24 @@ ++package net.sparklypower.sparklypaper ++ ++import io.papermc.paper.util.TickThread ++import java.util.concurrent.ThreadFactory ++import java.util.concurrent.atomic.AtomicInteger ++ ++class ServerLevelTickExecutorThreadFactory : ThreadFactory { ++ private val threadNumber = AtomicInteger(1) ++ ++ override fun newThread(p0: Runnable): Thread { ++ val threadCount = threadNumber.getAndAdd(1) ++ val tickThread = TickThread.ServerLevelTickThread(p0, "serverlevel-tick-worker-$threadCount") ++ ++ if (tickThread.isDaemon) { ++ tickThread.isDaemon = false ++ } ++ ++ if (tickThread.priority != 5) { ++ tickThread.priority = 5 ++ } ++ ++ return tickThread ++ } ++} +\ No newline at end of file diff --git a/sparklypaper.png b/sparklypaper.png index b3edf82..3be8453 100644 Binary files a/sparklypaper.png and b/sparklypaper.png differ