From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Altiami Date: Wed, 5 Mar 2025 13:16:44 -0800 Subject: [PATCH] SparklyPaper: Parallel world ticking Original project: https://github.com/SparklyPower/SparklyPaper 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..09dfab1ace05ee62df5cf6e292f8be0146e85a36 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); + private static final boolean HARD_THROW = !org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.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()); // SparklyPaper - parallel world ticking 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()); // SparklyPaper - parallel world ticking 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()); // SparklyPaper - parallel world ticking 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()); // SparklyPaper - parallel world ticking 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()); // SparklyPaper - parallel world ticking 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()); // SparklyPaper - parallel world ticking LOGGER.error(ex, new Throwable()); + if (HARD_THROW) // SparklyPaper - parallel world ticking throw new IllegalStateException(ex); } } @@ -87,12 +95,71 @@ 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()); // SparklyPaper - parallel world ticking LOGGER.error(ex, new Throwable()); + if (HARD_THROW) // SparklyPaper - parallel world ticking throw new IllegalStateException(ex); } } + // SparklyPaper start - 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()); // SparklyPaper - parallel world ticking + if (HARD_THROW) throw new IllegalStateException(reason); + } + } + + // 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); + } + } + + // 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; + } + // SparklyPaper end - parallel world ticking + 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,46 +194,74 @@ public class TickThread extends Thread { } public static boolean isTickThreadFor(final Level world, final BlockPos pos) { - return isTickThread(); + return isTickThreadFor(world); // Leaf - SparklyPaper - parallel world ticking mod (use methods for what they were made for) } public static boolean isTickThreadFor(final Level world, final BlockPos pos, final int blockRadius) { - return isTickThread(); + return isTickThreadFor(world); // Leaf - SparklyPaper - parallel world ticking mod (add missing replacement / use methods for what they were made for) } public static boolean isTickThreadFor(final Level world, final ChunkPos pos) { - return isTickThread(); + return isTickThreadFor(world); // Leaf - SparklyPaper - parallel world ticking mod (use methods for what they were made for) } public static boolean isTickThreadFor(final Level world, final Vec3 pos) { - return isTickThread(); + return isTickThreadFor(world); // Leaf - SparklyPaper - parallel world ticking mod (use methods for what they were made for) } public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ) { - return isTickThread(); + return isTickThreadFor(world); // Leaf - SparklyPaper - parallel world ticking mod (use methods for what they were made for) } public static boolean isTickThreadFor(final Level world, final AABB aabb) { - return isTickThread(); + return isTickThreadFor(world); // Leaf - SparklyPaper - parallel world ticking mod (use methods for what they were made for) } public static boolean isTickThreadFor(final Level world, final double blockX, final double blockZ) { - return isTickThread(); + return isTickThreadFor(world); // Leaf - SparklyPaper - parallel world ticking mod (use methods for what they were made for) } public static boolean isTickThreadFor(final Level world, final Vec3 position, final Vec3 deltaMovement, final int buffer) { - return isTickThread(); + return isTickThreadFor(world); // Leaf - SparklyPaper - parallel world ticking mod (use methods for what they were made for) } public static boolean isTickThreadFor(final Level world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) { - return isTickThread(); + return isTickThreadFor(world); // Leaf - SparklyPaper - parallel world ticking mod (use methods for what they were made for) } public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ, final int radius) { - return isTickThread(); + return isTickThreadFor(world); // Leaf - SparklyPaper - parallel world ticking mod (use methods for what they were made for) + } + + // SparklyPaper start - 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) { + if (Thread.currentThread() instanceof ServerLevelTickThread serverLevelTickThread) { + return serverLevelTickThread.currentlyTickingServerLevel == world; + } else { + return isTickThread(); + } } public static boolean isTickThreadFor(final Entity entity) { - return isTickThread(); + if (entity == null) { + return true; + } + + return isTickThreadFor(entity.level()); // Leaf - SparklyPaper - parallel world ticking mod (use methods for what they were made for) + } + + 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 - parallel world ticking } diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index 61121d2efd0df2fcafdc4c272e1cd1b986f42e24..ee5f342995a335593932a497c2bafd36d34cecb2 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -480,7 +480,12 @@ public class CraftWorld extends CraftRegionAccessor implements World { } private boolean unloadChunk0(int x, int z, boolean save) { - org.spigotmc.AsyncCatcher.catchOp("chunk unload"); // Spigot + // Leaf start - SparklyPaper - parallel world ticking mod (make configurable) + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot unload chunk asynchronously"); // SparklyPaper - parallel world ticking (additional concurrency issues logs) + else + org.spigotmc.AsyncCatcher.catchOp("chunk unload"); // Spigot + // Leaf end - SparklyPaper - parallel world ticking mod (make configurable) if (!this.isChunkLoaded(x, z)) { return true; } @@ -497,6 +502,8 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean refreshChunk(int x, int z) { + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) // Leaf - SparklyPaper - parallel world ticking mod (make configurable) + 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; @@ -547,7 +554,12 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean loadChunk(int x, int z, boolean generate) { - org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot + // Leaf start - SparklyPaper - parallel world ticking mod (make configurable) + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) + 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) + else + org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot + // Leaf end - SparklyPaper - parallel world ticking mod (make configurable) 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 @@ -775,6 +787,8 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean generateTree(Location loc, TreeType type, BlockChangeDelegate delegate) { + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) // Leaf - SparklyPaper - parallel world ticking mod (make configurable) + 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); @@ -890,6 +904,8 @@ 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 + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) // Leaf - SparklyPaper - parallel world ticking mod (make configurable) + 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 @@ -981,6 +997,8 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public int getHighestBlockYAt(int x, int z, org.bukkit.HeightMap heightMap) { + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) // Leaf - SparklyPaper - parallel world ticking mod (make configurable) + 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); @@ -1011,6 +1029,8 @@ 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); + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) // Leaf - SparklyPaper - parallel world ticking mod (make configurable) + 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); @@ -2319,6 +2339,8 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void sendGameEvent(Entity sourceEntity, org.bukkit.GameEvent gameEvent, Vector position) { + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) // Leaf - SparklyPaper - parallel world ticking mod (make configurable) + 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 dd122bbbe2c33183017dbde6997d3f1cd08479b5..d164d50bcee7281283c6d9a7b85ee2596d89bcd1 100644 --- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java @@ -74,12 +74,97 @@ public class CraftBlock implements Block { return new CraftBlock(world, position); } + // Leaf start - SparklyPaper - parallel world ticking + private ServerLevel getServerLevel() { + return (this.world instanceof ServerLevel serverLevel) ? serverLevel : null; + } + + private boolean needsBuffering(ServerLevel level, String handlingMode) { + // No ServerLevel means no queue, can't buffer + return level != null && org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled && handlingMode.equals("BUFFERED") && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level); + } + + private void checkStrictMode(ServerLevel level, String handlingMode, String methodName) { + // Only check if PWT enabled, mode is STRICT, and we have a ServerLevel + if (level != null && org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled && handlingMode.equals("STRICT")) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(level, this.position, "PWT: Async unsafe block read (strict mode): " + methodName); + } + } + + private T executeBufferedRead(ServerLevel level, org.dreeam.leaf.async.world.ReadOperationType type, Object[] params, T defaultValue, String methodName) { + if (level == null) { // Should not happen if called correctly after needsBuffering + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: executeBufferedRead called with null ServerLevel for " + methodName); + return defaultValue; + } + + java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); + + // Leaf start - SparklyPaper - parallel world ticking - Shutdown handling for async reads + if (level.isShuttingDown()) { + future.completeExceptionally(new IllegalStateException("World " + level.getWorld().getName() + " is shutting down. Cannot queue new buffered read: " + type)); + } else { + org.dreeam.leaf.async.world.WorldReadRequest request = new org.dreeam.leaf.async.world.WorldReadRequest(type, params, future); + level.asyncReadRequestQueue.offer(request); // Assumes queue exists on ServerLevel + } + // Leaf end - SparklyPaper - parallel world ticking - Shutdown handling for async reads + + try { + Object result = future.join(); // Block until tick thread completes it + if (result == null) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: Buffered async read returned null for " + methodName + " - returning default."); + return defaultValue; + } + return (T) result; + } catch (java.util.concurrent.CompletionException e) { + // Leaf start - SparklyPaper - parallel world ticking - Shutdown handling for async reads + if (e.getCause() instanceof IllegalStateException && e.getCause().getMessage() != null && e.getCause().getMessage().contains("shutting down")) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: Async block read for " + methodName + " cancelled due to world shutdown: " + e.getCause().getMessage()); + } else { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Async block read failed for " + methodName + " on tick thread.", e.getCause() != null ? e.getCause() : e); + } + return defaultValue; // Return default or rethrow if appropriate for your error handling strategy + // Leaf end - SparklyPaper - parallel world ticking - Shutdown handling for async reads + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Unexpected error during async block read for " + methodName, e); + return defaultValue; + } + } + // Leaf end - SparklyPaper - parallel world ticking + public net.minecraft.world.level.block.state.BlockState getNMS() { - return this.world.getBlockState(this.position); + // Leaf start - SparklyPaper - parallel world ticking + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + + if (needsBuffering(level, handlingMode)) { + // Buffered path + return executeBufferedRead(level, org.dreeam.leaf.async.world.ReadOperationType.BLOCK_GET_NMS_STATE, new Object[]{this.position}, Blocks.AIR.defaultBlockState(), "getNMS"); + } else { + // Strict/Disabled/Non-ServerLevel path + checkStrictMode(level, handlingMode, "getNMS"); + try { + return this.world.getBlockState(this.position); + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for getNMS" + (level == null ? " (Not a ServerLevel)" : ""), e); + return Blocks.AIR.defaultBlockState(); + } + } + // Leaf end - SparklyPaper - parallel world ticking } public net.minecraft.world.level.material.FluidState getNMSFluid() { - return this.world.getFluidState(this.position); + // Leaf start - SparklyPaper - parallel world ticking + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + checkStrictMode(level, handlingMode, "getNMSFluid"); + + try { + return this.world.getFluidState(this.position); + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for getNMSFluid" + (level == null ? " (Not a ServerLevel)" : ""), e); + return net.minecraft.world.level.material.Fluids.EMPTY.defaultFluidState(); + } + // Leaf end - SparklyPaper - parallel world ticking } public BlockPos getPosition() { @@ -142,10 +227,12 @@ public class CraftBlock implements Block { return this.getWorld().getChunkAt(this); } + @Deprecated // Leaf - SparklyPaper - parallel world ticking - Magic value public void setData(final byte data) { this.setData(data, net.minecraft.world.level.block.Block.UPDATE_ALL); } + @Deprecated // Leaf - SparklyPaper - parallel world ticking - Magic value public void setData(final byte data, boolean applyPhysics) { if (applyPhysics) { this.setData(data, net.minecraft.world.level.block.Block.UPDATE_ALL); @@ -155,12 +242,18 @@ public class CraftBlock implements Block { } private void setData(final byte data, int flags) { + // SparklyPaper start - parallel world ticking + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled && world instanceof ServerLevel serverWorld) { // Leaf - SparklyPaper - parallel world ticking mod (make configurable) + 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); } @Override + @Deprecated // Leaf - SparklyPaper - parallel world ticking - Magic value public byte getData() { - net.minecraft.world.level.block.state.BlockState state = this.world.getBlockState(this.position); + net.minecraft.world.level.block.state.BlockState state = this.getNMS(); // Leaf - SparklyPaper - parallel world ticking return CraftMagicNumbers.toLegacyData(state); } @@ -177,6 +270,7 @@ public class CraftBlock implements Block { @Override public void setType(Material type, boolean applyPhysics) { Preconditions.checkArgument(type != null, "Material cannot be null"); + // Leaf - SparklyPaper - parallel world ticking - Delegates to setBlockData, which delegates to setTypeAndData (which has checks) this.setBlockData(type.createBlockData(), applyPhysics); } @@ -196,6 +290,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 (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled && world instanceof ServerLevel serverWorld) { // Leaf - SparklyPaper - parallel world ticking mod (make configurable) + 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 @@ -227,22 +326,62 @@ public class CraftBlock implements Block { @Override public Material getType() { - return this.world.getBlockState(this.position).getBukkitMaterial(); // Paper - optimise getType calls + return this.getNMS().getBukkitMaterial(); // Paper - optimise getType calls // Leaf - SparklyPaper - parallel world ticking } @Override public byte getLightLevel() { - return (byte) this.world.getMinecraftWorld().getMaxLocalRawBrightness(this.position); + // Leaf start - SparklyPaper - parallel world ticking + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + checkStrictMode(level, handlingMode, "getLightLevel"); // Strict check if applicable + + try { + // Requires Level for getMaxLocalRawBrightness + if (this.world instanceof net.minecraft.world.level.Level nmsLevel) { + return (byte) nmsLevel.getMaxLocalRawBrightness(this.position); + } + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: getLightLevel called on non-Level - returning 0"); + return 0; + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for getLightLevel" + (level == null ? " (Not a ServerLevel)" : ""), e); + return 0; + } + // Leaf end - SparklyPaper - parallel world ticking } @Override public byte getLightFromSky() { - return (byte) this.world.getBrightness(LightLayer.SKY, this.position); + // Leaf start - SparklyPaper - parallel world ticking + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + checkStrictMode(level, handlingMode, "getLightFromSky"); // Strict check if applicable + + try { + // LevelAccessor has getBrightness + return (byte) this.world.getBrightness(LightLayer.SKY, this.position); + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for getLightFromSky" + (level == null ? " (Not a ServerLevel)" : ""), e); + return 0; + } + // Leaf end - SparklyPaper - parallel world ticking } @Override public byte getLightFromBlocks() { - return (byte) this.world.getBrightness(LightLayer.BLOCK, this.position); + // Leaf start - SparklyPaper - parallel world ticking + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + checkStrictMode(level, handlingMode, "getLightFromBlocks"); // Strict check if applicable + + try { + // LevelAccessor has getBrightness + return (byte) this.world.getBrightness(LightLayer.BLOCK, this.position); + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for getLightFromBlocks" + (level == null ? " (Not a ServerLevel)" : ""), e); + return 0; + } + // Leaf end - SparklyPaper - parallel world ticking } public Block getFace(final BlockFace face) { @@ -287,47 +426,32 @@ public class CraftBlock implements Block { } public static BlockFace notchToBlockFace(Direction notch) { - if (notch == null) { - return BlockFace.SELF; - } - switch (notch) { - case DOWN: - return BlockFace.DOWN; - case UP: - return BlockFace.UP; - case NORTH: - return BlockFace.NORTH; - case SOUTH: - return BlockFace.SOUTH; - case WEST: - return BlockFace.WEST; - case EAST: - return BlockFace.EAST; - default: - return BlockFace.SELF; - } + // Leaf start - SparklyPaper - parallel world ticking - formatting + if (notch == null) return BlockFace.SELF; + return switch (notch) { + case DOWN -> BlockFace.DOWN; + case UP -> BlockFace.UP; + case NORTH -> BlockFace.NORTH; + case SOUTH -> BlockFace.SOUTH; + case WEST -> BlockFace.WEST; + case EAST -> BlockFace.EAST; + }; + // Leaf end - SparklyPaper - parallel world ticking - formatting } public static Direction blockFaceToNotch(BlockFace face) { - if (face == null) { - return null; - } - switch (face) { - case DOWN: - return Direction.DOWN; - case UP: - return Direction.UP; - case NORTH: - return Direction.NORTH; - case SOUTH: - return Direction.SOUTH; - case WEST: - return Direction.WEST; - case EAST: - return Direction.EAST; - default: - return null; - } + // Leaf start - SparklyPaper - parallel world ticking - formatting + if (face == null) return null; + return switch (face) { + case DOWN -> Direction.DOWN; + case UP -> Direction.UP; + case NORTH -> Direction.NORTH; + case SOUTH -> Direction.SOUTH; + case WEST -> Direction.WEST; + case EAST -> Direction.EAST; + default -> null; + }; + // Leaf end - SparklyPaper - parallel world ticking - formatting } @Override @@ -344,18 +468,65 @@ public class CraftBlock implements Block { @Override public Biome getBiome() { - return this.getWorld().getBiome(this.getX(), this.getY(), this.getZ()); + // Leaf start - SparklyPaper - parallel world ticking + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + + if (needsBuffering(level, handlingMode)) { + // Buffered path + net.minecraft.core.Holder nmsResult = executeBufferedRead( + level, org.dreeam.leaf.async.world.ReadOperationType.BLOCK_GET_BIOME, new Object[]{this.position}, null, "getBiome" + ); + return (nmsResult != null) ? CraftBiome.minecraftHolderToBukkit(nmsResult) : Biome.PLAINS; // Default biome + } else { + // Strict/Disabled/Non-ServerLevel path + checkStrictMode(level, handlingMode, "getBiome"); + try { + // Use Bukkit API, which should delegate safely (or buffer itself if needed) + return this.getWorld().getBiome(this.getX(), this.getY(), this.getZ()); + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for getBiome" + (level == null ? " (Not a ServerLevel)" : ""), e); + return Biome.PLAINS; // Default biome on error + } + } + // Leaf end - SparklyPaper - parallel world ticking } // Paper start @Override public Biome getComputedBiome() { - return this.getWorld().getComputedBiome(this.getX(), this.getY(), this.getZ()); + // Leaf start - SparklyPaper - parallel world ticking + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + + if (needsBuffering(level, handlingMode)) { + // Buffered path + net.minecraft.core.Holder nmsResult = executeBufferedRead( + level, org.dreeam.leaf.async.world.ReadOperationType.BLOCK_GET_COMPUTED_BIOME, new Object[]{this.position}, null, "getComputedBiome" + ); + return (nmsResult != null) ? CraftBiome.minecraftHolderToBukkit(nmsResult) : Biome.PLAINS; // Default biome + } else { + // Strict/Disabled/Non-ServerLevel path + checkStrictMode(level, handlingMode, "getComputedBiome"); + try { + // Use Bukkit API + return this.getWorld().getComputedBiome(this.getX(), this.getY(), this.getZ()); + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for getComputedBiome" + (level == null ? " (Not a ServerLevel)" : ""), e); + return Biome.PLAINS; // Default biome on error + } + } + // Leaf end - SparklyPaper - parallel world ticking } // Paper end @Override public void setBiome(Biome bio) { + // SparklyPaper start - parallel world ticking + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled && world instanceof ServerLevel serverWorld) { // Leaf - SparklyPaper - parallel world ticking mod (make configurable) + 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); } @@ -371,12 +542,50 @@ public class CraftBlock implements Block { @Override public boolean isBlockPowered() { - return this.world.getMinecraftWorld().getDirectSignalTo(this.position) > 0; + // Leaf start - SparklyPaper - parallel world ticking + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + checkStrictMode(level, handlingMode, "isBlockPowered"); // Strict check if applicable + + try { + // Requires Level.getDirectSignalTo + if (this.world instanceof net.minecraft.world.level.Level nmsLevel) { + return nmsLevel.getDirectSignalTo(this.position) > 0; + } + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: isBlockPowered called on non-Level - returning false"); + return false; + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for isBlockPowered" + (level == null ? " (Not a ServerLevel)" : ""), e); + return false; + } + // Leaf end - SparklyPaper - parallel world ticking } @Override public boolean isBlockIndirectlyPowered() { - return this.world.getMinecraftWorld().hasNeighborSignal(this.position); + // Leaf start - SparklyPaper - parallel world ticking + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + + if (needsBuffering(level, handlingMode)) { + // Buffered path + return executeBufferedRead(level, org.dreeam.leaf.async.world.ReadOperationType.BLOCK_IS_INDIRECTLY_POWERED, new Object[]{this.position}, false, "isBlockIndirectlyPowered"); + } else { + // Strict/Disabled/Non-ServerLevel path + checkStrictMode(level, handlingMode, "isBlockIndirectlyPowered"); + try { + // Requires Level.hasNeighborSignal + if (this.world instanceof net.minecraft.world.level.Level nmsLevel) { + return nmsLevel.hasNeighborSignal(this.position); + } + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: isBlockIndirectlyPowered called on non-Level - returning false"); + return false; + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for isBlockIndirectlyPowered" + (level == null ? " (Not a ServerLevel)" : ""), e); + return false; + } + } + // Leaf end - SparklyPaper - parallel world ticking } @Override @@ -398,44 +607,103 @@ public class CraftBlock implements Block { @Override public boolean isBlockFacePowered(BlockFace face) { - return this.world.getMinecraftWorld().hasSignal(this.position, CraftBlock.blockFaceToNotch(face)); + // Leaf start - SparklyPaper - parallel world ticking + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + checkStrictMode(level, handlingMode, "isBlockFacePowered"); // Strict check if applicable + + try { + if (this.world instanceof net.minecraft.world.level.Level nmsLevel) { + return nmsLevel.hasSignal(this.position, CraftBlock.blockFaceToNotch(face)); + } + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: isBlockFacePowered called on non-Level - returning false"); + return false; + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for isBlockFacePowered" + (level == null ? " (Not a ServerLevel)" : ""), e); + return false; + } + // Leaf end - SparklyPaper - parallel world ticking } @Override public boolean isBlockFaceIndirectlyPowered(BlockFace face) { - int power = this.world.getMinecraftWorld().getSignal(this.position, CraftBlock.blockFaceToNotch(face)); + // Leaf start - SparklyPaper - parallel world ticking + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + checkStrictMode(level, handlingMode, "isBlockFaceIndirectlyPowered"); // Strict check if applicable + + try { + // Requires Level.getSignal and potentially relative block access (which might need safety checks) + if (this.world instanceof net.minecraft.world.level.Level nmsLevel) { + int power = nmsLevel.getSignal(this.position, CraftBlock.blockFaceToNotch(face)); + Block relative = this.getRelative(face); // getRelative delegates, safety depends on target block + if (relative.getType() == Material.REDSTONE_WIRE) { + // getData might need buffering if called on relative block + return Math.max(power, relative.getData()) > 0; + } + return power > 0; + } + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: isBlockFaceIndirectlyPowered called on non-Level - returning false"); + return false; + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for isBlockFaceIndirectlyPowered" + (level == null ? " (Not a ServerLevel)" : ""), e); - Block relative = this.getRelative(face); - if (relative.getType() == Material.REDSTONE_WIRE) { - return Math.max(power, relative.getData()) > 0; // todo remove legacy usage + return false; } - return power > 0; + // Leaf end - SparklyPaper - parallel world ticking } + // Leaf start - SparklyPaper - parallel world ticking @Override public int getBlockPower(BlockFace face) { - int power = 0; - net.minecraft.world.level.Level world = this.world.getMinecraftWorld(); - int x = this.getX(); - int y = this.getY(); - int z = this.getZ(); - if ((face == BlockFace.DOWN || face == BlockFace.SELF) && world.hasSignal(new BlockPos(x, y - 1, z), Direction.DOWN)) power = CraftBlock.getPower(power, world.getBlockState(new BlockPos(x, y - 1, z))); - if ((face == BlockFace.UP || face == BlockFace.SELF) && world.hasSignal(new BlockPos(x, y + 1, z), Direction.UP)) power = CraftBlock.getPower(power, world.getBlockState(new BlockPos(x, y + 1, z))); - if ((face == BlockFace.EAST || face == BlockFace.SELF) && world.hasSignal(new BlockPos(x + 1, y, z), Direction.EAST)) power = CraftBlock.getPower(power, world.getBlockState(new BlockPos(x + 1, y, z))); - if ((face == BlockFace.WEST || face == BlockFace.SELF) && world.hasSignal(new BlockPos(x - 1, y, z), Direction.WEST)) power = CraftBlock.getPower(power, world.getBlockState(new BlockPos(x - 1, y, z))); - if ((face == BlockFace.NORTH || face == BlockFace.SELF) && world.hasSignal(new BlockPos(x, y, z - 1), Direction.NORTH)) power = CraftBlock.getPower(power, world.getBlockState(new BlockPos(x, y, z - 1))); - if ((face == BlockFace.SOUTH || face == BlockFace.SELF) && world.hasSignal(new BlockPos(x, y, z + 1), Direction.SOUTH)) power = CraftBlock.getPower(power, world.getBlockState(new BlockPos(x, y, z + 1))); - return power > 0 ? power : (face == BlockFace.SELF ? this.isBlockIndirectlyPowered() : this.isBlockFaceIndirectlyPowered(face)) ? 15 : 0; - } - - private static int getPower(int power, net.minecraft.world.level.block.state.BlockState state) { - if (!state.is(Blocks.REDSTONE_WIRE)) { - return power; + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + + if (needsBuffering(level, handlingMode)) { + // Buffered path + return executeBufferedRead(level, org.dreeam.leaf.async.world.ReadOperationType.BLOCK_GET_BLOCK_POWER, new Object[]{this.position, face}, 0, "getBlockPower"); + } else { + // Strict/Disabled/Non-ServerLevel path + checkStrictMode(level, handlingMode, "getBlockPower"); + try { + // Requires Level for hasSignal and getBlockState + if (this.world instanceof net.minecraft.world.level.Level nmsLevel) { + int power = 0; + BlockPos currentPos = this.position; // Use immutable position + + // Check neighbors using relative positions + if ((face == BlockFace.DOWN || face == BlockFace.SELF) && nmsLevel.hasSignal(currentPos.below(), Direction.DOWN)) power = CraftBlock.getPower(power, nmsLevel.getBlockState(currentPos.below())); + if ((face == BlockFace.UP || face == BlockFace.SELF) && nmsLevel.hasSignal(currentPos.above(), Direction.UP)) power = CraftBlock.getPower(power, nmsLevel.getBlockState(currentPos.above())); + if ((face == BlockFace.EAST || face == BlockFace.SELF) && nmsLevel.hasSignal(currentPos.east(), Direction.EAST)) power = CraftBlock.getPower(power, nmsLevel.getBlockState(currentPos.east())); + if ((face == BlockFace.WEST || face == BlockFace.SELF) && nmsLevel.hasSignal(currentPos.west(), Direction.WEST)) power = CraftBlock.getPower(power, nmsLevel.getBlockState(currentPos.west())); + if ((face == BlockFace.NORTH || face == BlockFace.SELF) && nmsLevel.hasSignal(currentPos.north(), Direction.NORTH)) power = CraftBlock.getPower(power, nmsLevel.getBlockState(currentPos.north())); + if ((face == BlockFace.SOUTH || face == BlockFace.SELF) && nmsLevel.hasSignal(currentPos.south(), Direction.SOUTH)) power = CraftBlock.getPower(power, nmsLevel.getBlockState(currentPos.south())); + + // Need to call isBlockIndirectlyPowered/isBlockFaceIndirectlyPowered safely + boolean indirect = (face == BlockFace.SELF) ? this.isBlockIndirectlyPowered() : this.isBlockFaceIndirectlyPowered(face); + return power > 0 ? power : (indirect ? 15 : 0); + } else { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: getBlockPower called on non-Level - returning 0"); + return 0; + } + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for getBlockPower" + (level == null ? " (Not a ServerLevel)" : ""), e); + return 0; + } + } + } + + // Static helper, safe + public static int getPower(int currentMax, net.minecraft.world.level.block.state.BlockState neighborState) { + if (!neighborState.is(Blocks.REDSTONE_WIRE)) { + return currentMax; } else { - return Math.max(state.getValue(RedStoneWireBlock.POWER), power); + int neighborPower = neighborState.getValue(RedStoneWireBlock.POWER); + return Math.max(neighborPower, currentMax); } } + // Leaf end - SparklyPaper - parallel world ticking @Override public int getBlockPower() { @@ -478,23 +746,35 @@ public class CraftBlock implements Block { @Override public PistonMoveReaction getPistonMoveReaction() { + // Leaf - SparklyPaper - parallel world ticking - Uses safe getNMS() return PistonMoveReaction.getById(this.getNMS().getPistonPushReaction().ordinal()); } @Override public boolean breakNaturally() { - return this.breakNaturally(null); + // Leaf start - SparklyPaper - parallel world ticking - Write operation check + ServerLevel level = getServerLevel(); + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) { + if (level != null) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(level, position, "PWT: Cannot modify world asynchronously (breakNaturally)"); + } else { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: Attempted breakNaturally on non-ServerLevel - safety not guaranteed."); + } + } + // Delegates to overload + return this.breakNaturally(null, true, true); // Default to dropping XP and triggering effects + // Leaf end - SparklyPaper - parallel world ticking - Write operation check } @Override public boolean breakNaturally(ItemStack item) { // Paper start - return this.breakNaturally(item, false); + return this.breakNaturally(item, true, true); // Leaf - SparklyPaper - parallel world ticking - Delegates to full overload } @Override public boolean breakNaturally(boolean triggerEffect, boolean dropExperience) { - return this.breakNaturally(null, triggerEffect, dropExperience); + return this.breakNaturally(null, triggerEffect, dropExperience);// Leaf - SparklyPaper - parallel world ticking - Delegates to full overload } @Override @@ -506,84 +786,147 @@ public class CraftBlock implements Block { public boolean breakNaturally(ItemStack item, boolean triggerEffect, boolean dropExperience, boolean forceEffect) { // Paper end // Order matters here, need to drop before setting to air so skulls can get their data + // Leaf start - SparklyPaper - parallel world ticking - Write operation check + // (already done in simpler overload, but check again for safety) + ServerLevel level = getServerLevel(); + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) { + if (level != null) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(level, position, "PWT: Cannot modify world asynchronously (breakNaturally item...)"); + } else { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: Attempted breakNaturally(item...) on non-ServerLevel - safety not guaranteed."); + } + } + + // Get NMS state safely + net.minecraft.world.level.block.state.BlockState state = this.getNMS(); net.minecraft.world.level.block.Block block = state.getBlock(); net.minecraft.world.item.ItemStack nmsItem = CraftItemStack.asNMSCopy(item); - boolean result = false; + boolean droppedItems = false; - // Modelled off Player#hasCorrectToolForDrops - if (block != Blocks.AIR && (item == null || !state.requiresCorrectToolForDrops() || nmsItem.isCorrectToolForDrops(state))) { - net.minecraft.world.level.block.Block.dropResources(state, this.world.getMinecraftWorld(), this.position, this.world.getBlockEntity(this.position), null, nmsItem, false); // Paper - Properly handle xp dropping - // Paper start - improve Block#breakNaturally - if (dropExperience) block.popExperience(this.world.getMinecraftWorld(), this.position, block.getExpDrop(state, this.world.getMinecraftWorld(), this.position, nmsItem, true)); - // Paper end - result = true; - } + // Experience dropping requires ServerLevel + ServerLevel serverLevelForDrops = getServerLevel(); // Re-get ServerLevel specifically for drop logic - if ((result && triggerEffect) || (forceEffect && block != Blocks.AIR)) { - if (state.getBlock() instanceof net.minecraft.world.level.block.BaseFireBlock) { - this.world.levelEvent(net.minecraft.world.level.block.LevelEvent.SOUND_EXTINGUISH_FIRE, this.position, 0); - } else { - this.world.levelEvent(net.minecraft.world.level.block.LevelEvent.PARTICLES_DESTROY_BLOCK, this.position, net.minecraft.world.level.block.Block.getId(state)); + if (serverLevelForDrops != null) { // Only attempt drops/XP if we have a ServerLevel + // Check if block should drop items + if (!state.isAir() && (item == null || !state.requiresCorrectToolForDrops() || nmsItem.isCorrectToolForDrops(state))) { + // Drop items using ServerLevel + net.minecraft.world.level.block.Block.dropResources(state, serverLevelForDrops, this.position, this.world.getBlockEntity(this.position), null, nmsItem, false); + + // Drop experience using ServerLevel + if (dropExperience) { + int xp = block.getExpDrop(state, serverLevelForDrops, this.position, nmsItem, true); + if (xp > 0) { // Only pop if there's XP to drop + block.popExperience(serverLevelForDrops, this.position, xp); + + } + } + droppedItems = true; + } + } else { + // Log if we couldn't drop XP because it wasn't a ServerLevel + if (dropExperience && !state.isAir()) { // Only warn if XP was requested and block wasn't air + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: Cannot drop experience for breakNaturally: Not a ServerLevel."); } + // Still trigger effects if requested and possible with LevelAccessor + if (triggerEffect && !state.isAir()) { + int eventId = (state.getBlock() instanceof net.minecraft.world.level.block.BaseFireBlock) + ? net.minecraft.world.level.block.LevelEvent.SOUND_EXTINGUISH_FIRE + : net.minecraft.world.level.block.LevelEvent.PARTICLES_DESTROY_BLOCK; + int eventData = (eventId == net.minecraft.world.level.block.LevelEvent.PARTICLES_DESTROY_BLOCK) + ? net.minecraft.world.level.block.Block.getId(state) : 0; + this.world.levelEvent(eventId, this.position, eventData); + } + } + + // Trigger effect using LevelAccessor (safe) + if ((droppedItems && triggerEffect) || (forceEffect && block != Blocks.AIR)) { + int eventId = (state.getBlock() instanceof net.minecraft.world.level.block.BaseFireBlock) + ? net.minecraft.world.level.block.LevelEvent.SOUND_EXTINGUISH_FIRE + : net.minecraft.world.level.block.LevelEvent.PARTICLES_DESTROY_BLOCK; + int eventData = (eventId == net.minecraft.world.level.block.LevelEvent.PARTICLES_DESTROY_BLOCK) + ? net.minecraft.world.level.block.Block.getId(state) : 0; + this.world.levelEvent(eventId, this.position, eventData); } - // SPIGOT-6778: Directly call setBlock instead of setBlockState, so that the block entity is not removed and custom remove logic is run. - // Paper start - improve breakNaturally + // Remove the block using LevelAccessor (safe) boolean destroyed = this.world.removeBlock(this.position, false); if (destroyed) { + // Call destroy hook using LevelAccessor (safe) block.destroy(this.world, this.position, state); } - if (result) { - // special cases + // Special case handling - requires Level, check again + if (droppedItems && this.world instanceof net.minecraft.world.level.Level nmsLevelForSpecialCases) { if (block instanceof net.minecraft.world.level.block.IceBlock iceBlock) { - iceBlock.afterDestroy(this.world.getMinecraftWorld(), this.position, nmsItem); + iceBlock.afterDestroy(nmsLevelForSpecialCases, this.position, nmsItem); } else if (block instanceof net.minecraft.world.level.block.TurtleEggBlock turtleEggBlock) { - turtleEggBlock.decreaseEggs(this.world.getMinecraftWorld(), this.position, state); + turtleEggBlock.decreaseEggs(nmsLevelForSpecialCases, this.position, state); } + } else if (droppedItems) { // Log if special cases couldn't run + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: Cannot perform post-break special actions: Not a Level."); } - return destroyed && result; - // Paper end + // Return true if the block was successfully destroyed AND items were dropped (or would have dropped if tool was right) + return destroyed && droppedItems; + // Leaf end - SparklyPaper - parallel world ticking - Write operation check } @Override public boolean applyBoneMeal(BlockFace face) { + // Leaf start - SparklyPaper - parallel world ticking - Write operation check + ServerLevel level = getServerLevel(); + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) { + if (level != null) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(level, position, "PWT: Cannot modify world asynchronously (applyBoneMeal)"); + } else { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: Attempted applyBoneMeal on non-ServerLevel - safety not guaranteed."); + return false; // Cannot bonemeal without ServerLevel + } + } else if (level == null) { + // If PWT is off, but it's still not a ServerLevel, we also can't bonemeal + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "Cannot apply bonemeal: Not a ServerLevel."); + return false; + } + + // Original logic requires ServerLevel (level is guaranteed non-null here) Direction direction = CraftBlock.blockFaceToNotch(face); BlockFertilizeEvent event = null; - ServerLevel world = this.getCraftWorld().getHandle(); - UseOnContext context = new UseOnContext(world, null, InteractionHand.MAIN_HAND, Items.BONE_MEAL.getDefaultInstance(), new BlockHitResult(Vec3.ZERO, direction, this.getPosition(), false)); + UseOnContext context = new UseOnContext(level, null, InteractionHand.MAIN_HAND, Items.BONE_MEAL.getDefaultInstance(), new BlockHitResult(Vec3.ZERO, direction, this.getPosition(), false)); // SPIGOT-6895: Call StructureGrowEvent and BlockFertilizeEvent - world.captureTreeGeneration = true; + level.captureTreeGeneration = true; InteractionResult result = BoneMealItem.applyBonemeal(context); - world.captureTreeGeneration = false; - - if (!world.capturedBlockStates.isEmpty()) { - TreeType treeType = SaplingBlock.treeType; - SaplingBlock.treeType = null; - List states = new ArrayList<>(world.capturedBlockStates.values()); - world.capturedBlockStates.clear(); + level.captureTreeGeneration = false; + + if (!level.capturedBlockStates.isEmpty()) { + TreeType treeType = SaplingBlock.getTreeTypeRT(); // Use thread-local getter + SaplingBlock.setTreeTypeRT(null); // Use thread-local setter + // Need Bukkit BlockState list for events + List bukkitStates = new ArrayList<>(level.capturedBlockStates.values()); + level.capturedBlockStates.clear(); // Clear NMS map StructureGrowEvent structureEvent = null; if (treeType != null) { - structureEvent = new StructureGrowEvent(this.getLocation(), treeType, true, null, states); + structureEvent = new StructureGrowEvent(this.getLocation(), treeType, true, null, bukkitStates); Bukkit.getPluginManager().callEvent(structureEvent); } - event = new BlockFertilizeEvent(CraftBlock.at(world, this.getPosition()), null, states); + event = new BlockFertilizeEvent(this, null, bukkitStates); // Use 'this' as the CraftBlock event.setCancelled(structureEvent != null && structureEvent.isCancelled()); Bukkit.getPluginManager().callEvent(event); if (!event.isCancelled()) { - for (BlockState state : states) { + for (BlockState state : bukkitStates) { CraftBlockState craftBlockState = (CraftBlockState) state; - craftBlockState.place(craftBlockState.getFlags()); - world.checkCapturedTreeStateForObserverNotify(this.position, craftBlockState); // Paper - notify observers even if grow failed + craftBlockState.place(craftBlockState.getFlags()); // This performs the actual block changes + // Notify observers using the captured state info + level.checkCapturedTreeStateForObserverNotify(this.position, craftBlockState); } } } + // Return true only if bonemeal succeeded AND the event wasn't cancelled return result == InteractionResult.SUCCESS && (event == null || !event.isCancelled()); + // Leaf end - SparklyPaper - parallel world ticking - Write operation check } @Override @@ -598,20 +941,45 @@ public class CraftBlock implements Block { @Override public Collection getDrops(ItemStack item, Entity entity) { - net.minecraft.world.level.block.state.BlockState state = this.getNMS(); - net.minecraft.world.item.ItemStack nms = CraftItemStack.asNMSCopy(item); + // Leaf start - SparklyPaper - parallel world ticking - Write operation check + // Requires ServerLevel for Block.getDrops call + ServerLevel level = getServerLevel(); + if (level == null) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: Cannot getDrops: Not a ServerLevel."); + return Collections.emptyList(); + } - // Modelled off Player#hasCorrectToolForDrops - if (item == null || CraftBlockData.isPreferredTool(state, nms)) { - return net.minecraft.world.level.block.Block.getDrops(state, this.world.getMinecraftWorld(), this.position, this.world.getBlockEntity(this.position), entity == null ? null : ((CraftEntity) entity).getHandle(), nms) - .stream().map(CraftItemStack::asBukkitCopy).collect(Collectors.toList()); - } else { + // Strict check if applicable + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + checkStrictMode(level, handlingMode, "getDrops"); + + try { + net.minecraft.world.level.block.state.BlockState state = this.getNMS(); // Use safe getNMS + net.minecraft.world.item.ItemStack nmsItem = CraftItemStack.asNMSCopy(item); + net.minecraft.world.entity.Entity nmsEntity = (entity == null) ? null : ((CraftEntity) entity).getHandle(); + + // Check tool requirement + if (item == null || !state.requiresCorrectToolForDrops() || nmsItem.isCorrectToolForDrops(state)) { + // Call NMS getDrops using the verified ServerLevel + List nmsDrops = net.minecraft.world.level.block.Block.getDrops( + state, level, this.position, this.world.getBlockEntity(this.position), nmsEntity, nmsItem + ); + // Convert to Bukkit ItemStacks + return nmsDrops.stream().map(CraftItemStack::asBukkitCopy).collect(Collectors.toList()); + } else { + // Tool was required but not correct/present + return Collections.emptyList(); + } + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for getDrops", e); return Collections.emptyList(); } + // Leaf end - SparklyPaper - parallel world ticking - Write operation check } @Override public boolean isPreferredTool(ItemStack item) { + // Leaf - SparklyPaper - parallel world ticking - Uses safe getNMS() net.minecraft.world.level.block.state.BlockState state = this.getNMS(); net.minecraft.world.item.ItemStack nms = CraftItemStack.asNMSCopy(item); return CraftBlockData.isPreferredTool(state, nms); @@ -620,9 +988,23 @@ public class CraftBlock implements Block { @Override public float getBreakSpeed(Player player) { Preconditions.checkArgument(player != null, "player cannot be null"); - return this.getNMS().getDestroyProgress(((CraftPlayer) player).getHandle(), this.world, this.position); + // Leaf start - SparklyPaper - parallel world ticking + // Requires LevelReader (LevelAccessor is sufficient) + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + checkStrictMode(level, handlingMode, "getBreakSpeed"); // Strict check if applicable + + try { + // Uses safe getNMS() + return this.getNMS().getDestroyProgress(((CraftPlayer) player).getHandle(), this.world, this.position); + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for getBreakSpeed" + (level == null ? " (Not a ServerLevel)" : ""), e); + return 0.0f; + } + // Leaf end - SparklyPaper - parallel world ticking } + // Leaf - SparklyPaper - parallel world ticking - Metadata methods delegate to CraftWorld, assumed safe @Override public void setMetadata(String metadataKey, MetadataValue newMetadataValue) { this.getCraftWorld().getBlockMetadata().setMetadata(this, metadataKey, newMetadataValue); @@ -645,57 +1027,148 @@ public class CraftBlock implements Block { @Override public boolean isPassable() { - return this.getNMS().getCollisionShape(this.world, this.position).isEmpty(); + // Leaf start - SparklyPaper - parallel world ticking + // Requires LevelReader (LevelAccessor is sufficient) + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + checkStrictMode(level, handlingMode, "isPassable"); // Strict check if applicable + + try { + // Uses safe getNMS() + VoxelShape shape = this.getNMS().getCollisionShape(this.world, this.position); + return shape.isEmpty(); + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for isPassable" + (level == null ? " (Not a ServerLevel)" : ""), e); + return true; // Default to passable on error? Or false? Passable seems safer. + } + // Leaf end - SparklyPaper - parallel world ticking } @Override public RayTraceResult rayTrace(Location start, Vector direction, double maxDistance, FluidCollisionMode fluidCollisionMode) { + // Leaf start - SparklyPaper - parallel world ticking + // Validate inputs Preconditions.checkArgument(start != null, "Location start cannot be null"); - Preconditions.checkArgument(this.getWorld().equals(start.getWorld()), "Location start cannot be a different world"); + Preconditions.checkArgument(this.getWorld().equals(start.getWorld()), "Location start cannot be in a different world"); start.checkFinite(); Preconditions.checkArgument(direction != null, "Vector direction cannot be null"); direction.checkFinite(); - Preconditions.checkArgument(direction.lengthSquared() > 0, "Direction's magnitude (%s) must be greater than 0", direction.lengthSquared()); + Preconditions.checkArgument(direction.lengthSquared() > 1.0E-8, "Direction's magnitude (%s) must be greater than 0", direction.lengthSquared()); // Avoid near-zero vectors Preconditions.checkArgument(fluidCollisionMode != null, "FluidCollisionMode cannot be null"); - if (maxDistance < 0.0D) { - return null; - } - - Vector dir = direction.clone().normalize().multiply(maxDistance); - Vec3 startPos = CraftLocation.toVec3(start); - Vec3 endPos = startPos.add(dir.getX(), dir.getY(), dir.getZ()); + if (maxDistance < 0.0D) return null; // Max distance must be non-negative + + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + + if (needsBuffering(level, handlingMode)) { + // Buffered path + HitResult nmsHitResult = executeBufferedRead( + level, org.dreeam.leaf.async.world.ReadOperationType.BLOCK_RAY_TRACE, + new Object[]{this.position, start, direction, maxDistance, fluidCollisionMode}, // Pass all params + null, "rayTrace" + ); + // Convert NMS result (can be null) to Bukkit result + return CraftRayTraceResult.convertFromInternal(this.world, nmsHitResult); + } else { + // Strict/Disabled/Non-ServerLevel path + checkStrictMode(level, handlingMode, "rayTrace"); + try { + // Calculate start and end points for the ray trace + Vec3 startPos = CraftLocation.toVec3(start); + Vector dirNormalized = direction.clone().normalize(); // Normalize once + Vec3 endPos = startPos.add(dirNormalized.getX() * maxDistance, dirNormalized.getY() * maxDistance, dirNormalized.getZ() * maxDistance); + + // Perform the clip using LevelAccessor (safe) + // Pass the block's position as the context position for the clip function + HitResult nmsHitResult = this.world.clip( + new ClipContext(startPos, endPos, ClipContext.Block.OUTLINE, CraftFluidCollisionMode.toFluid(fluidCollisionMode), CollisionContext.empty()), + this.position // Provide the block's position here + ); - HitResult hitResult = this.world.clip(new ClipContext(startPos, endPos, ClipContext.Block.OUTLINE, CraftFluidCollisionMode.toFluid(fluidCollisionMode), CollisionContext.empty()), this.position); - return CraftRayTraceResult.convertFromInternal(this.world, hitResult); + // Convert NMS result to Bukkit result + return CraftRayTraceResult.convertFromInternal(this.world, nmsHitResult); + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for rayTrace" + (level == null ? " (Not a ServerLevel)" : ""), e); + return null; // Return null on error + } + } + // Leaf end - SparklyPaper - parallel world ticking } @Override public BoundingBox getBoundingBox() { - VoxelShape shape = this.getNMS().getShape(this.world, this.position); - - if (shape.isEmpty()) { - return new BoundingBox(); // Return an empty bounding box if the block has no dimension + // Leaf start - SparklyPaper - parallel world ticking + // Requires LevelReader (LevelAccessor is sufficient) + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + checkStrictMode(level, handlingMode, "getBoundingBox"); // Strict check if applicable + + try { + // Uses safe getNMS() + VoxelShape shape = this.getNMS().getShape(this.world, this.position); + if (shape.isEmpty()) { + // Return a zero-sized box at the block's corner if shape is empty + return new BoundingBox(getX(), getY(), getZ(), getX(), getY(), getZ()); + } + // Get AABB relative to 0,0,0 and offset by block position + AABB aabb = shape.bounds(); + return new BoundingBox( + this.getX() + aabb.minX, this.getY() + aabb.minY, this.getZ() + aabb.minZ, + this.getX() + aabb.maxX, this.getY() + aabb.maxY, this.getZ() + aabb.maxZ + ); + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for getBoundingBox" + (level == null ? " (Not a ServerLevel)" : ""), e); + // Default to a full 1x1x1 box on error + return new BoundingBox(getX(), getY(), getZ(), getX() + 1, getY() + 1, getZ() + 1); } - AABB aabb = shape.bounds(); - return new BoundingBox(this.getX() + aabb.minX, this.getY() + aabb.minY, this.getZ() + aabb.minZ, this.getX() + aabb.maxX, this.getY() + aabb.maxY, this.getZ() + aabb.maxZ); + // Leaf end - SparklyPaper - parallel world ticking } @Override public org.bukkit.util.VoxelShape getCollisionShape() { - VoxelShape shape = this.getNMS().getCollisionShape(this.world, this.position); - return new CraftVoxelShape(shape); + // Leaf start - SparklyPaper - parallel world ticking + // Requires LevelReader (LevelAccessor is sufficient) + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; + checkStrictMode(level, handlingMode, "getCollisionShape"); // Strict check if applicable + + try { + // Uses safe getNMS() + VoxelShape shape = this.getNMS().getCollisionShape(this.world, this.position); + return new CraftVoxelShape(shape); // Wrap NMS shape + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for getCollisionShape" + (level == null ? " (Not a ServerLevel)" : ""), e); + return new CraftVoxelShape(net.minecraft.world.phys.shapes.Shapes.empty()); // Default to empty shape on error + } + // Leaf end - SparklyPaper - parallel world ticking } @Override public boolean canPlace(BlockData data) { 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(); + // Leaf start - SparklyPaper - parallel world ticking + ServerLevel level = getServerLevel(); + String handlingMode = org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling; - return iblockdata.canSurvive(world, this.position); + if (needsBuffering(level, handlingMode)) { + // Buffered path + return executeBufferedRead(level, org.dreeam.leaf.async.world.ReadOperationType.BLOCK_CAN_PLACE, new Object[]{this.position, data}, false, "canPlace"); + } else { + // Strict/Disabled/Non-ServerLevel path + checkStrictMode(level, handlingMode, "canPlace"); + try { + net.minecraft.world.level.block.state.BlockState nmsData = ((CraftBlockData) data).getState(); + // LevelAccessor has canSurvive + return nmsData.canSurvive(this.world, this.position); + } catch (Exception e) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "PWT: Direct access failed for canPlace" + (level == null ? " (Not a ServerLevel)" : ""), e); + return false; // Default to false on error + } + } + // Leaf end - SparklyPaper - parallel world ticking } @Override @@ -711,7 +1184,10 @@ public class CraftBlock implements Block { // Paper start @Override public com.destroystokyo.paper.block.BlockSoundGroup getSoundGroup() { - return new com.destroystokyo.paper.block.CraftBlockSoundGroup(getNMS().getBlock().defaultBlockState().getSoundType()); + // Leaf start - SparklyPaper - parallel world ticking + net.minecraft.world.level.block.SoundType nmsSoundType = this.getNMS().getSoundType(); + return new com.destroystokyo.paper.block.CraftBlockSoundGroup(nmsSoundType); + // Leaf end - SparklyPaper - parallel world ticking } @Override @@ -724,26 +1200,76 @@ public class CraftBlock implements Block { return this.getNMS().getBlock().getDescriptionId(); } + // Leaf start - SparklyPaper - parallel world ticking + @Override public boolean isValidTool(ItemStack itemStack) { - return getDrops(itemStack).size() != 0; + if (itemStack == null || itemStack.getType().isAir()) { + return false; + } + return !this.getDrops(itemStack).isEmpty(); + // Leaf end - SparklyPaper - parallel world ticking } @Override public void tick() { - final ServerLevel level = this.world.getMinecraftWorld(); + // Leaf start - SparklyPaper - parallel world ticking - Write operation check + // (block ticks can modify state) + ServerLevel level = getServerLevel(); + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) { + if (level != null) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(level, position, "PWT: Cannot tick block asynchronously (tick)"); + } else { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: Attempted tick on non-ServerLevel - safety not guaranteed."); + return; // Cannot tick without ServerLevel + } + } else if (level == null) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "Cannot tick block: Not a ServerLevel."); + return; + } + this.getNMS().tick(level, this.position, level.random); + // Leaf end - SparklyPaper - parallel world ticking - } @Override public void fluidTick() { - this.getNMSFluid().tick(this.world.getMinecraftWorld(), this.position, this.getNMS()); + // Leaf start - SparklyPaper - parallel world ticking + // Fluid ticks can modify state + ServerLevel level = getServerLevel(); + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) { + if (level != null) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(level, position, "PWT: Cannot tick fluid asynchronously (fluidTick)"); + } else { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: Attempted fluidTick on non-ServerLevel - safety not guaranteed."); + return; // Cannot tick fluid without ServerLevel + } + } else if (level == null) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "Cannot tick fluid: Not a ServerLevel."); + return; + } + this.getNMSFluid().tick(level, this.position, this.getNMS()); + // Leaf end - SparklyPaper - parallel world ticking } @Override public void randomTick() { - final ServerLevel level = this.world.getMinecraftWorld(); + // Leaf start - SparklyPaper - parallel world ticking - Write operation check + // (random ticks can modify state) + ServerLevel level = getServerLevel(); + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) { + if (level != null) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(level, position, "PWT: Cannot randomTick block asynchronously"); + } else { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.WARNING, "PWT: Attempted randomTick on non-ServerLevel - safety not guaranteed."); + return; + } + } else if (level == null) { + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "Cannot randomTick block: Not a ServerLevel."); + return; + } this.getNMS().randomTick(level, this.position, level.random); + // Leaf end - SparklyPaper - parallel world ticking - Write operation check } // Paper end } diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java index 5d4faad9df4824cfd61abfd4df011c006f114424..40fb6081bc2a6045c76f6e86584327758627f444 100644 --- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java +++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java @@ -33,6 +33,27 @@ public abstract class CraftBlockEntityState extends Craft private final T snapshot; public boolean snapshotDisabled; // Paper public static boolean DISABLE_SNAPSHOT = false; // Paper + public static ThreadLocal DISABLE_SNAPSHOT_TL = ThreadLocal.withInitial(() -> Boolean.FALSE); // SparklyPaper - parallel world ticking // Leaf - SparklyPaper - parallel world ticking mod (distinguish name) + + // Leaf start - SparklyPaper - parallel world ticking mod + // refer to original field in case plugins attempt to modify it + public static boolean getDisableSnapshotTL() { + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled && DISABLE_SNAPSHOT_TL.get()) + return true; + synchronized (CraftBlockEntityState.class) { + return DISABLE_SNAPSHOT; + } + } + + // update original field in case plugins attempt to access it + public static void setDisableSnapshotTL(boolean value) { + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) + DISABLE_SNAPSHOT_TL.set(value); + synchronized (CraftBlockEntityState.class) { + DISABLE_SNAPSHOT = value; + } + } + // Leaf end - SparklyPaper - parallel world ticking mod public CraftBlockEntityState(World world, T blockEntity) { super(world, blockEntity.getBlockPos(), blockEntity.getBlockState()); @@ -41,8 +62,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 = getDisableSnapshotTL(); // SparklyPaper - parallel world ticking // Leaf - SparklyPaper - parallel world ticking mod (collapse original behavior) + if (this.snapshotDisabled) { // SparklyPaper - parallel world ticking // Leaf - SparklyPaper - parallel world ticking mod (collapse original behavior) 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..eb7e63d4549e672ff1206055d2d754395f189a4a 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 (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled && 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,8 @@ public class CraftBlockState implements BlockState { @Override public java.util.Collection getDrops(org.bukkit.inventory.ItemStack item, org.bukkit.entity.Entity entity) { + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) // Leaf - SparklyPaper - parallel world ticking mod (make configurable) + 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 18f09de5c6549df3562e710ede825f75d69c046e..1b06f97caeda6f33938ff5391ecaad5a1fc26f36 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.getDisableSnapshotTL(); // SparklyPaper - parallel world ticking // Leaf - SparklyPaper - parallel world ticking mod (collapse original behavior) + CraftBlockEntityState.setDisableSnapshotTL(!useSnapshot); // SparklyPaper - parallel world ticking // Leaf - SparklyPaper - parallel world ticking mod (collapse original behavior) 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.setDisableSnapshotTL(prev); // SparklyPaper - parallel world ticking // Leaf - SparklyPaper - parallel world ticking mod (collapse original behavior) } } diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java index b4ee0f809c1524c74eca74ee6bc471a3051d92a6..96177467807e75bacb8c7c11ba7263f89bc0933a 100644 --- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java @@ -829,6 +829,28 @@ public class CraftEventFactory { } 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<>(); // SparklyPaper - parallel world ticking (this is from Folia, fixes concurrency bugs with sculk catalysts) + + // Leaf start - SparklyPaper - parallel world ticking mod + // refer to original field in case plugins attempt to modify it + public static BlockPos getSourceBlockOverrideRT() { + BlockPos sourceBlockOverrideRTCopy; + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled && (sourceBlockOverrideRTCopy = sourceBlockOverrideRT.get()) != null) + return sourceBlockOverrideRTCopy; + synchronized (CraftEventFactory.class) { + return sourceBlockOverride; + } + } + + // update original field in case plugins attempt to access it + public static void setSourceBlockOverrideRT(BlockPos value) { + if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) + sourceBlockOverrideRT.set(value); + synchronized (CraftEventFactory.class) { + sourceBlockOverride = value; + } + } + // Leaf end - SparklyPaper - parallel world ticking mod 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); @@ -844,7 +866,10 @@ 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); + // Leaf start - SparklyPaper parallel world ticking mod (collapse original behavior) + final BlockPos sourceBlockOverrideRTSnap = getSourceBlockOverrideRT(); + BlockSpreadEvent event = new BlockSpreadEvent(snapshot.getBlock(), CraftBlock.at(world, sourceBlockOverrideRTSnap != null ? sourceBlockOverrideRTSnap : source), snapshot); // SparklyPaper - parallel world ticking + // Leaf end - SparklyPaper parallel world ticking mod (collapse original behavior) if (event.callEvent()) { boolean result = snapshot.place(flags); return !checkSetResult || result;