From dff1f3d302298955e86b9ec508939e83ba15299c Mon Sep 17 00:00:00 2001 From: MrPowerGamerBR Date: Sun, 25 May 2025 21:41:29 -0300 Subject: [PATCH] Parallel World Ticking patches (unfinished) --- .../0001-Parallel-World-Ticking.patch | 706 ++++++++++++++++++ .../0001-Parallel-World-Ticking.patch | 626 ++++++++++++++++ .../ServerLevelTickExecutorThreadFactory.kt | 20 + 3 files changed, 1352 insertions(+) create mode 100644 sparklypaper-server/minecraft-patches/features/0001-Parallel-World-Ticking.patch create mode 100644 sparklypaper-server/paper-patches/features/0001-Parallel-World-Ticking.patch create mode 100644 sparklypaper-server/src/main/kotlin/net/sparklypower/sparklypaper/ServerLevelTickExecutorThreadFactory.kt diff --git a/sparklypaper-server/minecraft-patches/features/0001-Parallel-World-Ticking.patch b/sparklypaper-server/minecraft-patches/features/0001-Parallel-World-Ticking.patch new file mode 100644 index 0000000..6a49873 --- /dev/null +++ b/sparklypaper-server/minecraft-patches/features/0001-Parallel-World-Ticking.patch @@ -0,0 +1,706 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrPowerGamerBR +Date: Sun, 25 May 2025 21:39:32 -0300 +Subject: [PATCH] Parallel World Ticking + + +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +index f473999938840562b1007a789600342e5796a123..cb62ac5ee7ed059adde7027156421bdbef0924f3 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +@@ -1116,7 +1116,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) { +@@ -1142,7 +1142,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"); +@@ -1424,7 +1424,7 @@ public final class ChunkHolderManager { + + 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/net/minecraft/core/dispenser/DispenseItemBehavior.java b/net/minecraft/core/dispenser/DispenseItemBehavior.java +index ca02c4c71a0a5a1a0ae8bbb40f0b1b7eac64e6fd..7ce68f270d809ab0b2be45ffdd0346f053bd3b1e 100644 +--- a/net/minecraft/core/dispenser/DispenseItemBehavior.java ++++ b/net/minecraft/core/dispenser/DispenseItemBehavior.java +@@ -401,8 +401,8 @@ public interface DispenseItemBehavior { + // CraftBukkit start + level.captureTreeGeneration = false; + if (!level.capturedBlockStates.isEmpty()) { +- org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType; +- net.minecraft.world.level.block.SaplingBlock.treeType = null; ++ org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeTypeRT.get(); // SparklyPaper - parallel world ticking ++ net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(null); // SparklyPaper - parallel world ticking + org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(blockPos, level.getWorld()); + List states = new java.util.ArrayList<>(level.capturedBlockStates.values()); + level.capturedBlockStates.clear(); +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 6670762f64eb4f1904c1a83f55aae4fe638462d3..7485ce0c4ff7dd1d2b04cb162b2c2759752df98a 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -303,6 +303,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping + public static final long SERVER_INIT = System.nanoTime(); // Paper - Lag compensation + public final Set entitiesWithScheduledTasks = java.util.concurrent.ConcurrentHashMap.newKeySet(); // SparklyPaper - skip EntityScheduler's executeTick checks if there isn't any tasks to be run (concurrent because plugins may schedule tasks async) ++ public java.util.concurrent.Semaphore serverLevelTickingSemaphore = null; // SparklyPaper - parallel world ticking + + public static S spin(Function threadFunction) { + AtomicReference atomicReference = new AtomicReference<>(); +@@ -1717,6 +1718,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop> tasks = new java.util.ArrayDeque<>(); ++ try { + for (ServerLevel serverLevel : this.getAllLevels()) { + serverLevel.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent + serverLevel.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent +@@ -1733,27 +1737,46 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { ++ try { ++ ca.spottedleaf.moonrise.common.util.TickThread.ServerLevelTickThread currentThread = (ca.spottedleaf.moonrise.common.util.TickThread.ServerLevelTickThread) Thread.currentThread(); ++ currentThread.currentlyTickingServerLevel = serverLevel; ++ ++ long i = Util.getNanos(); // SparklyPaper - track world's MSPT ++ serverLevel.tick(hasTimeLeft); ++ // SparklyPaper start - track world's MSPT ++ long j = Util.getNanos() - i; ++ ++ // These are from the "tickServer" function ++ serverLevel.tickTimes5s.add(this.tickCount, j); ++ serverLevel.tickTimes10s.add(this.tickCount, j); ++ serverLevel.tickTimes60s.add(this.tickCount, j); ++ // SparklyPaper end ++ } catch (Throwable throwable) { ++ CrashReport crashreport = CrashReport.forThrowable(throwable, "Exception ticking world"); ++ ++ serverLevel.fillReportDetails(crashreport); ++ throw new ReportedException(crashreport); ++ } finally { ++ serverLevelTickingSemaphore.release(); ++ } ++ }, serverLevel) ++ ); + + profilerFiller.pop(); + profilerFiller.pop(); + serverLevel.explosionDensityCache.clear(); // Paper - Optimize explosions + } ++ ++ while (!tasks.isEmpty()) { ++ tasks.pop().get(); ++ } ++ } catch (java.lang.InterruptedException | java.util.concurrent.ExecutionException e) { ++ throw new RuntimeException(e); // Propagate exception ++ } ++ // SparklyPaper end + this.isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked + + profilerFiller.popPush("connection"); +@@ -1844,6 +1867,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop, ServerLevel> oldLevels = this.levels; + Map, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels); + newLevels.remove(level.dimension()); ++ level.tickExecutor.shutdown(); // SparklyPaper - parallel world ticking (We remove it in here instead of ServerLevel.close() because ServerLevel.close() is never called!) + this.levels = Collections.unmodifiableMap(newLevels); + } + // CraftBukkit end +diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java +index 8e83d37ffda1b5bfcacd5578e622b5dc09f6870c..11d334db1688dfaf087d854da604895fe7091b8e 100644 +--- a/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/net/minecraft/server/dedicated/DedicatedServer.java +@@ -193,6 +193,10 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + } + net.sparklypower.sparklypaper.SparklyPaperCommands.INSTANCE.registerCommands(this); + // SparklyPaper end ++ // SparklyPaper start - parallel world ticking start ++ serverLevelTickingSemaphore = new java.util.concurrent.Semaphore(net.sparklypower.sparklypaper.configs.SparklyPaperConfigUtils.INSTANCE.getConfig().getParallelWorldTicking().getThreads()); ++ DedicatedServer.LOGGER.info("Using " + serverLevelTickingSemaphore.availablePermits() + " permits for parallel world ticking"); // SparklyPaper - parallel world ticking ++ // SparklyPaper end + + this.setPvpAllowed(properties.pvp); + this.setFlightAllowed(properties.allowFlight); +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index 2915ad139a094f6d487c65b39ef065c1340a4685..dac59bdd0496c592d8148e05b82e6ff280f05d5d 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -180,7 +180,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + private final MinecraftServer server; + public final net.minecraft.world.level.storage.PrimaryLevelData serverLevelData; // CraftBukkit - type + private int lastSpawnChunkRadius; +- final EntityTickList entityTickList = new EntityTickList(); ++ final EntityTickList entityTickList = new EntityTickList(this); // SparklyPaper - parallel world ticking + // Paper - rewrite chunk system + private final GameEventDispatcher gameEventDispatcher; + public boolean noSave; +@@ -204,6 +204,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + private final StructureCheck structureCheck; + private final boolean tickTime; + private final RandomSequences randomSequences; ++ public java.util.concurrent.ExecutorService tickExecutor; // SparklyPaper - parallel world ticking + + // CraftBukkit start + public final LevelStorageSource.LevelStorageAccess levelStorageAccess; +@@ -677,6 +678,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this, this.chunkTaskScheduler); + // Paper end - rewrite chunk system + this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit ++ this.tickExecutor = java.util.concurrent.Executors.newSingleThreadExecutor(new net.sparklypower.sparklypaper.ServerLevelTickExecutorThreadFactory(getWorld().getName())); // SparklyPaper - parallel world ticking + } + + // Paper start +@@ -1225,7 +1227,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + // Paper start - rewrite chunk system + if ((++this.tickedBlocksOrFluids & 7L) != 0L) { +- ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks(); ++ // ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks(); // SparklyPaper - parallel world ticking (only run mid-tick at the end of each tick / fixes concurrency bugs related to executeMidTickTasks) + } + // Paper end - rewrite chunk system + +@@ -1238,7 +1240,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + // Paper start - rewrite chunk system + if ((++this.tickedBlocksOrFluids & 7L) != 0L) { +- ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks(); ++ // ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks(); // SparklyPaper - parallel world ticking (only run mid-tick at the end of each tick / fixes concurrency bugs related to executeMidTickTasks) + } + // Paper end - rewrite chunk system + +@@ -1498,6 +1500,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + private void addPlayer(ServerPlayer player) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot add player off-main"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + Entity entity = this.getEntity(player.getUUID()); + if (entity != null) { + LOGGER.warn("Force-added player with duplicate UUID {}", player.getUUID()); +@@ -1510,7 +1513,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + // CraftBukkit start + private boolean addEntity(Entity entity, @Nullable org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason) { +- org.spigotmc.AsyncCatcher.catchOp("entity add"); // Spigot ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot add entity off-main"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + entity.generation = false; // Paper - Don't fire sync event during generation; Reset flag if it was added during a ServerLevel generation process + // Paper start - extra debug info + if (entity.valid) { +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index 3781d9cc174b7aecacb9b9855d52c7b1ff05835c..1f762b49a2a8f6e469ce0d7336198d5f82737a0b 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -450,6 +450,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + return this.viewDistanceHolder; + } + // Paper end - rewrite chunk system ++ public boolean hasTickedAtLeastOnceInNewWorld = false; // SparklyPaper - parallel world ticking (fixes bug in DreamResourceReset where the inventory is opened AFTER the player has changed worlds, if you click with the quick tp torch in a chest, because the inventory is opened AFTER the player has teleported) + + public ServerPlayer(MinecraftServer server, ServerLevel level, GameProfile gameProfile, ClientInformation clientInformation) { + super(level, level.getSharedSpawnPos(), level.getSharedSpawnAngle(), gameProfile); +@@ -736,6 +737,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + + @Override + public void tick() { ++ hasTickedAtLeastOnceInNewWorld = true; // SparklyPaper - parallel world ticking (see: PARALLEL_NOTES.md - Opening an inventory after a world switch) + // CraftBukkit start + if (this.joining) { + this.joining = false; +@@ -1364,6 +1366,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + return this; + } else { + // CraftBukkit start ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureOnlyTickThread("Cannot change dimension of a player off-main, from world " + serverLevel().getWorld().getName() + " to world " + level.getWorld().getName()); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + /* + this.isChangingDimension = true; + LevelData levelData = level.getLevelData(); +@@ -1696,6 +1699,12 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + return OptionalInt.empty(); + } else { + // CraftBukkit start ++ // SparklyPaper start - parallel world ticking (see: PARALLEL_NOTES.md - Opening an inventory after a world switch) ++ if (!hasTickedAtLeastOnceInNewWorld) { ++ MinecraftServer.LOGGER.warn("Ignoring request to open container " + abstractContainerMenu + " because we haven't ticked in the current world yet!", new Throwable()); ++ return OptionalInt.empty(); ++ } ++ // SparklyPaper end + this.containerMenu = abstractContainerMenu; // Moved up + if (!this.isImmobile()) + this.connection +@@ -1760,6 +1769,11 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + } + @Override + public void closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) { ++ // SparklyPaper start - parallel world ticking (debugging) ++ if (net.sparklypower.sparklypaper.configs.SparklyPaperConfigUtils.INSTANCE.getLogContainerCreationStacktraces()) { ++ MinecraftServer.LOGGER.warn("Closing " + this.getBukkitEntity().getName() + " inventory that was created at", this.containerMenu.containerCreationStacktrace); ++ } ++ // SparklyPaper end + org.bukkit.craftbukkit.event.CraftEventFactory.handleInventoryCloseEvent(this, reason); // CraftBukkit + // Paper end - Inventory close reason + this.connection.send(new ClientboundContainerClosePacket(this.containerMenu.containerId)); +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 9ca3c55a3b5b1a532b86b08eb92460df4cb54f2a..b493183213e540c4170680cd1b7699348ae53621 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -113,7 +113,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 = new UserBanList(USERBANLIST_FILE); + private final IpBanList ipBans = new IpBanList(IPBANLIST_FILE); + private final ServerOpList ops = new ServerOpList(OPLIST_FILE); +@@ -134,7 +134,7 @@ public abstract class PlayerList { + + // CraftBukkit start + private org.bukkit.craftbukkit.CraftServer cserver; +- private final Map playersByName = new java.util.HashMap<>(); ++ private final Map playersByName = new java.util.HashMap<>(); // SparklyPaper - parallel world ticking (we don't need to replace the original map because we never iterate on top of this map) + public @Nullable String collideRuleTeamName; // Paper - Configurable player collision + + public PlayerList(MinecraftServer server, LayeredRegistryAccess registries, PlayerDataStorage playerIo, int maxPlayers) { +@@ -150,6 +150,7 @@ public abstract class PlayerList { + abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor + + public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie cookie) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureOnlyTickThread("Cannot place new player off-main"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + player.isRealPlayer = true; // Paper + player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed + GameProfile gameProfile = player.getGameProfile(); +@@ -713,6 +714,12 @@ public abstract class PlayerList { + } + + public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason, @Nullable org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason, @Nullable org.bukkit.Location location) { ++ // SparklyPaper - parallel world ticking (additional concurrency issues logs) ++ if (location != null) // TODO: Is this really never null, or is IntelliJ IDEA tripping? Because I'm pretty sure that this can be null and there isn't any @NotNull annotations ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureOnlyTickThread("Cannot respawn player off-main, from world " + player.serverLevel().getWorld().getName() + " to world " + location.getWorld().getName()); ++ else ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureOnlyTickThread("Cannot respawn player off-main, respawning in world " + player.serverLevel().getWorld().getName()); ++ // SparklyPaper end + player.stopRiding(); // CraftBukkit + this.players.remove(player); + this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot +@@ -723,6 +730,7 @@ public abstract class PlayerList { + ServerPlayer serverPlayer = player; + Level fromWorld = player.level(); + player.wonGame = false; ++ serverPlayer.hasTickedAtLeastOnceInNewWorld = false; // SparklyPaper - parallel world ticking (see: PARALLEL_NOTES.md - Opening an inventory after a world switch) + // CraftBukkit end + serverPlayer.connection = player.connection; + serverPlayer.restoreFrom(player, keepInventory); +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 81a18b8e605bd4c28b48a32c80be231609182970..bdb0e6b868101c1204b93c13623d2f55c96e64c8 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -805,7 +805,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + // CraftBukkit start + public void postTick() { + // No clean way to break out of ticking once the entity has been copied to a new world, so instead we move the portalling later in the tick cycle +- if (!(this instanceof ServerPlayer) && this.isAlive()) { // Paper - don't attempt to teleport dead entities ++ if (false && !(this instanceof ServerPlayer) && this.isAlive()) { // Paper - don't attempt to teleport dead entities // SparklyPaper - parallel world ticking (see issue #9, this is executed in the server level tick for non-player entities) + this.handlePortal(); + } + } +@@ -3788,6 +3788,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + private Entity teleportCrossDimension(ServerLevel level, TeleportTransition teleportTransition) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(level, "Cannot teleport entity to another world off-main, from world " + this.level.getWorld().getName() + " to world " + level.getWorld().getName()); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + List passengers = this.getPassengers(); + List list = new ArrayList<>(passengers.size()); + this.ejectPassengers(); +diff --git a/net/minecraft/world/inventory/AbstractContainerMenu.java b/net/minecraft/world/inventory/AbstractContainerMenu.java +index 813417a09b4acc7d57e80a53d970767e230d75b1..b8ff46e7543f00d963a6aa87509a1173c0ee34ec 100644 +--- a/net/minecraft/world/inventory/AbstractContainerMenu.java ++++ b/net/minecraft/world/inventory/AbstractContainerMenu.java +@@ -95,8 +95,14 @@ public abstract class AbstractContainerMenu { + + public void startOpen() {} + // CraftBukkit end ++ public Throwable containerCreationStacktrace; // SparklyPaper - parallel world ticking (debugging) + + protected AbstractContainerMenu(@Nullable MenuType menuType, int containerId) { ++ // SparklyPaper - parallel world ticking (debugging) ++ if (net.sparklypower.sparklypaper.configs.SparklyPaperConfigUtils.INSTANCE.getLogContainerCreationStacktraces()) { ++ this.containerCreationStacktrace = new Throwable(); ++ } ++ // SparklyPaper end + this.menuType = menuType; + this.containerId = containerId; + } +diff --git a/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java +index 24ecca78dc1140b6fc47d59f2acefca6bc2b0220..e02e149c4ea52f2c3e4670d81504b655ebd86026 100644 +--- a/net/minecraft/world/item/ItemStack.java ++++ b/net/minecraft/world/item/ItemStack.java +@@ -406,8 +406,8 @@ public final class ItemStack implements DataComponentHolder { + if (interactionResult.consumesAction() && serverLevel.captureTreeGeneration && !serverLevel.capturedBlockStates.isEmpty()) { + serverLevel.captureTreeGeneration = false; + org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(clickedPos, serverLevel.getWorld()); +- org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType; +- net.minecraft.world.level.block.SaplingBlock.treeType = null; ++ org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeTypeRT.get(); // SparklyPaper - parallel world ticking ++ net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(null); + List blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values()); + serverLevel.capturedBlockStates.clear(); + org.bukkit.event.world.StructureGrowEvent structureEvent = null; +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index 71454f733a568e713d6be01a8c882529a0ef1b35..617d1b0d99c90a4f2e252a06cb531ed9ceaae9fb 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -161,6 +161,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl + } + // Paper end - add paper world config + ++ public io.papermc.paper.redstone.RedstoneWireTurbo turbo = new io.papermc.paper.redstone.RedstoneWireTurbo((net.minecraft.world.level.block.RedStoneWireBlock) net.minecraft.world.level.block.Blocks.REDSTONE_WIRE); // SparklyPaper - parallel world ticking (moved to world) + public static @Nullable BlockPos lastPhysicsProblem; // Spigot + private int tileTickPosition; + public final Map explosionDensityCache = new java.util.HashMap<>(); // Paper - Optimize explosions +@@ -1085,6 +1086,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl + + @Override + public boolean setBlock(BlockPos pos, BlockState state, int flags, int recursionLeft) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, pos, "Updating block asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + // CraftBukkit start - tree generation + if (this.captureTreeGeneration) { + // Paper start - Protect Bedrock and End Portal/Frames from being destroyed +@@ -1466,7 +1468,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl + tickingBlockEntity.tick(); + // Paper start - rewrite chunk system + if ((++tickedEntities & 7) == 0) { +- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)(Level)(Object)this).moonrise$midTickTasks(); ++ // ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)(Level)(Object)this).moonrise$midTickTasks(); // SparklyPaper - parallel world ticking (only run mid-tick at the end of each tick / fixes concurrency bugs related to executeMidTickTasks) + } + // Paper end - rewrite chunk system + } +@@ -1489,7 +1491,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl + entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); + // Paper end - Prevent block entity and entity crashes + } +- this.moonrise$midTickTasks(); // Paper - rewrite chunk system ++ // this.moonrise$midTickTasks(); // Paper - rewrite chunk system // SparklyPaper - parallel world ticking (only run mid-tick at the end of each tick / fixes concurrency bugs related to executeMidTickTasks) + } + + // Paper start - Option to prevent armor stands from doing entity lookups +@@ -1632,6 +1634,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl + + @Nullable + public BlockEntity getBlockEntity(BlockPos pos, boolean validate) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThreadOrAsyncThread((ServerLevel) this, "Cannot read world asynchronously"); // SparklyPaper - parallel world ticking + // Paper start - Perf: Optimize capturedTileEntities lookup + net.minecraft.world.level.block.entity.BlockEntity blockEntity; + if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(pos)) != null) { +@@ -1649,6 +1652,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl + } + + public void setBlockEntity(BlockEntity blockEntity) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel) this, "Cannot modify world asynchronously"); // SparklyPaper - parallel world ticking + BlockPos blockPos = blockEntity.getBlockPos(); + if (!this.isOutsideBuildHeight(blockPos)) { + // CraftBukkit start +@@ -1733,6 +1737,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl + + @Override + public List getEntities(@Nullable Entity entity, AABB boundingBox, Predicate predicate) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, boundingBox, "Cannot getEntities asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + Profiler.get().incrementCounter("getEntities"); + List list = Lists.newArrayList(); + +@@ -2054,8 +2059,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl + public abstract RecipeAccess recipeAccess(); + + public BlockPos getBlockRandomPos(int x, int y, int z, int yMask) { +- this.randValue = this.randValue * 3 + 1013904223; +- int i = this.randValue >> 2; ++ int i = this.random.nextInt() >> 2; // SparklyPaper - parallel world ticking + return new BlockPos(x + (i & 15), y + (i >> 16 & yMask), z + (i >> 8 & 15)); + } + +diff --git a/net/minecraft/world/level/block/FungusBlock.java b/net/minecraft/world/level/block/FungusBlock.java +index 9711efb088bd0da9168e9bcd0496bd7caddd2974..63da283d68c7e1184d1995aeb08cd8720f1b02ee 100644 +--- a/net/minecraft/world/level/block/FungusBlock.java ++++ b/net/minecraft/world/level/block/FungusBlock.java +@@ -76,9 +76,9 @@ public class FungusBlock extends VegetationBlock implements BonemealableBlock { + // CraftBukkit start + .map((value) -> { + if (this == Blocks.WARPED_FUNGUS) { +- SaplingBlock.treeType = org.bukkit.TreeType.WARPED_FUNGUS; ++ SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.WARPED_FUNGUS); // SparklyPaper - parallel world ticking + } else if (this == Blocks.CRIMSON_FUNGUS) { +- SaplingBlock.treeType = org.bukkit.TreeType.CRIMSON_FUNGUS; ++ SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.CRIMSON_FUNGUS); // SparklyPaper - parallel world ticking + } + return value; + }) +diff --git a/net/minecraft/world/level/block/MushroomBlock.java b/net/minecraft/world/level/block/MushroomBlock.java +index d306f5f524dc64618df94c9783c2168dc561a5e3..72363832a60a380869b60b1ec4a352a1e11608a8 100644 +--- a/net/minecraft/world/level/block/MushroomBlock.java ++++ b/net/minecraft/world/level/block/MushroomBlock.java +@@ -93,7 +93,7 @@ public class MushroomBlock extends VegetationBlock implements BonemealableBlock + return false; + } else { + level.removeBlock(pos, false); +- SaplingBlock.treeType = (this == Blocks.BROWN_MUSHROOM) ? org.bukkit.TreeType.BROWN_MUSHROOM : org.bukkit.TreeType.RED_MUSHROOM; // CraftBukkit ++ SaplingBlock.treeTypeRT.set((this == Blocks.BROWN_MUSHROOM) ? org.bukkit.TreeType.BROWN_MUSHROOM : org.bukkit.TreeType.RED_MUSHROOM); // CraftBukkit // SparklyPaper - parallel world ticking + if (optional.get().value().place(level, level.getChunkSource().getGenerator(), random, pos)) { + return true; + } else { +diff --git a/net/minecraft/world/level/block/RedStoneWireBlock.java b/net/minecraft/world/level/block/RedStoneWireBlock.java +index 1943a6aad888647953e2d9dbbeedb0bd81c6f9df..2c4c57deead027fc78904e7e9bc8a2e213225960 100644 +--- a/net/minecraft/world/level/block/RedStoneWireBlock.java ++++ b/net/minecraft/world/level/block/RedStoneWireBlock.java +@@ -267,7 +267,7 @@ public class RedStoneWireBlock extends Block { + + // Paper start - Optimize redstone (Eigencraft) + // The bulk of the new functionality is found in RedstoneWireTurbo.java +- io.papermc.paper.redstone.RedstoneWireTurbo turbo = new io.papermc.paper.redstone.RedstoneWireTurbo(this); ++ // io.papermc.paper.redstone.RedstoneWireTurbo turbo = new io.papermc.paper.redstone.RedstoneWireTurbo(this); // SparklyPaper - parallel world ticking (moved to world) + + /* + * Modified version of pre-existing updateSurroundingRedstone, which is called from +@@ -283,7 +283,7 @@ public class RedStoneWireBlock extends Block { + if (orientation != null) { + source = pos.relative(orientation.getFront().getOpposite()); + } +- turbo.updateSurroundingRedstone(worldIn, pos, state, source); ++ worldIn.turbo.updateSurroundingRedstone(worldIn, pos, state, source); // SparklyPaper - parallel world ticking + return; + } + updatePowerStrength(worldIn, pos, state, orientation, blockAdded); +@@ -311,7 +311,7 @@ public class RedStoneWireBlock extends Block { + // [Space Walker] suppress shape updates and emit those manually to + // bypass the new neighbor update stack. + if (level.setBlock(pos, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) { +- turbo.updateNeighborShapes(level, pos, state); ++ level.turbo.updateNeighborShapes(level, pos, state); // SparklyPaper - parallel world ticking + } + } + } +diff --git a/net/minecraft/world/level/block/SaplingBlock.java b/net/minecraft/world/level/block/SaplingBlock.java +index a22cb810622e0ae97bc2a0d6390d026d9482b783..7934299d633524a9ef8c9d315a48c0f5bbe13b4c 100644 +--- a/net/minecraft/world/level/block/SaplingBlock.java ++++ b/net/minecraft/world/level/block/SaplingBlock.java +@@ -25,7 +25,7 @@ public class SaplingBlock extends VegetationBlock implements BonemealableBlock { + public static final IntegerProperty STAGE = BlockStateProperties.STAGE; + private static final VoxelShape SHAPE = Block.column(12.0, 0.0, 12.0); + protected final TreeGrower treeGrower; +- public static @javax.annotation.Nullable org.bukkit.TreeType treeType; // CraftBukkit ++ public static final ThreadLocal treeTypeRT = new ThreadLocal<>(); // SparklyPaper - parallel world ticking (from Folia) + + @Override + public MapCodec codec() { +@@ -62,8 +62,8 @@ public class SaplingBlock extends VegetationBlock implements BonemealableBlock { + this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random); + level.captureTreeGeneration = false; + if (!level.capturedBlockStates.isEmpty()) { +- org.bukkit.TreeType treeType = SaplingBlock.treeType; +- SaplingBlock.treeType = null; ++ org.bukkit.TreeType treeType = SaplingBlock.treeTypeRT.get(); // SparklyPaper - parallel world ticking ++ SaplingBlock.treeTypeRT.set(null); + org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(pos, level.getWorld()); + java.util.List blocks = new java.util.ArrayList<>(level.capturedBlockStates.values()); + level.capturedBlockStates.clear(); +diff --git a/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java b/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java +index c63370fd458fb4f7190b79b1a8174fcc92d88f9c..5c2033ef17d27ee7242de0c4a00425f74d50cad9 100644 +--- a/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java +@@ -79,6 +79,12 @@ public abstract class BaseContainerBlockEntity extends BlockEntity implements Co + } + + public static boolean canUnlock(Player player, LockCode code, Component displayName, @Nullable BlockEntity blockEntity) { ++ // SparklyPaper - parallel world ticking (see: PARALLEL_NOTES.md - Opening an inventory after a world switch) ++ if (player instanceof net.minecraft.server.level.ServerPlayer serverPlayer && blockEntity != null && blockEntity.getLevel() != serverPlayer.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(displayName)); +diff --git a/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java b/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +index 2627583ab12d886b1fba0b1d1e599f942926b499..2caa770411c542a70fe50267ce4cffb22fc94b97 100644 +--- a/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +@@ -43,9 +43,7 @@ public class SculkCatalystBlockEntity extends BlockEntity implements GameEventLi + // Paper end - Fix NPE in SculkBloomEvent world access + + public static void serverTick(Level level, BlockPos pos, BlockState state, SculkCatalystBlockEntity sculkCatalyst) { +- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = sculkCatalyst.getBlockPos(); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. + sculkCatalyst.catalystListener.getSculkSpreader().updateCursors(level, pos, level.getRandom(), true); +- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = null; // CraftBukkit + } + + @Override +diff --git a/net/minecraft/world/level/block/grower/TreeGrower.java b/net/minecraft/world/level/block/grower/TreeGrower.java +index d23f255de9208f42125fa358a9e8194c984fe4d3..54d64e77dc8c4bef25822d74a30be1a860cfec36 100644 +--- a/net/minecraft/world/level/block/grower/TreeGrower.java ++++ b/net/minecraft/world/level/block/grower/TreeGrower.java +@@ -203,55 +203,57 @@ public final class TreeGrower { + + // CraftBukkit start + private void setTreeType(Holder> feature) { ++ org.bukkit.TreeType treeType; // SparklyPaper - parallel world ticking + if (feature.is(TreeFeatures.OAK) || feature.is(TreeFeatures.OAK_BEES_005)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TREE; ++ treeType = org.bukkit.TreeType.TREE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.HUGE_RED_MUSHROOM)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.RED_MUSHROOM; ++ treeType = org.bukkit.TreeType.RED_MUSHROOM; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.HUGE_BROWN_MUSHROOM)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BROWN_MUSHROOM; ++ treeType = org.bukkit.TreeType.BROWN_MUSHROOM; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.JUNGLE_TREE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.COCOA_TREE; ++ treeType = org.bukkit.TreeType.COCOA_TREE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.JUNGLE_TREE_NO_VINE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SMALL_JUNGLE; ++ treeType = org.bukkit.TreeType.SMALL_JUNGLE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.PINE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_REDWOOD; ++ treeType = org.bukkit.TreeType.TALL_REDWOOD; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.SPRUCE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.REDWOOD; ++ treeType = org.bukkit.TreeType.REDWOOD; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.ACACIA)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.ACACIA; ++ treeType = org.bukkit.TreeType.ACACIA; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.BIRCH) || feature.is(TreeFeatures.BIRCH_BEES_005)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIRCH; ++ treeType = org.bukkit.TreeType.BIRCH; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.SUPER_BIRCH_BEES_0002)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_BIRCH; ++ treeType = org.bukkit.TreeType.TALL_BIRCH; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.SWAMP_OAK)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SWAMP; ++ treeType = org.bukkit.TreeType.SWAMP; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.FANCY_OAK) || feature.is(TreeFeatures.FANCY_OAK_BEES_005)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIG_TREE; ++ treeType = org.bukkit.TreeType.BIG_TREE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.JUNGLE_BUSH)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE_BUSH; ++ treeType = org.bukkit.TreeType.JUNGLE_BUSH; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.DARK_OAK)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.DARK_OAK; ++ treeType = org.bukkit.TreeType.DARK_OAK; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.MEGA_SPRUCE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_REDWOOD; ++ treeType = org.bukkit.TreeType.MEGA_REDWOOD; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.MEGA_PINE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_PINE; ++ treeType = org.bukkit.TreeType.MEGA_PINE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.MEGA_JUNGLE_TREE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE; ++ treeType = org.bukkit.TreeType.JUNGLE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.AZALEA_TREE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.AZALEA; ++ treeType = org.bukkit.TreeType.AZALEA; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.MANGROVE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MANGROVE; ++ treeType = org.bukkit.TreeType.MANGROVE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.TALL_MANGROVE)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_MANGROVE; ++ treeType = org.bukkit.TreeType.TALL_MANGROVE; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.CHERRY) || feature.is(TreeFeatures.CHERRY_BEES_005)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.CHERRY; ++ treeType = org.bukkit.TreeType.CHERRY; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.PALE_OAK) || feature.is(TreeFeatures.PALE_OAK_BONEMEAL)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK; ++ treeType = org.bukkit.TreeType.PALE_OAK; // SparklyPaper - parallel world ticking + } else if (feature.is(TreeFeatures.PALE_OAK_CREAKING)) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK_CREAKING; ++ treeType = org.bukkit.TreeType.PALE_OAK_CREAKING; // SparklyPaper - parallel world ticking + } else { + throw new IllegalArgumentException("Unknown tree generator " + feature); + } ++ net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(treeType); // SparklyPaper - parallel world ticking + } + // CraftBukkit end + } +diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java +index 5d3fc807221392d378fec283bfdefb8747fb8376..b674e2dbf35b489baf3615236b74f62efe2f33be 100644 +--- a/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/net/minecraft/world/level/chunk/LevelChunk.java +@@ -358,6 +358,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + @Nullable + @Override + public BlockState setBlockState(BlockPos pos, BlockState state, int flags) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, pos, "Updating block asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + int y = pos.getY(); + LevelChunkSection section = this.getSection(this.getSectionIndex(y)); + boolean hasOnlyAir = section.hasOnlyAir(); +diff --git a/net/minecraft/world/level/entity/EntityTickList.java b/net/minecraft/world/level/entity/EntityTickList.java +index 423779a2b690f387a4f0bd07b97b50e0baefda76..24f9632e73d73c2ad68ebf30eb0e4cca7befae2d 100644 +--- a/net/minecraft/world/level/entity/EntityTickList.java ++++ b/net/minecraft/world/level/entity/EntityTickList.java +@@ -10,17 +10,26 @@ import net.minecraft.world.entity.Entity; + + public class EntityTickList { + private final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet entities = new ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<>(); // Paper - rewrite chunk system ++ // SparklyPaper start - parallel world ticking ++ // Used to track async entity additions/removals/loops ++ private final net.minecraft.server.level.ServerLevel serverLevel; ++ public EntityTickList(net.minecraft.server.level.ServerLevel serverLevel) { ++ this.serverLevel = serverLevel; ++ } ++ // SparklyPaper end + + private void ensureActiveIsNotIterated() { + // Paper - rewrite chunk system + } + + public void add(Entity entity) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(entity, "Asynchronous entity ticklist addition"); // Paper // SparklyPaper - parallel world ticking (additional concurrency issues logs) + this.ensureActiveIsNotIterated(); + this.entities.add(entity); // Paper - rewrite chunk system + } + + public void remove(Entity entity) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(entity, "Asynchronous entity ticklist removal"); // Paper // SparklyPaper - parallel world ticking (additional concurrency issues logs) + this.ensureActiveIsNotIterated(); + this.entities.remove(entity); // Paper - rewrite chunk system + } +@@ -30,6 +39,7 @@ public class EntityTickList { + } + + public void forEach(Consumer entity) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverLevel, "Asynchronous entity ticklist iteration"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + // Paper start - rewrite chunk system + // To ensure nothing weird happens with dimension travelling, do not iterate over new entries... + // (by dfl iterator() is configured to not iterate over new entries) diff --git a/sparklypaper-server/paper-patches/features/0001-Parallel-World-Ticking.patch b/sparklypaper-server/paper-patches/features/0001-Parallel-World-Ticking.patch new file mode 100644 index 0000000..904d591 --- /dev/null +++ b/sparklypaper-server/paper-patches/features/0001-Parallel-World-Ticking.patch @@ -0,0 +1,626 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrPowerGamerBR +Date: Sun, 25 May 2025 21:40:06 -0300 +Subject: [PATCH] Parallel World Ticking + + +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java +index 69cdd304d255d52c9b7dc9b6a33ffdb630b79abe..8fc5691d608359ce848b8caecdd55ddd4372abfa 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java +@@ -14,6 +14,7 @@ import java.util.concurrent.atomic.AtomicInteger; + public class TickThread extends Thread { + + private static final Logger LOGGER = LoggerFactory.getLogger(TickThread.class); ++ public static final boolean HARD_THROW = !Boolean.getBoolean("sparklypaper.disableHardThrow"); // SparklyPaper - parallel world ticking - THIS SHOULD NOT BE DISABLED SINCE IT CAN CAUSE DATA CORRUPTION!!! Anyhow, for production servers, if you want to make a test run to see if the server could crash, you can test it with this disabled + + private static String getThreadContext() { + return "thread=" + Thread.currentThread().getName(); +@@ -26,6 +27,7 @@ public class TickThread extends Thread { + public static void ensureTickThread(final String reason) { + if (!isTickThread()) { + LOGGER.error("Thread failed main thread check: " + reason + ", context=" + getThreadContext(), new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(reason); + } + } +@@ -33,8 +35,9 @@ public class TickThread extends Thread { + public static void ensureTickThread(final Level world, final BlockPos pos, final String reason) { + if (!isTickThreadFor(world, pos)) { + final String ex = "Thread failed main thread check: " + +- reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + pos; ++ reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + pos + " - " + getTickThreadInformation(world.getServer()); + LOGGER.error(ex, new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(ex); + } + } +@@ -42,8 +45,9 @@ public class TickThread extends Thread { + public static void ensureTickThread(final Level world, final BlockPos pos, final int blockRadius, final String reason) { + if (!isTickThreadFor(world, pos, blockRadius)) { + final String ex = "Thread failed main thread check: " + +- reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + pos + ", block_radius=" + blockRadius; ++ reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + pos + ", block_radius=" + blockRadius + " - " + getTickThreadInformation(world.getServer()); + LOGGER.error(ex, new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(ex); + } + } +@@ -51,8 +55,9 @@ public class TickThread extends Thread { + public static void ensureTickThread(final Level world, final ChunkPos pos, final String reason) { + if (!isTickThreadFor(world, pos)) { + final String ex = "Thread failed main thread check: " + +- reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", chunk_pos=" + pos; ++ reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", chunk_pos=" + pos + " - " + getTickThreadInformation(world.getServer()); + LOGGER.error(ex, new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(ex); + } + } +@@ -60,8 +65,9 @@ public class TickThread extends Thread { + public static void ensureTickThread(final Level world, final int chunkX, final int chunkZ, final String reason) { + if (!isTickThreadFor(world, chunkX, chunkZ)) { + final String ex = "Thread failed main thread check: " + +- reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", chunk_pos=" + new ChunkPos(chunkX, chunkZ); ++ reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", chunk_pos=" + new ChunkPos(chunkX, chunkZ) + " - " + getTickThreadInformation(world.getServer()); + LOGGER.error(ex, new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(ex); + } + } +@@ -69,8 +75,9 @@ public class TickThread extends Thread { + public static void ensureTickThread(final Entity entity, final String reason) { + if (!isTickThreadFor(entity)) { + final String ex = "Thread failed main thread check: " + +- reason + ", context=" + getThreadContext() + ", entity=" + EntityUtil.dumpEntity(entity); ++ reason + ", context=" + getThreadContext() + ", entity=" + EntityUtil.dumpEntity(entity) + " - " + getTickThreadInformation(entity.getServer()); + LOGGER.error(ex, new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(ex); + } + } +@@ -78,8 +85,9 @@ public class TickThread extends Thread { + public static void ensureTickThread(final Level world, final AABB aabb, final String reason) { + if (!isTickThreadFor(world, aabb)) { + final String ex = "Thread failed main thread check: " + +- reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", aabb=" + aabb; ++ reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", aabb=" + aabb + " - " + getTickThreadInformation(world.getServer()); + LOGGER.error(ex, new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(ex); + } + } +@@ -87,12 +95,75 @@ public class TickThread extends Thread { + public static void ensureTickThread(final Level world, final double blockX, final double blockZ, final String reason) { + if (!isTickThreadFor(world, blockX, blockZ)) { + final String ex = "Thread failed main thread check: " + +- reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + new Vec3(blockX, 0.0, blockZ); ++ reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + new Vec3(blockX, 0.0, blockZ) + " - " + getTickThreadInformation(world.getServer()); + LOGGER.error(ex, new Throwable()); ++ if (HARD_THROW) // SparklyPaper - parallel world ticking + throw new IllegalStateException(ex); + } + } + ++ // SparklyPaper - parallel world ticking ++ // This is an additional method to check if the tick thread is bound to a specific world because, by default, Paper's isTickThread methods do not provide this information ++ // Because we only tick worlds in parallel (instead of regions), we can use this for our checks ++ public static void ensureTickThread(final net.minecraft.server.level.ServerLevel world, final String reason) { ++ if (!isTickThreadFor(world)) { ++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason + " @ world " + world.getWorld().getName() + " - " + getTickThreadInformation(world.getServer()), new Throwable()); ++ if (HARD_THROW) ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ // SparklyPaper - parallel world ticking ++ // This is an additional method to check if it is a tick thread but ONLY a tick thread ++ public static void ensureOnlyTickThread(final String reason) { ++ boolean isTickThread = isTickThread(); ++ boolean isServerLevelTickThread = isServerLevelTickThread(); ++ if (!isTickThread || isServerLevelTickThread) { ++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread ONLY tick thread check: " + reason, new Throwable()); ++ if (HARD_THROW) ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ // SparklyPaper - parallel world ticking ++ // This is an additional method to check if the tick thread is bound to a specific world or if it is an async thread. ++ public static void ensureTickThreadOrAsyncThread(final net.minecraft.server.level.ServerLevel world, final String reason) { ++ boolean isValidTickThread = isTickThreadFor(world); ++ boolean isAsyncThread = !isTickThread(); ++ boolean isValid = isAsyncThread || isValidTickThread; ++ if (!isValid) { ++ LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread or async thread check: " + reason + " @ world " + world.getWorld().getName() + " - " + getTickThreadInformation(world.getServer()), new Throwable()); ++ if (HARD_THROW) ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ public static String getTickThreadInformation(net.minecraft.server.MinecraftServer minecraftServer) { ++ StringBuilder sb = new StringBuilder(); ++ Thread currentThread = Thread.currentThread(); ++ sb.append("Is tick thread? "); ++ sb.append(currentThread instanceof TickThread); ++ sb.append("; Is server level tick thread? "); ++ sb.append(currentThread instanceof ServerLevelTickThread); ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ sb.append("; Currently ticking level: "); ++ if (serverLevelTickThread.currentlyTickingServerLevel != null) { ++ sb.append(serverLevelTickThread.currentlyTickingServerLevel.getWorld().getName()); ++ } else { ++ sb.append("null"); ++ } ++ } ++ sb.append("; Is iterating over levels? "); ++ sb.append(minecraftServer.isIteratingOverLevels); ++ sb.append("; Are we going to hard throw? "); ++ sb.append(HARD_THROW); ++ return sb.toString(); ++ } ++ ++ public static boolean isServerLevelTickThread() { ++ return Thread.currentThread() instanceof ServerLevelTickThread; ++ } ++ + public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */ + + private static final AtomicInteger ID_GENERATOR = new AtomicInteger(); +@@ -127,7 +198,11 @@ public class TickThread extends Thread { + } + + public static boolean isTickThreadFor(final Level world, final BlockPos pos) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final BlockPos pos, final int blockRadius) { +@@ -135,38 +210,103 @@ public class TickThread extends Thread { + } + + public static boolean isTickThreadFor(final Level world, final ChunkPos pos) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final Vec3 pos) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final AABB aabb) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final double blockX, final double blockZ) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final Vec3 position, final Vec3 deltaMovement, final int buffer) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ, final int radius) { +- return isTickThread(); ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; ++ } ++ ++ // SparklyPaper - parallel world ticking ++ // This is an additional method to check if the tick thread is bound to a specific world because, by default, Paper's isTickThread methods do not provide this information ++ // Because we only tick worlds in parallel (instead of regions), we can use this for our checks ++ public static boolean isTickThreadFor(final Level world) { ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == world; ++ } else return currentThread instanceof TickThread; + } + + public static boolean isTickThreadFor(final Entity entity) { +- return isTickThread(); ++ if (entity == null) { ++ return true; ++ } ++ ++ Thread currentThread = Thread.currentThread(); ++ ++ if (currentThread instanceof ServerLevelTickThread serverLevelTickThread) { ++ return serverLevelTickThread.currentlyTickingServerLevel == entity.level(); ++ } else return currentThread instanceof TickThread; ++ } ++ ++ // SparklyPaper start - parallel world ticking ++ public static class ServerLevelTickThread extends TickThread { ++ public ServerLevelTickThread(String name) { ++ super(name); ++ } ++ ++ public ServerLevelTickThread(Runnable run, String name) { ++ super(run, name); ++ } ++ ++ public net.minecraft.server.level.ServerLevel currentlyTickingServerLevel; + } ++ // SparklyPaper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index aae378697b2f2e388d2a5dfaca24c9197b8abf3e..4b2367be92e28331d4e15bab68333d3afcd61950 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -456,7 +456,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + } + + private boolean unloadChunk0(int x, int z, boolean save) { +- org.spigotmc.AsyncCatcher.catchOp("chunk unload"); // Spigot ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot unload chunk asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + if (!this.isChunkLoaded(x, z)) { + return true; + } +@@ -473,6 +473,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean refreshChunk(int x, int z) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot refresh chunk asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); + if (playerChunk == null) return false; + +@@ -523,6 +524,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean loadChunk(int x, int z, boolean generate) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.getHandle(), x, z, "May not sync load chunks asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot + warnUnsafeChunk("loading a faraway chunk", x, z); // Paper + ChunkAccess chunk = this.world.getChunkSource().getChunk(x, z, generate || isChunkGenerated(x, z) ? ChunkStatus.FULL : ChunkStatus.EMPTY, true); // Paper +@@ -751,6 +753,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean generateTree(Location loc, TreeType type, BlockChangeDelegate delegate) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, loc.getX(), loc.getZ(), "Cannot generate tree asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + this.world.captureTreeGeneration = true; + this.world.captureBlockStates = true; + boolean grownTree = this.generateTree(loc, type); +@@ -866,6 +869,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + } + public boolean createExplosion(double x, double y, double z, float power, boolean setFire, boolean breakBlocks, Entity source, Consumer configurator) { + // Paper end - expand explosion API ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot create explosion asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + net.minecraft.world.level.Level.ExplosionInteraction explosionType; + if (!breakBlocks) { + explosionType = net.minecraft.world.level.Level.ExplosionInteraction.NONE; // Don't break blocks +@@ -957,6 +961,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public int getHighestBlockYAt(int x, int z, org.bukkit.HeightMap heightMap) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x >> 4, z >> 4, "Cannot retrieve chunk asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + warnUnsafeChunk("getting a faraway chunk", x >> 4, z >> 4); // Paper + // Transient load for this tick + return this.world.getChunk(x >> 4, z >> 4).getHeight(CraftHeightMap.toNMS(heightMap), x, z); +@@ -987,6 +992,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); ++ ca.spottedleaf.moonrise.common.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); + +@@ -2295,6 +2301,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void sendGameEvent(Entity sourceEntity, org.bukkit.GameEvent gameEvent, Vector position) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, position.getX(), position.getZ(), "Cannot send game event asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + getHandle().gameEvent(sourceEntity != null ? ((CraftEntity) sourceEntity).getHandle(): null, net.minecraft.core.registries.BuiltInRegistries.GAME_EVENT.get(org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(gameEvent.getKey())).orElseThrow(), org.bukkit.craftbukkit.util.CraftVector.toBlockPos(position)); + } + // Paper end +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +index a4d5c65edc1db59f3486ce5d3757cc306211a54b..3609e71f382f80017acb91c5e286008d1684d224 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +@@ -155,6 +155,11 @@ public class CraftBlock implements Block { + } + + private void setData(final byte data, int flags) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + this.world.setBlock(this.position, CraftMagicNumbers.getBlock(this.getType(), data), flags); + } + +@@ -196,6 +201,11 @@ public class CraftBlock implements Block { + } + + public static boolean setBlockState(LevelAccessor world, BlockPos pos, net.minecraft.world.level.block.state.BlockState oldState, net.minecraft.world.level.block.state.BlockState newState, boolean applyPhysics) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, pos, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + // SPIGOT-611: need to do this to prevent glitchiness. Easier to handle this here (like /setblock) than to fix weirdness in block entity cleanup + if (oldState.hasBlockEntity() && newState.getBlock() != oldState.getBlock()) { // SPIGOT-3725 remove old block entity if block changes + // SPIGOT-4612: faster - just clear tile +@@ -344,18 +354,33 @@ public class CraftBlock implements Block { + + @Override + public Biome getBiome() { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + return this.getWorld().getBiome(this.getX(), this.getY(), this.getZ()); + } + + // Paper start + @Override + public Biome getComputedBiome() { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + return this.getWorld().getComputedBiome(this.getX(), this.getY(), this.getZ()); + } + // Paper end + + @Override + public void setBiome(Biome bio) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + this.getWorld().setBiome(this.getX(), this.getY(), this.getZ(), bio); + } + +@@ -376,6 +401,11 @@ public class CraftBlock implements Block { + + @Override + public boolean isBlockIndirectlyPowered() { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + return this.world.getMinecraftWorld().hasNeighborSignal(this.position); + } + +@@ -415,6 +445,11 @@ public class CraftBlock implements Block { + + @Override + public int getBlockPower(BlockFace face) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + int power = 0; + net.minecraft.world.level.Level world = this.world.getMinecraftWorld(); + int x = this.getX(); +@@ -499,6 +534,11 @@ public class CraftBlock implements Block { + + @Override + public boolean breakNaturally(ItemStack item, boolean triggerEffect, boolean dropExperience) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + // Paper end + // Order matters here, need to drop before setting to air so skulls can get their data + net.minecraft.world.level.block.state.BlockState state = this.getNMS(); +@@ -542,6 +582,11 @@ public class CraftBlock implements Block { + + @Override + public boolean applyBoneMeal(BlockFace face) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + Direction direction = CraftBlock.blockFaceToNotch(face); + BlockFertilizeEvent event = null; + ServerLevel world = this.getCraftWorld().getHandle(); +@@ -553,8 +598,8 @@ public class CraftBlock implements Block { + world.captureTreeGeneration = false; + + if (!world.capturedBlockStates.isEmpty()) { +- TreeType treeType = SaplingBlock.treeType; +- SaplingBlock.treeType = null; ++ TreeType treeType = SaplingBlock.treeTypeRT.get(); // SparklyPaper - parallel world ticking ++ SaplingBlock.treeTypeRT.set(null);// SparklyPaper - parallel world ticking + List states = new ArrayList<>(world.capturedBlockStates.values()); + world.capturedBlockStates.clear(); + StructureGrowEvent structureEvent = null; +@@ -593,6 +638,11 @@ public class CraftBlock implements Block { + @Override + public Collection getDrops(ItemStack item, Entity entity) { + net.minecraft.world.level.block.state.BlockState state = this.getNMS(); ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + net.minecraft.world.item.ItemStack nms = CraftItemStack.asNMSCopy(item); + + // Modelled off Player#hasCorrectToolForDrops +@@ -644,6 +694,11 @@ public class CraftBlock implements Block { + + @Override + public RayTraceResult rayTrace(Location start, Vector direction, double maxDistance, FluidCollisionMode fluidCollisionMode) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + Preconditions.checkArgument(start != null, "Location start cannot be null"); + Preconditions.checkArgument(this.getWorld().equals(start.getWorld()), "Location start cannot be a different world"); + start.checkFinite(); +@@ -685,6 +740,11 @@ public class CraftBlock implements Block { + + @Override + public boolean canPlace(BlockData data) { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot read world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + Preconditions.checkArgument(data != null, "BlockData cannot be null"); + net.minecraft.world.level.block.state.BlockState iblockdata = ((CraftBlockData) data).getState(); + net.minecraft.world.level.Level world = this.world.getMinecraftWorld(); +@@ -724,6 +784,11 @@ public class CraftBlock implements Block { + + @Override + public void tick() { ++ // SparklyPaper start - parallel world ticking ++ if (world instanceof ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking + final ServerLevel level = this.world.getMinecraftWorld(); + this.getNMS().tick(level, this.position, level.random); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java +index 3422970353dcd886934b9ee906467769d39abbde..79f53cfa396ae67b1e5432ca353cebafae9499fa 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java +@@ -25,7 +25,7 @@ public abstract class CraftBlockEntityState extends Craft + private final T blockEntity; + private final T snapshot; + public boolean snapshotDisabled; // Paper +- public static boolean DISABLE_SNAPSHOT = false; // Paper ++ public static ThreadLocal DISABLE_SNAPSHOT = ThreadLocal.withInitial(() -> Boolean.FALSE); // SparklyPaper - parallel world ticking + + public CraftBlockEntityState(World world, T blockEntity) { + super(world, blockEntity.getBlockPos(), blockEntity.getBlockState()); +@@ -34,8 +34,8 @@ public abstract class CraftBlockEntityState extends Craft + + try { // Paper - Show blockstate location if we failed to read it + // Paper start +- this.snapshotDisabled = DISABLE_SNAPSHOT; +- if (DISABLE_SNAPSHOT) { ++ this.snapshotDisabled = DISABLE_SNAPSHOT.get(); // SparklyPaper - parallel world ticking ++ if (DISABLE_SNAPSHOT.get()) { // SparklyPaper - parallel world ticking + this.snapshot = this.blockEntity; + } else { + this.snapshot = this.createSnapshot(blockEntity); +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java +index 196835bdf95ba0e149b2977e9ef41698971f501f..641adf9666fef4d15bc9b585aabfe687a83fbe86 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java +@@ -218,6 +218,12 @@ public class CraftBlockState implements BlockState { + LevelAccessor access = this.getWorldHandle(); + CraftBlock block = this.getBlock(); + ++ // SparklyPaper start - parallel world ticking ++ if (access instanceof net.minecraft.server.level.ServerLevel serverWorld) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); ++ } ++ // SparklyPaper end - parallel world ticking ++ + if (block.getType() != this.getType()) { + if (!force) { + return false; +@@ -365,6 +371,7 @@ public class CraftBlockState implements BlockState { + + @Override + public java.util.Collection getDrops(org.bukkit.inventory.ItemStack item, org.bukkit.entity.Entity entity) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(world.getHandle(), position, "Cannot modify world asynchronously"); // SparklyPaper - parallel world ticking + this.requirePlaced(); + net.minecraft.world.item.ItemStack nms = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(item); + +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java +index 2338e7c115037430cefae26a571ded71f77983c4..15ba9c5588b6ef8da7e495dccbdcd53711ee4af1 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java +@@ -195,14 +195,14 @@ public final class CraftBlockStates { + BlockPos pos = craftBlock.getPosition(); + net.minecraft.world.level.block.state.BlockState state = craftBlock.getNMS(); + BlockEntity blockEntity = craftBlock.getHandle().getBlockEntity(pos); +- boolean prev = CraftBlockEntityState.DISABLE_SNAPSHOT; +- CraftBlockEntityState.DISABLE_SNAPSHOT = !useSnapshot; ++ boolean prev = CraftBlockEntityState.DISABLE_SNAPSHOT.get(); // SparklyPaper - parallel world ticking ++ CraftBlockEntityState.DISABLE_SNAPSHOT.set(!useSnapshot); // SparklyPaper - parallel world ticking + try { + CraftBlockState blockState = CraftBlockStates.getBlockState(world, pos, state, blockEntity); + blockState.setWorldHandle(craftBlock.getHandle()); // Inject the block's generator access + return blockState; + } finally { +- CraftBlockEntityState.DISABLE_SNAPSHOT = prev; ++ CraftBlockEntityState.DISABLE_SNAPSHOT.set(prev); // SparklyPaper - parallel world ticking + } + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index b60d1816fe97c01d13ba46311c57a0fc29e86e19..077f3d3173bfab9196b504a0386479e3af41f59a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -808,7 +808,7 @@ public class CraftEventFactory { + return false; + } + +- public static BlockPos sourceBlockOverride = null; // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPos up to five methods deep. ++ public static final ThreadLocal sourceBlockOverrideRT = new ThreadLocal<>(); // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // SparklyPaper - parallel world ticking (this is from Folia, fixes concurrency bugs with sculk catalysts) + + public static boolean handleBlockSpreadEvent(LevelAccessor world, BlockPos source, BlockPos target, net.minecraft.world.level.block.state.BlockState state, int flags) { + return handleBlockSpreadEvent(world, source, target, state, flags, false); +@@ -824,7 +824,7 @@ public class CraftEventFactory { + CraftBlockState snapshot = CraftBlockStates.getBlockState(world, target); + snapshot.setData(state); + +- BlockSpreadEvent event = new BlockSpreadEvent(snapshot.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverride != null ? CraftEventFactory.sourceBlockOverride : source), snapshot); ++ BlockSpreadEvent event = new BlockSpreadEvent(snapshot.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverrideRT.get() != null ? CraftEventFactory.sourceBlockOverrideRT.get() : source), snapshot); + if (event.callEvent()) { + boolean result = snapshot.place(flags); + return !checkSetResult || result; diff --git a/sparklypaper-server/src/main/kotlin/net/sparklypower/sparklypaper/ServerLevelTickExecutorThreadFactory.kt b/sparklypaper-server/src/main/kotlin/net/sparklypower/sparklypaper/ServerLevelTickExecutorThreadFactory.kt new file mode 100644 index 0000000..9a40afb --- /dev/null +++ b/sparklypaper-server/src/main/kotlin/net/sparklypower/sparklypaper/ServerLevelTickExecutorThreadFactory.kt @@ -0,0 +1,20 @@ +package net.sparklypower.sparklypaper + +import ca.spottedleaf.moonrise.common.util.TickThread +import java.util.concurrent.ThreadFactory + +class ServerLevelTickExecutorThreadFactory(private val worldName: String) : ThreadFactory { + override fun newThread(p0: Runnable): Thread { + val tickThread = TickThread.ServerLevelTickThread(p0, "serverlevel-tick-worker [$worldName]") + + if (tickThread.isDaemon) { + tickThread.isDaemon = false + } + + if (tickThread.priority != 5) { + tickThread.priority = 5 + } + + return tickThread + } +} \ No newline at end of file