diff --git a/leaf-server/minecraft-patches/features/0134-SparklyPaper-Parallel-world-ticking.patch b/leaf-server/minecraft-patches/features/0134-SparklyPaper-Parallel-world-ticking.patch index ff920aca..d9544927 100644 --- a/leaf-server/minecraft-patches/features/0134-SparklyPaper-Parallel-world-ticking.patch +++ b/leaf-server/minecraft-patches/features/0134-SparklyPaper-Parallel-world-ticking.patch @@ -265,7 +265,7 @@ index 5ab2c8333178335515e619b87ae420f948c83bd1..be3b2f023897a8823560ee059cb16ec9 } diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index c50a301a0c2365c2052aefc6a23fcf6fa82e1b9d..ac751d460ae0c8dbb858c4047c459a11b57ae175 100644 +index c50a301a0c2365c2052aefc6a23fcf6fa82e1b9d..7f791ff5bdf6c29c78f863d21af16270a74d8e7e 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java @@ -291,6 +291,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop serverPlayer1.connection.suspendFlushing()); this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit -@@ -1743,28 +1768,50 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop, ServerLevel> oldLevels = this.levels; Map, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels); newLevels.remove(level.dimension()); @@ -568,7 +581,7 @@ index d4048661575ebfaf128ba25da365843774364e0e..33dd16a26edd2974f04d9a868d3e58e8 // Gale start - Pufferfish - SIMD support diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index a66e5f6652d9633c856490de36d8d8fdf8a5298a..60e0296312030d25f917c568c17ce86d08e18122 100644 +index a66e5f6652d9633c856490de36d8d8fdf8a5298a..d6524d5c442555eaeb4d90f6a101262ee669f0d9 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -182,7 +182,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @@ -580,23 +593,173 @@ index a66e5f6652d9633c856490de36d8d8fdf8a5298a..60e0296312030d25f917c568c17ce86d // Paper - rewrite chunk system private final GameEventDispatcher gameEventDispatcher; public boolean noSave; -@@ -208,6 +208,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -208,7 +208,9 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe private double preciseTime; // Purpur - Configurable daylight cycle private boolean forceTime; // Purpur - Configurable daylight cycle private final RandomSequences randomSequences; +- + public java.util.concurrent.ExecutorService tickExecutor; // SparklyPaper - parallel world ticking - ++ public final java.util.concurrent.ConcurrentLinkedQueue asyncReadRequestQueue = new java.util.concurrent.ConcurrentLinkedQueue<>(); // Leaf - SparklyPaper - parallel world ticking ++ private volatile boolean isShuttingDown = false; // Leaf - SparklyPaper - parallel world ticking - Shutdown handling for async reads // CraftBukkit start public final LevelStorageSource.LevelStorageAccess levelStorageAccess; -@@ -703,6 +704,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + public final UUID uuid; +@@ -703,8 +705,26 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit this.preciseTime = this.serverLevelData.getDayTime(); // Purpur - Configurable daylight cycle this.realPlayers = Lists.newArrayList(); // Leaves - skip + this.tickExecutor = java.util.concurrent.Executors.newSingleThreadExecutor(new org.dreeam.leaf.async.world.SparklyPaperServerLevelTickExecutorThreadFactory(getWorld().getName())); // SparklyPaper - parallel world ticking ++ } ++ ++ // Leaf start - SparklyPaper - parallel world ticking - Shutdown handling for async reads ++ public boolean isShuttingDown() { ++ return this.isShuttingDown; } ++ public void prepareShutdown() { ++ this.isShuttingDown = true; ++ org.dreeam.leaf.async.world.WorldReadRequest req; ++ int clearedRequests = 0; ++ while ((req = this.asyncReadRequestQueue.poll()) != null) { ++ req.future().completeExceptionally(new IllegalStateException("World " + this.getWorld().getName() + " is shutting down. Cannot process buffered read: " + req.type())); ++ clearedRequests++; ++ } ++ if (clearedRequests > 0) MinecraftServer.LOGGER.info("PWT: Cleared " + clearedRequests + " pending async read requests for world " + this.getWorld().getName() + " during shutdown."); ++ } ++ // Leaf end - SparklyPaper - parallel world ticking - Shutdown handling for async reads ++ // Paper start -@@ -1313,9 +1315,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + @Override + public boolean hasChunk(int chunkX, int chunkZ) { +@@ -737,8 +757,112 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + public Player[] eligibleDespawnCheckingPlayerCache = new Player[0]; // Leaf - Cache eligible players for despawn checks + ++ // Leaf start - SparklyPaper - parallel world ticking ++ private void processAsyncReadRequests() { ++ // Only process if parallel ticking is enabled and buffering is active ++ if (!org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled || ++ !org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.asyncUnsafeReadHandling.equals("BUFFERED")) { ++ // Clear queue if buffering gets disabled to prevent memory leaks ++ if (!this.asyncReadRequestQueue.isEmpty()) { ++ org.dreeam.leaf.async.world.WorldReadRequest req; ++ while ((req = this.asyncReadRequestQueue.poll()) != null) { ++ req.future().completeExceptionally(new IllegalStateException("Async read buffering disabled while request was pending.")); ++ } ++ } ++ return; ++ } ++ ++ org.dreeam.leaf.async.world.WorldReadRequest request; ++ int processed = 0; ++ // Limit processing per tick to avoid stalling the tick loop ++ int maxToProcess = 16384; // Consider making this configurable ++ ++ while (processed < maxToProcess && (request = this.asyncReadRequestQueue.poll()) != null) { ++ processed++; ++ // Ensure we are on the correct thread before executing ++ // This check might be redundant if called correctly from tick(), but good for safety ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Processing async read request off-thread"); ++ ++ try { ++ Object result = executeReadRequest(request); ++ request.future().complete(result); ++ } catch (Throwable t) { ++ // Log the error from the tick thread side ++ org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "Exception processing buffered async world read for type " + request.type(), t); ++ request.future().completeExceptionally(t); ++ } ++ } ++ } ++ ++ // Executes the actual read operation based on the request type ++ private Object executeReadRequest(org.dreeam.leaf.async.world.WorldReadRequest request) { ++ Object[] params = request.params(); ++ BlockPos pos; // Declare pos outside the switch ++ ++ switch (request.type()) { ++ case BLOCK_GET_NMS_STATE: { // ++ pos = (BlockPos) params[0]; ++ return this.getBlockState(pos); ++ } ++ case BLOCK_GET_BIOME: { ++ pos = (BlockPos) params[0]; ++ return this.getNoiseBiome(pos.getX() >> 2, pos.getY() >> 2, pos.getZ() >> 2); ++ } ++ case BLOCK_GET_COMPUTED_BIOME: { ++ pos = (BlockPos) params[0]; ++ return this.getBiome(pos); ++ } ++ case BLOCK_IS_INDIRECTLY_POWERED: { ++ pos = (BlockPos) params[0]; ++ return this.hasNeighborSignal(pos); ++ } ++ case BLOCK_GET_BLOCK_POWER: { ++ pos = (BlockPos) params[0]; ++ org.bukkit.block.BlockFace face = (org.bukkit.block.BlockFace) params[1]; ++ int power = 0; ++ Direction notchDir = org.bukkit.craftbukkit.block.CraftBlock.blockFaceToNotch(face); ++ ++ if ((face == org.bukkit.block.BlockFace.DOWN || face == org.bukkit.block.BlockFace.SELF) && this.hasSignal(pos.below(), Direction.DOWN)) power = org.bukkit.craftbukkit.block.CraftBlock.getPower(power, this.getBlockState(pos.below())); ++ if ((face == org.bukkit.block.BlockFace.UP || face == org.bukkit.block.BlockFace.SELF) && this.hasSignal(pos.above(), Direction.UP)) power = org.bukkit.craftbukkit.block.CraftBlock.getPower(power, this.getBlockState(pos.above())); ++ if ((face == org.bukkit.block.BlockFace.EAST || face == org.bukkit.block.BlockFace.SELF) && this.hasSignal(pos.east(), Direction.EAST)) power = org.bukkit.craftbukkit.block.CraftBlock.getPower(power, this.getBlockState(pos.east())); ++ if ((face == org.bukkit.block.BlockFace.WEST || face == org.bukkit.block.BlockFace.SELF) && this.hasSignal(pos.west(), Direction.WEST)) power = org.bukkit.craftbukkit.block.CraftBlock.getPower(power, this.getBlockState(pos.west())); ++ if ((face == org.bukkit.block.BlockFace.NORTH || face == org.bukkit.block.BlockFace.SELF) && this.hasSignal(pos.north(), Direction.NORTH)) power = org.bukkit.craftbukkit.block.CraftBlock.getPower(power, this.getBlockState(pos.north())); ++ if ((face == org.bukkit.block.BlockFace.SOUTH || face == org.bukkit.block.BlockFace.SELF) && this.hasSignal(pos.south(), Direction.SOUTH)) power = org.bukkit.craftbukkit.block.CraftBlock.getPower(power, this.getBlockState(pos.south())); ++ ++ boolean indirectlyPowered = (face == org.bukkit.block.BlockFace.SELF) ? this.hasNeighborSignal(pos) : (this.getSignal(pos, notchDir) > 0); // Simplified indirect check for faces ++ return power > 0 ? power : (indirectlyPowered ? 15 : 0); ++ } ++ case BLOCK_RAY_TRACE: { ++ pos = (BlockPos) params[0]; ++ org.bukkit.Location start = (org.bukkit.Location) params[1]; ++ org.bukkit.util.Vector direction = (org.bukkit.util.Vector) params[2]; ++ double maxDistance = (double) params[3]; ++ org.bukkit.FluidCollisionMode fluidCollisionMode = (org.bukkit.FluidCollisionMode) params[4]; ++ ++ org.bukkit.util.Vector dir = direction.clone().normalize().multiply(maxDistance); ++ Vec3 startPos = org.bukkit.craftbukkit.util.CraftLocation.toVec3D(start); ++ Vec3 endPos = startPos.add(dir.getX(), dir.getY(), dir.getZ()); ++ ++ return this.clip(new net.minecraft.world.level.ClipContext(startPos, endPos, net.minecraft.world.level.ClipContext.Block.OUTLINE, org.bukkit.craftbukkit.CraftFluidCollisionMode.toNMS(fluidCollisionMode), net.minecraft.world.phys.shapes.CollisionContext.empty()), pos); // Pass block pos ++ } ++ case BLOCK_CAN_PLACE: { ++ pos = (BlockPos) params[0]; ++ org.bukkit.block.data.BlockData data = (org.bukkit.block.data.BlockData) params[1]; ++ net.minecraft.world.level.block.state.BlockState nmsData = ((org.bukkit.craftbukkit.block.data.CraftBlockData) data).getState(); ++ return nmsData.canSurvive(this, pos); ++ } ++ // Add cases for other ReadOperationType values here... ++ // case GET_ENTITIES_IN_BOX: ... (complex, needs careful list handling) ++ ++ default: ++ throw new UnsupportedOperationException("Unsupported buffered read type: " + request.type()); ++ } ++ } ++ // Leaf end - SparklyPaper - parallel world ticking ++ + public void tick(BooleanSupplier hasTimeLeft) { + this.handlingTick = true; ++ this.processAsyncReadRequests(); // Leaf - SparklyPaper - parallel world ticking + TickRateManager tickRateManager = this.tickRateManager(); + boolean runsNormally = tickRateManager.runsNormally(); + if (runsNormally) { +@@ -746,6 +870,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + this.advanceWeatherCycle(); + } + ++ // Leaf start - SparklyPaper - parallel world ticking ++ if (!org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled) { ++ this.moonrise$midTickTasks(); ++ } else if ((++this.tickedBlocksOrFluids & 7L) != 0L) { // Keep original mid-tick logic for PWT enabled ++ this.server.moonrise$executeMidTickTasks(); ++ } ++ // Leaf end - SparklyPaper - parallel world ticking ++ + int _int = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); + if (this.purpurConfig.playersSkipNight && this.sleepStatus.areEnoughSleeping(_int) && this.sleepStatus.areEnoughDeepSleeping(_int, this.players)) { // Purpur - Config for skipping night + // Paper start - create time skip event - move up calculations +@@ -1313,9 +1445,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe fluidState.tick(this, pos, blockState); } // Paper start - rewrite chunk system @@ -611,7 +774,7 @@ index a66e5f6652d9633c856490de36d8d8fdf8a5298a..60e0296312030d25f917c568c17ce86d // Paper end - rewrite chunk system } -@@ -1326,9 +1331,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1326,9 +1461,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe blockState.tick(this, pos, this.random); } // Paper start - rewrite chunk system @@ -626,7 +789,7 @@ index a66e5f6652d9633c856490de36d8d8fdf8a5298a..60e0296312030d25f917c568c17ce86d // Paper end - rewrite chunk system } -@@ -1579,6 +1587,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1579,6 +1717,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } private void addPlayer(ServerPlayer player) { @@ -635,7 +798,7 @@ index a66e5f6652d9633c856490de36d8d8fdf8a5298a..60e0296312030d25f917c568c17ce86d Entity entity = this.getEntities().get(player.getUUID()); if (entity != null) { LOGGER.warn("Force-added player with duplicate UUID {}", player.getUUID()); -@@ -1591,7 +1601,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1591,7 +1731,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // CraftBukkit start private boolean addEntity(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason) { diff --git a/leaf-server/minecraft-patches/features/0135-SparklyPaper-Track-each-world-MSPT.patch b/leaf-server/minecraft-patches/features/0135-SparklyPaper-Track-each-world-MSPT.patch index aef67f40..f16beee9 100644 --- a/leaf-server/minecraft-patches/features/0135-SparklyPaper-Track-each-world-MSPT.patch +++ b/leaf-server/minecraft-patches/features/0135-SparklyPaper-Track-each-world-MSPT.patch @@ -6,10 +6,10 @@ Subject: [PATCH] SparklyPaper: Track each world MSPT Original project: https://github.com/SparklyPower/SparklyPaper diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index ac751d460ae0c8dbb858c4047c459a11b57ae175..24926aa7ed5c78b235659daf18b224b14beb744c 100644 +index 7f791ff5bdf6c29c78f863d21af16270a74d8e7e..1431a0fac3c8a846535c1bd2f60a1279d08f14ea 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java -@@ -1693,7 +1693,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function threadFunction) { ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system -@@ -1088,6 +1089,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop realPlayers; // Leaves - skip @@ -178,7 +178,7 @@ index 60d5539f2a94b19281bcd8bdc2044eae1225278d..e95cbbc2757ed7f8d8aa873cec6bd492 public LevelChunk getChunkIfLoaded(int x, int z) { return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately -@@ -335,6 +345,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -336,6 +346,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks(); } @@ -191,7 +191,7 @@ index 60d5539f2a94b19281bcd8bdc2044eae1225278d..e95cbbc2757ed7f8d8aa873cec6bd492 @Override public final ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus status) { return this.moonrise$getChunkTaskScheduler().syncLoadNonFull(chunkX, chunkZ, status); -@@ -711,6 +727,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -712,6 +728,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.preciseTime = this.serverLevelData.getDayTime(); // Purpur - Configurable daylight cycle this.realPlayers = Lists.newArrayList(); // Leaves - skip this.tickExecutor = java.util.concurrent.Executors.newSingleThreadExecutor(new org.dreeam.leaf.async.world.SparklyPaperServerLevelTickExecutorThreadFactory(getWorld().getName())); // SparklyPaper - parallel world ticking @@ -204,8 +204,8 @@ index 60d5539f2a94b19281bcd8bdc2044eae1225278d..e95cbbc2757ed7f8d8aa873cec6bd492 + // Leaf end - Async target finding } - // Paper start -@@ -855,12 +878,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + // Leaf start - SparklyPaper - parallel world ticking - Shutdown handling for async reads +@@ -985,12 +1008,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } this.moonrise$midTickTasks(); // Paper - rewrite chunk system // Gale end - Airplane - remove lambda from ticking guard - copied from guardEntityTick @@ -220,7 +220,7 @@ index 60d5539f2a94b19281bcd8bdc2044eae1225278d..e95cbbc2757ed7f8d8aa873cec6bd492 } // Paper - rewrite chunk system -@@ -1333,6 +1358,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1463,6 +1488,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Leaf end - 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 @@ -228,7 +228,7 @@ index 60d5539f2a94b19281bcd8bdc2044eae1225278d..e95cbbc2757ed7f8d8aa873cec6bd492 } private void tickBlock(BlockPos pos, Block block) { -@@ -1349,6 +1375,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1479,6 +1505,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Leaf end - 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 @@ -307,14 +307,14 @@ index 05d5cde42b7011091ef4ee874c0d9d5586ae3f10..88809afe30bb970a7de8bdfd26926880 this.navigation.tick(); this.customServerAiStep((ServerLevel)this.level()); diff --git a/net/minecraft/world/entity/ai/goal/AvoidEntityGoal.java b/net/minecraft/world/entity/ai/goal/AvoidEntityGoal.java -index 4b24bb47a53a46586a642f3fb2656b1b8b670bf2..22f052d02ccf22f22894a0b236af1b95c8d65e97 100644 +index 4b24bb47a53a46586a642f3fb2656b1b8b670bf2..9abb8e7b0dea2cb63dad234812d773403d0716f6 100644 --- a/net/minecraft/world/entity/ai/goal/AvoidEntityGoal.java +++ b/net/minecraft/world/entity/ai/goal/AvoidEntityGoal.java @@ -67,15 +67,24 @@ public class AvoidEntityGoal extends Goal { @Override public boolean canUse() { -+ // Leaf start - Async target finding ++ // Leaf start - Async Avoid Entity Finding + if (org.dreeam.leaf.config.modules.async.AsyncTargetFinding.searchEntity) { + if (!poll()) { + getNearestEntityAsync(); @@ -328,11 +328,11 @@ index 4b24bb47a53a46586a642f3fb2656b1b8b670bf2..22f052d02ccf22f22894a0b236af1b95 this.mob, this.mob.getX(), - this.mob.getY(), -+ this.mob.getEyeY(), // Leaf - Async target finding ++ this.mob.getEyeY(), // Leaf - Async Avoid Entity Finding this.mob.getZ() ); + } -+ // Leaf end - Async target finding ++ // Leaf end - Async Avoid Entity Finding if (this.toAvoid == null) { return false; } else { @@ -340,7 +340,7 @@ index 4b24bb47a53a46586a642f3fb2656b1b8b670bf2..22f052d02ccf22f22894a0b236af1b95 } } -+ // Leaf start - Async target finding ++ // Leaf start - Async Avoid Entity Finding + private boolean poll() { + if (!(this.mob.getGoalCtx().result() instanceof LivingEntity target)) return false; + var serverLevel = getServerLevel(this.mob); @@ -368,25 +368,25 @@ index 4b24bb47a53a46586a642f3fb2656b1b8b670bf2..22f052d02ccf22f22894a0b236af1b95 + z + ); + } -+ // Leaf end - Async target finding ++ // Leaf end - Async Avoid Entity Finding + @Override public boolean canContinueToUse() { return !this.pathNav.isDone(); diff --git a/net/minecraft/world/entity/ai/goal/BegGoal.java b/net/minecraft/world/entity/ai/goal/BegGoal.java -index 28ef40e8a645989ea181297069cf2bbe571f3082..1c128f60adf6a72bed0ca40b98dc579cdf9d0e21 100644 +index 28ef40e8a645989ea181297069cf2bbe571f3082..d011e4735cb8fd65a39a6b7a66386375b12aca78 100644 --- a/net/minecraft/world/entity/ai/goal/BegGoal.java +++ b/net/minecraft/world/entity/ai/goal/BegGoal.java @@ -27,8 +27,43 @@ public class BegGoal extends Goal { this.setFlags(EnumSet.of(Goal.Flag.LOOK)); } -+ // Leaf start - Async target finding ++ // Leaf start - Async Target Finding + protected boolean poll() { + if (!(this.wolf.getGoalCtx().result() instanceof Player target)) return false; + if (target == null) return false; + ServerLevel serverLevel = getServerLevel(this.wolf); -+ if (serverLevel == null || !target.isAlive() || !playerHoldingInteresting0(target)) return false; ++ if (serverLevel == null || !target.isAlive() || !playerHoldingInteresting(target)) return false; + this.player = target; + return true; + } @@ -399,17 +399,17 @@ index 28ef40e8a645989ea181297069cf2bbe571f3082..1c128f60adf6a72bed0ca40b98dc579c + final TargetingConditions begTargeting = this.begTargeting; + ctx.wake = () -> { + var player = serverLevel.getNearestPlayer(begTargeting, wolf); -+ if (player != null && playerHoldingInteresting0(player)) { ++ if (player != null && playerHoldingInteresting(player)) { + return player; + } + return null; + }; + } -+ // Leaf end - Async target finding ++ // Leaf end - Async Target Finding + @Override public boolean canUse() { -+ // Leaf start - Async target finding ++ // Leaf start - Async Target Finding + if (poll()) { + return true; + } @@ -417,28 +417,23 @@ index 28ef40e8a645989ea181297069cf2bbe571f3082..1c128f60adf6a72bed0ca40b98dc579c + findTargetAsync(); + return false; + } -+ // Leaf end - Async target finding ++ // Leaf end - Async Target Finding this.player = this.level.getNearestPlayer(this.begTargeting, this.wolf); return this.player != null && this.playerHoldingInteresting(this.player); } -@@ -69,4 +104,17 @@ public class BegGoal extends Goal { - - return false; +@@ -59,10 +94,10 @@ public class BegGoal extends Goal { + this.lookTime--; } -+ -+ // Leaf start - Async target finding - static impl -+ private static boolean playerHoldingInteresting0(Player player) { -+ for (InteractionHand interactionHand : InteractionHand.values()) { -+ ItemStack itemInHand = player.getItemInHand(interactionHand); -+ if (itemInHand.is(Items.BONE) || itemInHand.is(net.minecraft.tags.ItemTags.WOLF_FOOD)) { -+ return true; -+ } -+ } -+ -+ return false; -+ } -+ // Leaf end - Async target finding - static impl - } + +- private boolean playerHoldingInteresting(Player player) { ++ private static boolean playerHoldingInteresting(Player player) { // Leaf start - Async Target Finding - static + for (InteractionHand interactionHand : InteractionHand.values()) { + ItemStack itemInHand = player.getItemInHand(interactionHand); +- if (itemInHand.is(Items.BONE) || this.wolf.isFood(itemInHand)) { ++ if (itemInHand.is(Items.BONE) || itemInHand.is(net.minecraft.tags.ItemTags.WOLF_FOOD)) { // Leaf end - Async Target Finding + return true; + } + } diff --git a/net/minecraft/world/entity/ai/goal/CatLieOnBedGoal.java b/net/minecraft/world/entity/ai/goal/CatLieOnBedGoal.java index 4c46cd105cde3cbcde65a02ff691c3a8edd56c76..b3205c9f35687bc37124876198ec2d657ccaa96c 100644 --- a/net/minecraft/world/entity/ai/goal/CatLieOnBedGoal.java @@ -674,7 +669,7 @@ index 3093f03d4f298bf39fec8bad2b6c22518774aea8..0a41797fd7beddce0b93d42bac6e0270 } else { this.parent = animal; diff --git a/net/minecraft/world/entity/ai/goal/GoalSelector.java b/net/minecraft/world/entity/ai/goal/GoalSelector.java -index e82e32407cec6109b9c3b0106295217f4a3f4aa2..852393af4013178562940f6a0d07e5364758ec21 100644 +index e82e32407cec6109b9c3b0106295217f4a3f4aa2..3c24382a3cced8dcea103ccc87cb506310de8461 100644 --- a/net/minecraft/world/entity/ai/goal/GoalSelector.java +++ b/net/minecraft/world/entity/ai/goal/GoalSelector.java @@ -26,13 +26,23 @@ public class GoalSelector { @@ -723,7 +718,7 @@ index e82e32407cec6109b9c3b0106295217f4a3f4aa2..852393af4013178562940f6a0d07e536 flagIterator ^= ca.spottedleaf.concurrentutil.util.IntegerUtil.getTrailingBit(flagIterator); // Paper end - Perf: optimize goal types if (!flag.getOrDefault(flag1, NO_GOAL).canBeReplacedBy(goal)) { -@@ -85,7 +96,136 @@ public class GoalSelector { +@@ -85,7 +96,131 @@ public class GoalSelector { return true; } @@ -748,12 +743,7 @@ index e82e32407cec6109b9c3b0106295217f4a3f4aa2..852393af4013178562940f6a0d07e536 + ctx.state = true; + } + -+ for (Goal.Flag flag : GOAL_FLAG_VALUES) { -+ WrappedGoal goal = this.lockedFlags.get(flag); -+ if (goal != null && !goal.isRunning()) { -+ this.lockedFlags.remove(flag); -+ } -+ } ++ this.lockedFlags.entrySet().removeIf(entry -> !entry.getValue().isRunning()); + + ctxIndex = 0; + ctx.state = true; @@ -801,7 +791,7 @@ index e82e32407cec6109b9c3b0106295217f4a3f4aa2..852393af4013178562940f6a0d07e536 + if (!ctx.state) { + switch (goal.getGoal()) { + case net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal t -> t.poll(); -+ case net.minecraft.world.entity.ai.goal.target.ResetUniversalAngerTargetGoal t -> t.poll(); ++ case net.minecraft.world.entity.ai.goal.target.ResetUniversalAngerTargetGoal t -> t.poll(); + default -> {} + } + } @@ -860,7 +850,7 @@ index e82e32407cec6109b9c3b0106295217f4a3f4aa2..852393af4013178562940f6a0d07e536 for (WrappedGoal wrappedGoal : this.availableGoals) { if (wrappedGoal.isRunning() && (goalContainsAnyFlags(wrappedGoal, this.goalTypes) || !wrappedGoal.canContinueToUse())) { // Paper - Perf: optimize goal types by removing streams wrappedGoal.stop(); -@@ -116,6 +256,24 @@ public class GoalSelector { +@@ -116,6 +251,18 @@ public class GoalSelector { } public void tickRunningGoals(boolean tickAllRunning) { @@ -872,12 +862,6 @@ index e82e32407cec6109b9c3b0106295217f4a3f4aa2..852393af4013178562940f6a0d07e536 + availableGoalsDirty = false; + } + ctxState = tickAllRunning ? 2 : 3; -+ } else { -+ for (WrappedGoal wrappedGoal : java.util.Objects.requireNonNull(this.ctxGoals)) { -+ if (wrappedGoal.isRunning() && (tickAllRunning || wrappedGoal.requiresUpdateEveryTick())) { -+ wrappedGoal.tick(); -+ } -+ } + } + return; + } @@ -886,7 +870,7 @@ index e82e32407cec6109b9c3b0106295217f4a3f4aa2..852393af4013178562940f6a0d07e536 if (wrappedGoal.isRunning() && (tickAllRunning || wrappedGoal.requiresUpdateEveryTick())) { wrappedGoal.tick(); diff --git a/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java b/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java -index be59d0c27a83b329ec3f97c029cfb9c114e22472..5c70f33d9633326cdc59fd0812f49aa38b3d6e14 100644 +index be59d0c27a83b329ec3f97c029cfb9c114e22472..86f041ff21cf44a0cded5744055654a0bff40c42 100644 --- a/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java +++ b/net/minecraft/world/entity/ai/goal/LlamaFollowCaravanGoal.java @@ -20,20 +20,83 @@ public class LlamaFollowCaravanGoal extends Goal { @@ -951,7 +935,7 @@ index be59d0c27a83b329ec3f97c029cfb9c114e22472..5c70f33d9633326cdc59fd0812f49aa3 + // Leaf start - Async Target Finding + Llama llama = poll(); + double d = Double.MAX_VALUE; -+ if (org.dreeam.leaf.config.modules.async.AsyncTargetFinding.searchEntity) { ++ if (org.dreeam.leaf.config.modules.async.AsyncTargetFinding.enabled) { + if (llama == null) { + findTargetAsync(); + return false; @@ -1096,10 +1080,10 @@ index 6463c3c9b08d6058f2843c225b08a40fc30a960b..98c2b4a298ada4b02afa55f991791d86 @Override public boolean canContinueToUse() { diff --git a/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java b/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java -index 3f080b15543bf8c5fa0774b62d7f12e13b82511a..52e353ff7865cc187d83ada4e93dc955ead2a578 100644 +index 3f080b15543bf8c5fa0774b62d7f12e13b82511a..9b8453ce2bc2cafca7c670d79b40434e7c93afca 100644 --- a/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java +++ b/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java -@@ -41,14 +41,67 @@ public abstract class MoveToBlockGoal extends Goal { +@@ -41,8 +41,60 @@ public abstract class MoveToBlockGoal extends Goal { this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.JUMP)); } @@ -1147,7 +1131,6 @@ index 3f080b15543bf8c5fa0774b62d7f12e13b82511a..52e353ff7865cc187d83ada4e93dc955 + Strider, + TurtleToWater, + TurtleLay, -+ Unknown, + } + // Leaf end - Async search block + @@ -1161,32 +1144,20 @@ index 3f080b15543bf8c5fa0774b62d7f12e13b82511a..52e353ff7865cc187d83ada4e93dc955 if (this.nextStartTick > 0) { this.nextStartTick--; return false; - } else { - this.nextStartTick = this.nextStartTick(this.mob); -- return this.findNearestBlock(); -+ return this.findNearestBlockAsync(); // Leaf - Async search block - } +@@ -109,6 +161,12 @@ public abstract class MoveToBlockGoal extends Goal { } -@@ -108,6 +161,17 @@ public abstract class MoveToBlockGoal extends Goal { - return this.reachedTarget; - } - -+ // Leaf start - Async search block -+ protected boolean findNearestBlockAsync() { -+ if (org.dreeam.leaf.config.modules.async.AsyncTargetFinding.searchBlock -+ && this.typeToCheck() != TypeToCheck.Unknown) { + protected boolean findNearestBlock() { ++ // Leaf start - Async search block ++ if (org.dreeam.leaf.config.modules.async.AsyncTargetFinding.searchBlock) { + getBlockAsync(); + return false; + } -+ return findNearestBlock(); -+ } -+ // Leaf end - Async search block -+ - protected boolean findNearestBlock() { ++ // Leaf end - Async search block int i = this.searchRange; int i1 = this.verticalSearchRange; -@@ -133,5 +197,108 @@ public abstract class MoveToBlockGoal extends Goal { + BlockPos blockPos = this.mob.blockPosition(); +@@ -133,5 +191,105 @@ public abstract class MoveToBlockGoal extends Goal { return false; } @@ -1229,9 +1200,7 @@ index 3f080b15543bf8c5fa0774b62d7f12e13b82511a..52e353ff7865cc187d83ada4e93dc955 protected abstract boolean isValidTarget(LevelReader level, BlockPos pos); + + // Leaf start - Async search block -+ protected TypeToCheck typeToCheck() { -+ return TypeToCheck.Unknown; -+ } ++ protected abstract TypeToCheck typeToCheck(); + + private static boolean isValidTargetAsync( + TypeToCheck type, @@ -1289,7 +1258,6 @@ index 3f080b15543bf8c5fa0774b62d7f12e13b82511a..52e353ff7865cc187d83ada4e93dc955 + case TurtleLay -> { + return level.isEmptyBlock(pos.above()) && net.minecraft.world.level.block.TurtleEggBlock.isSand(level, pos); + } -+ case Unknown -> throw new IllegalStateException(); + case null -> throw new IllegalStateException(); + } + // Leaf end - Async search block @@ -1359,10 +1327,10 @@ index 3c274d917bca9de87abfb842f5f332e112a7a2d7..2491b84641443ecfb8afc3b179e1cf80 public boolean canContinueToUse() { return this.tick > 0; diff --git a/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java b/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java -index c67a88c9c77ece7c85ffb169ac96da4f28291228..93bf51c9299354be2a581618bd31d2c4cc7c1d30 100644 +index c67a88c9c77ece7c85ffb169ac96da4f28291228..14d9b492ba431d534e0c6a567d0b7700b4c8a02d 100644 --- a/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java +++ b/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java -@@ -37,10 +37,17 @@ public class RemoveBlockGoal extends MoveToBlockGoal { +@@ -37,7 +37,14 @@ public class RemoveBlockGoal extends MoveToBlockGoal { public boolean canUse() { if (!getServerLevel(this.removerMob).purpurConfig.zombieBypassMobGriefing == !getServerLevel(this.removerMob).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { // Purpur - Add mobGriefing bypass to everything affected return false; @@ -1377,11 +1345,7 @@ index c67a88c9c77ece7c85ffb169ac96da4f28291228..93bf51c9299354be2a581618bd31d2c4 + if (this.nextStartTick > 0) { this.nextStartTick--; return false; -- } else if (this.findNearestBlock()) { -+ } else if (this.findNearestBlockAsync()) { // Leaf - async search block - this.nextStartTick = reducedTickDelay(20); - return true; - } else { + } else if (this.findNearestBlock()) { @@ -151,8 +158,15 @@ public class RemoveBlockGoal extends MoveToBlockGoal { protected boolean isValidTarget(LevelReader level, BlockPos pos) { ChunkAccess chunk = level.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4); // Paper - Prevent AI rules from loading chunks @@ -1400,10 +1364,10 @@ index c67a88c9c77ece7c85ffb169ac96da4f28291228..93bf51c9299354be2a581618bd31d2c4 + // Leaf end - Async search block } diff --git a/net/minecraft/world/entity/ai/goal/TemptGoal.java b/net/minecraft/world/entity/ai/goal/TemptGoal.java -index f88f618d34fb343b31de3af1a875d6633703df71..22656c9ef08a65e23cabdd9f6af84f3d2279b075 100644 +index f88f618d34fb343b31de3af1a875d6633703df71..754c379b42cf65c1d2278b474cdfbe50e9e62b34 100644 --- a/net/minecraft/world/entity/ai/goal/TemptGoal.java +++ b/net/minecraft/world/entity/ai/goal/TemptGoal.java -@@ -36,14 +36,43 @@ public class TemptGoal extends Goal { +@@ -36,12 +36,51 @@ public class TemptGoal extends Goal { this.targetingConditions = TEMPT_TARGETING.copy().selector((entity, level) -> this.shouldFollow(entity)); } @@ -1433,22 +1397,28 @@ index f88f618d34fb343b31de3af1a875d6633703df71..22656c9ef08a65e23cabdd9f6af84f3d this.calmDown--; return false; } else { -- this.player = getServerLevel(this.mob) -- .getNearestPlayer(this.targetingConditions.range(this.mob.getAttributeValue(Attributes.TEMPT_RANGE)), this.mob); + // Leaf start - Async Tempt Finding + if (org.dreeam.leaf.config.modules.async.AsyncTargetFinding.searchEntity) { -+ if (!poll()) { ++ if (poll()) { ++ if (this.player != null) { ++ org.bukkit.event.entity.EntityTargetLivingEntityEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTargetLivingEvent(this.mob, this.player, org.bukkit.event.entity.EntityTargetEvent.TargetReason.TEMPT); ++ if (event.isCancelled()) { ++ return false; ++ } ++ this.player = (event.getTarget() == null) ? null : ((org.bukkit.craftbukkit.entity.CraftLivingEntity) event.getTarget()).getHandle(); ++ } ++ if (this.player != null) { ++ return true; ++ } ++ } else { + getNearestPlayerAsync(); + return false; + } -+ } else { -+ this.player = getServerLevel(this.mob) -+ .getNearestPlayer(this.targetingConditions.range(this.mob.getAttributeValue(Attributes.TEMPT_RANGE)), this.mob); + } + // Leaf end - Async Tempt Finding + this.player = getServerLevel(this.mob) + .getNearestPlayer(this.targetingConditions.range(this.mob.getAttributeValue(Attributes.TEMPT_RANGE)), this.mob); // CraftBukkit start - if (this.player != null) { - org.bukkit.event.entity.EntityTargetLivingEntityEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityTargetLivingEvent(this.mob, this.player, org.bukkit.event.entity.EntityTargetEvent.TargetReason.TEMPT); diff --git a/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java b/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java index 4644f3f7af89623ca6218c0dd24bb6cd67db553b..c9750ad322ddaa9c457f0e652d87c7abf8559358 100644 --- a/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java @@ -1628,10 +1598,10 @@ index 25fe78116ce01eeefe5c958423734195d27302eb..e306c1cfc44878ea130d8046b31cf617 mob.setTarget(target, org.bukkit.event.entity.EntityTargetEvent.TargetReason.TARGET_ATTACKED_NEARBY_ENTITY, true); // CraftBukkit - reason } diff --git a/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java b/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java -index 85eae0a14f7a417dfd8c911079d05354a98e5834..745ef9ea16171d8dad3082878f8883031bd79a84 100644 +index 85eae0a14f7a417dfd8c911079d05354a98e5834..f59d5c9be0eb10f5b5192442e1850900d71a31e9 100644 --- a/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java +++ b/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java -@@ -41,12 +41,52 @@ public class NearestAttackableTargetGoal extends TargetG +@@ -41,8 +41,43 @@ public class NearestAttackableTargetGoal extends TargetG this.targetConditions = TargetingConditions.forCombat().range(this.getFollowDistance()).selector(selector); } @@ -1644,12 +1614,7 @@ index 85eae0a14f7a417dfd8c911079d05354a98e5834..745ef9ea16171d8dad3082878f888303 + return true; + } + -+ protected void findTargetAsync() { -+ if (!org.dreeam.leaf.config.modules.async.AsyncTargetFinding.searchEntity) { -+ this.findTarget(); -+ return; -+ } -+ ++ private void findTargetAsync() { + final Mob mob = this.mob; + final var ctx = mob.getGoalCtx(); + if (!ctx.state) return; @@ -1680,16 +1645,27 @@ index 85eae0a14f7a417dfd8c911079d05354a98e5834..745ef9ea16171d8dad3082878f888303 if (this.randomInterval > 0 && this.mob.getRandom().nextInt(this.randomInterval) != 0) { return false; } else { -- this.findTarget(); -+ this.findTargetAsync(); // Leaf - Async target finding - return this.target != null; - } - } +@@ -57,6 +92,15 @@ public class NearestAttackableTargetGoal extends TargetG + + protected void findTarget() { + ServerLevel serverLevel = getServerLevel(this.mob); ++ ++ // Leaf start - Async Target Finding ++ if (org.dreeam.leaf.config.modules.async.AsyncTargetFinding.searchEntity) { ++ this.findTargetAsync(); ++ this.target = null; ++ return; ++ } ++ // Leaf end - Async Target Finding ++ + if (this.targetType != Player.class && this.targetType != ServerPlayer.class) { + this.target = serverLevel.getNearestEntity( + this.mob.level().getEntitiesOfClass(this.targetType, this.getTargetSearchArea(this.getFollowDistance()), entity -> true), diff --git a/net/minecraft/world/entity/ai/goal/target/NearestHealableRaiderTargetGoal.java b/net/minecraft/world/entity/ai/goal/target/NearestHealableRaiderTargetGoal.java -index 4604a603c4ddd0a9242e859aaa5a511c2d4c4f84..84c7b89e7c894c0f544cf0ffcf9dff3f6a5919cc 100644 +index 4604a603c4ddd0a9242e859aaa5a511c2d4c4f84..7274dfb52ca4a08cdebcd04294cedc73460593e5 100644 --- a/net/minecraft/world/entity/ai/goal/target/NearestHealableRaiderTargetGoal.java +++ b/net/minecraft/world/entity/ai/goal/target/NearestHealableRaiderTargetGoal.java -@@ -23,12 +23,20 @@ public class NearestHealableRaiderTargetGoal extends Nea +@@ -23,7 +23,15 @@ public class NearestHealableRaiderTargetGoal extends Nea @Override public boolean canUse() { @@ -1706,12 +1682,6 @@ index 4604a603c4ddd0a9242e859aaa5a511c2d4c4f84..84c7b89e7c894c0f544cf0ffcf9dff3f return false; } else if (!((Raider)this.mob).hasActiveRaid()) { return false; - } else { -- this.findTarget(); -+ this.findTargetAsync(); // Leaf - Async target finding - return this.target != null; - } - } diff --git a/net/minecraft/world/entity/ai/goal/target/NonTameRandomTargetGoal.java b/net/minecraft/world/entity/ai/goal/target/NonTameRandomTargetGoal.java index abf57494950f55bbd75f335f26736cb9e703c197..efd2418f56c36e7850edde483a2a4906dd622441 100644 --- a/net/minecraft/world/entity/ai/goal/target/NonTameRandomTargetGoal.java diff --git a/leaf-server/minecraft-patches/features/0163-Protocol-Core.patch b/leaf-server/minecraft-patches/features/0163-Protocol-Core.patch index 67e20d21..3aa9a319 100644 --- a/leaf-server/minecraft-patches/features/0163-Protocol-Core.patch +++ b/leaf-server/minecraft-patches/features/0163-Protocol-Core.patch @@ -22,10 +22,10 @@ index 7e19dfe90a63ff26f03b95891dacb7360bba5a3c..5d0961f06c23121883c4f0b889ccdf32 } diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index 98af1ad020a003db66d7319f33d43deec315aec5..9669036e6b7f1830888e48c99acb01d443f4e9f0 100644 +index c92005fcb9c6fdf63bf0854122d59d130315db85..e3194caff68499028d61d5755edce29b5c23253f 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java -@@ -1839,6 +1839,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop implements ca.spottedleaf.moonrise.patch - return ""; - } else { - Property property = propertyEntry.getKey(); -- return property.getName() + "=" + this.getName(property, propertyEntry.getValue()); -+ return property.getName() + "=" + this.getName(property, propertyEntry.getValue()); // Leaf - paw optimization - diff on change - } - } - -@@ -73,13 +73,37 @@ public abstract class StateHolder implements ca.spottedleaf.moonrise.patch - stringBuilder.append(this.owner); - if (!this.getValues().isEmpty()) { - stringBuilder.append('['); -- stringBuilder.append(this.getValues().entrySet().stream().map(PROPERTY_ENTRY_TO_STRING_FUNCTION).collect(Collectors.joining(","))); -+ // Leaf start - paw optimization -+ int i = 0; -+ for (Map.Entry, Comparable> propertyEntry : this.getValues().entrySet()) { -+ if (propertyEntry == null) { -+ stringBuilder.append(""); -+ } else { -+ Property property = propertyEntry.getKey(); -+ Comparable value = propertyEntry.getValue(); -+ -+ stringBuilder.append(property.getName()).append("=").append(getValueName(property, value)); -+ } -+ -+ if (i < this.getValues().size() - 1) { -+ stringBuilder.append(","); -+ } -+ -+ i++; -+ } -+ // Leaf end - paw optimization - stringBuilder.append(']'); - } - - return stringBuilder.toString(); - } - -+ // Leaf start - paw optimization -+ private > String getValueName(Property property, Comparable value) { -+ return property.getName((T) value); -+ } -+ // Leaf end - paw optimization -+ - public Collection> getProperties() { - return this.optimisedTable.getProperties(); // Paper - optimise blockstate property access - } diff --git a/net/minecraft/world/level/levelgen/structure/structures/DesertPyramidStructure.java b/net/minecraft/world/level/levelgen/structure/structures/DesertPyramidStructure.java index 03ec2264b19e1794b609fe09d1ceaba4e0c4d669..3f38fe0140d13c7c356340ba06b55469ede0a1ad 100644 --- a/net/minecraft/world/level/levelgen/structure/structures/DesertPyramidStructure.java diff --git a/leaf-server/paper-patches/features/0030-SparklyPaper-Parallel-world-ticking.patch b/leaf-server/paper-patches/features/0030-SparklyPaper-Parallel-world-ticking.patch index 2defa582..32ce99bb 100644 --- a/leaf-server/paper-patches/features/0030-SparklyPaper-Parallel-world-ticking.patch +++ b/leaf-server/paper-patches/features/0030-SparklyPaper-Parallel-world-ticking.patch @@ -255,7 +255,7 @@ index 548fcd9646dee0c40b6ba9b3dafb9ca157dfe324..d7af94890bfccd6ff665d920cecfa1e5 } else if (!event.isAsynchronous() && !this.server.isPrimaryThread() && !this.server.isStopping()) { // Leaf start - Multithreaded tracker diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index af33cab59932f4ec135caf94dc5828930833daf6..92463ddc6fdcf542ce4a6d2a5059d4a9f7a6085a 100644 +index af33cab59932f4ec135caf94dc5828930833daf6..caa92e48d031cb54950e6613a82f407d7ed2455a 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -455,7 +455,12 @@ public class CraftWorld extends CraftRegionAccessor implements World { @@ -313,16 +313,7 @@ index af33cab59932f4ec135caf94dc5828930833daf6..92463ddc6fdcf542ce4a6d2a5059d4a9 net.minecraft.world.level.Level.ExplosionInteraction explosionType; if (!breakBlocks) { explosionType = net.minecraft.world.level.Level.ExplosionInteraction.NONE; // Don't break blocks -@@ -956,6 +972,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); -@@ -986,6 +1004,8 @@ public class CraftWorld extends CraftRegionAccessor implements World { +@@ -986,6 +1002,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); @@ -331,7 +322,7 @@ index af33cab59932f4ec135caf94dc5828930833daf6..92463ddc6fdcf542ce4a6d2a5059d4a9 if (this.world.hasChunkAt(pos)) { net.minecraft.world.level.chunk.LevelChunk chunk = this.world.getChunkAt(pos); -@@ -2328,6 +2348,8 @@ public class CraftWorld extends CraftRegionAccessor implements World { +@@ -2328,6 +2346,8 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void sendGameEvent(Entity sourceEntity, org.bukkit.GameEvent gameEvent, Vector position) { @@ -341,22 +332,124 @@ index af33cab59932f4ec135caf94dc5828930833daf6..92463ddc6fdcf542ce4a6d2a5059d4a9 } // 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 811823a1a7e24a19a7e37eb4c08efdfa19e839ed..b94efcab12d41fa8745e5bb55cfa6481d8262e74 100644 +index 811823a1a7e24a19a7e37eb4c08efdfa19e839ed..ac45c5cbe547705e3e341011740cf911c39f80c0 100644 --- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java -@@ -75,6 +75,11 @@ public class CraftBlock implements Block { +@@ -74,13 +74,98 @@ public class CraftBlock implements Block { + return new CraftBlock(world, position); } - public net.minecraft.world.level.block.state.BlockState getNMS() { -+ // 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 read world asynchronously"); ++ // 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); + } -+ // SparklyPaper end - parallel world ticking - return this.world.getBlockState(this.position); ++ } ++ ++ 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 } -@@ -157,6 +162,11 @@ public class CraftBlock implements Block { + // Paper start + 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 + } + // Paper end + +@@ -144,10 +229,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, 3); + } + ++ @Deprecated // Leaf - SparklyPaper - parallel world ticking - Magic value + public void setData(final byte data, boolean applyPhysics) { + if (applyPhysics) { + this.setData(data, 3); +@@ -157,12 +244,18 @@ public class CraftBlock implements Block { } private void setData(final byte data, int flag) { @@ -368,7 +461,23 @@ index 811823a1a7e24a19a7e37eb4c08efdfa19e839ed..b94efcab12d41fa8745e5bb55cfa6481 this.world.setBlock(this.position, CraftMagicNumbers.getBlock(this.getType(), data), flag); } -@@ -198,6 +208,12 @@ public class CraftBlock implements Block { + @Override ++ @Deprecated // Leaf - SparklyPaper - parallel world ticking - Magic value + public byte getData() { +- net.minecraft.world.level.block.state.BlockState blockData = this.world.getBlockState(this.position); ++ net.minecraft.world.level.block.state.BlockState blockData = this.getNMS(); // Leaf - SparklyPaper - parallel world ticking + return CraftMagicNumbers.toLegacyData(blockData); + } + +@@ -179,6 +272,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); + } + +@@ -198,6 +292,12 @@ public class CraftBlock implements Block { } public static boolean setTypeAndData(LevelAccessor world, BlockPos position, net.minecraft.world.level.block.state.BlockState old, net.minecraft.world.level.block.state.BlockState blockData, boolean applyPhysics) { @@ -381,27 +490,205 @@ index 811823a1a7e24a19a7e37eb4c08efdfa19e839ed..b94efcab12d41fa8745e5bb55cfa6481 // SPIGOT-611: need to do this to prevent glitchiness. Easier to handle this here (like /setblock) than to fix weirdness in tile entity cleanup if (old.hasBlockEntity() && blockData.getBlock() != old.getBlock()) { // SPIGOT-3725 remove old tile entity if block changes // SPIGOT-4612: faster - just clear tile -@@ -343,18 +359,33 @@ public class CraftBlock implements Block { +@@ -226,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) { +@@ -282,51 +422,37 @@ public class CraftBlock implements Block { + + @Override + public String toString() { +- return "CraftBlock{pos=" + this.position + ",type=" + this.getType() + ",data=" + this.getNMS() + ",fluid=" + this.world.getFluidState(this.position) + '}'; ++ return "CraftBlock{pos=" + this.position + ",type=" + this.getType() + ",data=" + this.getNMS() + ",fluid=" + this.getNMSFluid() + '}'; // Leaf - SparklyPaper - parallel world ticking + } + + 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; ++ default -> BlockFace.SELF; ++ }; ++ // 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 +@@ -343,18 +469,65 @@ public class CraftBlock implements Block { @Override public Biome getBiome() { -+ // 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 read world asynchronously"); +- 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 ++ } + } -+ // SparklyPaper end - parallel world ticking - return this.getWorld().getBiome(this.getX(), this.getY(), this.getZ()); ++ // Leaf end - SparklyPaper - parallel world ticking } // Paper start @Override public Biome getComputedBiome() { -+ // 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 read world asynchronously"); +- 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 ++ } + } -+ // SparklyPaper end - parallel world ticking - return this.getWorld().getComputedBiome(this.getX(), this.getY(), this.getZ()); ++ // Leaf end - SparklyPaper - parallel world ticking } // Paper end @@ -415,101 +702,756 @@ index 811823a1a7e24a19a7e37eb4c08efdfa19e839ed..b94efcab12d41fa8745e5bb55cfa6481 this.getWorld().setBiome(this.getX(), this.getY(), this.getZ(), bio); } -@@ -375,6 +406,11 @@ public class CraftBlock implements Block { +@@ -370,12 +543,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() { -+ // 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 read world asynchronously"); +- 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; ++ } + } -+ // SparklyPaper end - parallel world ticking - return this.world.getMinecraftWorld().hasNeighborSignal(this.position); ++ // Leaf end - SparklyPaper - parallel world ticking } -@@ -414,6 +450,11 @@ public class CraftBlock implements Block { + @Override +@@ -397,46 +608,102 @@ public class CraftBlock implements Block { @Override - public int getBlockPower(BlockFace face) { -+ // 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 read world asynchronously"); + 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; + } -+ // SparklyPaper end - parallel world ticking - int power = 0; - net.minecraft.world.level.Level world = this.world.getMinecraftWorld(); - int x = this.getX(); -@@ -484,6 +525,11 @@ public class CraftBlock implements Block { ++ // Leaf end - SparklyPaper - parallel world ticking + } + + @Override + public boolean isBlockFaceIndirectlyPowered(BlockFace face) { +- int power = this.world.getMinecraftWorld().getSignal(this.position, CraftBlock.blockFaceToNotch(face)); +- +- Block relative = this.getRelative(face); +- if (relative.getType() == Material.REDSTONE_WIRE) { +- return Math.max(power, relative.getData()) > 0; ++ // 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); ++ 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 i, net.minecraft.world.level.block.state.BlockState iblockdata) { +- if (!iblockdata.is(Blocks.REDSTONE_WIRE)) { +- return i; ++ 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 { +- int j = iblockdata.getValue(RedStoneWireBlock.POWER); ++ // 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; ++ int x = this.getX(); int y = this.getY(); int z = this.getZ(); ++ 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; ++ } ++ } ++ } + +- return j > i ? j : i; ++ // 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 { ++ int neighborPower = neighborState.getValue(RedStoneWireBlock.POWER); ++ return Math.max(neighborPower, currentMax); + } + } ++ // Leaf end - SparklyPaper - parallel world ticking + + @Override + public int getBlockPower() { +@@ -479,105 +746,179 @@ 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() { -+ // 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"); +- 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."); ++ } + } -+ // SparklyPaper end - parallel world ticking - return this.breakNaturally(null); ++ // 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 } -@@ -543,6 +589,11 @@ public class CraftBlock implements Block { + @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 + public boolean breakNaturally(ItemStack item, boolean triggerEffect, boolean dropExperience) { + // 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 iblockdata = this.getNMS(); + net.minecraft.world.level.block.Block block = iblockdata.getBlock(); + net.minecraft.world.item.ItemStack nmsItem = CraftItemStack.asNMSCopy(item); +- boolean result = false; +- +- // Modelled off EntityHuman#hasBlock +- if (block != Blocks.AIR && (item == null || !iblockdata.requiresCorrectToolForDrops() || nmsItem.isCorrectToolForDrops(iblockdata))) { +- net.minecraft.world.level.block.Block.dropResources(iblockdata, this.world.getMinecraftWorld(), this.position, this.world.getBlockEntity(this.position), null, nmsItem, false); // Paper - Properly handle xp dropping +- // Paper start - improve Block#breanNaturally +- if (triggerEffect) { +- if (iblockdata.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(iblockdata)); ++ boolean droppedItems = false; ++ ++ // Experience dropping requires ServerLevel ++ ServerLevel serverLevelForDrops = getServerLevel(); // Re-get ServerLevel specifically for drop logic ++ ++ if (serverLevelForDrops != null) { // Only attempt drops/XP if we have a ServerLevel ++ // Check if block should drop items ++ if (!iblockdata.isAir() && (item == null || !iblockdata.requiresCorrectToolForDrops() || nmsItem.isCorrectToolForDrops(iblockdata))) { ++ // Drop items using ServerLevel ++ net.minecraft.world.level.block.Block.dropResources(iblockdata, serverLevelForDrops, this.position, this.world.getBlockEntity(this.position), null, nmsItem, false); ++ ++ // Trigger effect using LevelAccessor (safe) ++ if (triggerEffect) { ++ int eventId = (iblockdata.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(iblockdata) : 0; ++ this.world.levelEvent(eventId, this.position, eventData); + } ++ // Drop experience using ServerLevel ++ if (dropExperience) { ++ int xp = block.getExpDrop(iblockdata, 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 && !iblockdata.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 && !iblockdata.isAir()) { ++ int eventId = (iblockdata.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(iblockdata) : 0; ++ this.world.levelEvent(eventId, this.position, eventData); + } +- if (dropExperience) block.popExperience(this.world.getMinecraftWorld(), this.position, block.getExpDrop(iblockdata, this.world.getMinecraftWorld(), this.position, nmsItem, true)); +- // Paper end +- result = true; + } + +- // SPIGOT-6778: Directly call setBlock instead of setTypeAndData, so that the tile entiy 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, iblockdata); + } +- 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, iblockdata); ++ turtleEggBlock.decreaseEggs(nmsLevelForSpecialCases, this.position, iblockdata); + } ++ } 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) { -+ // 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"); ++ // 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; + } -+ // SparklyPaper end - parallel world ticking ++ ++ // Original logic requires ServerLevel (level is guaranteed non-null here) Direction direction = CraftBlock.blockFaceToNotch(face); BlockFertilizeEvent event = null; - ServerLevel world = this.getCraftWorld().getHandle(); -@@ -554,8 +605,8 @@ public class CraftBlock implements Block { - world.captureTreeGeneration = false; +- 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)); - if (world.capturedBlockStates.size() > 0) { +- // SPIGOT-6895: Call StructureGrowEvent and BlockFertilizeEvent +- world.captureTreeGeneration = true; ++ level.captureTreeGeneration = true; + InteractionResult result = BoneMealItem.applyBonemeal(context); +- world.captureTreeGeneration = false; +- +- if (world.capturedBlockStates.size() > 0) { - TreeType treeType = SaplingBlock.treeType; - SaplingBlock.treeType = null; -+ TreeType treeType = SaplingBlock.getTreeTypeRT(); // SparklyPaper - parallel world ticking // Leaf - SparklyPaper - parallel world ticking mod (collapse original behavior) -+ SaplingBlock.setTreeTypeRT(null); // SparklyPaper - parallel world ticking // Leaf - SparklyPaper - parallel world ticking mod (collapse original behavior) - List blocks = new ArrayList<>(world.capturedBlockStates.values()); - world.capturedBlockStates.clear(); +- List blocks = new ArrayList<>(world.capturedBlockStates.values()); +- world.capturedBlockStates.clear(); ++ level.captureTreeGeneration = false; ++ ++ if (level.capturedBlockStates.size() > 0) { ++ 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; -@@ -644,6 +695,11 @@ public class CraftBlock implements Block { + + if (treeType != null) { +- structureEvent = new StructureGrowEvent(this.getLocation(), treeType, true, null, blocks); ++ structureEvent = new StructureGrowEvent(this.getLocation(), treeType, true, null, bukkitStates); + Bukkit.getPluginManager().callEvent(structureEvent); + } + +- event = new BlockFertilizeEvent(CraftBlock.at(world, this.getPosition()), null, blocks); ++ 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 blockstate : blocks) { +- blockstate.update(true); +- world.checkCapturedTreeStateForObserverNotify(this.position, (org.bukkit.craftbukkit.block.CraftBlockState) blockstate); // Paper - notify observers even if grow failed ++ for (org.bukkit.block.BlockState blockstate : bukkitStates) { ++ blockstate.update(true); // This performs the actual block changes ++ // Notify observers using the captured state info ++ level.checkCapturedTreeStateForObserverNotify(this.position, (org.bukkit.craftbukkit.block.CraftBlockState) blockstate); + } + } + } + ++ // 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 +@@ -592,31 +933,70 @@ public class CraftBlock implements Block { + + @Override + public Collection getDrops(ItemStack item, Entity entity) { +- net.minecraft.world.level.block.state.BlockState iblockdata = 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 EntityHuman#hasBlock +- if (item == null || CraftBlockData.isPreferredTool(iblockdata, nms)) { +- return net.minecraft.world.level.block.Block.getDrops(iblockdata, (ServerLevel) 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 iblockdata = 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 || !iblockdata.requiresCorrectToolForDrops() || nmsItem.isCorrectToolForDrops(iblockdata)) { ++ // Call NMS getDrops using the verified ServerLevel ++ List nmsDrops = net.minecraft.world.level.block.Block.getDrops( ++ iblockdata, 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 iblockdata = this.getNMS(); +- net.minecraft.world.item.ItemStack nms = CraftItemStack.asNMSCopy(item); +- return CraftBlockData.isPreferredTool(iblockdata, nms); ++ net.minecraft.world.item.ItemStack nmsItem = CraftItemStack.asNMSCopy(item); ++ return iblockdata.requiresCorrectToolForDrops() && nmsItem.isCorrectToolForDrops(iblockdata); // Leaf - SparklyPaper - parallel world ticking - Delegate to helper which checks tool tags + } + + @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); +@@ -639,57 +1019,147 @@ 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) { -+ // 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 read world asynchronously"); -+ } -+ // SparklyPaper end - parallel world ticking ++ // 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 a different world"); ++ Preconditions.checkArgument(this.getWorld().equals(start.getWorld()), "Location start cannot be in a different world"); start.checkFinite(); -@@ -685,6 +741,11 @@ public class CraftBlock implements Block { + + 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.toVec3D(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.fromNMS(this.getWorld(), nmsHitResult); ++ } else { ++ // Strict/Disabled/Non-ServerLevel path ++ checkStrictMode(level, handlingMode, "rayTrace"); ++ try { ++ // Calculate start and end points for the ray trace ++ Vec3 startPos = CraftLocation.toVec3D(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.toNMS(fluidCollisionMode), CollisionContext.empty()), ++ this.position // Provide the block's position here ++ ); + +- HitResult nmsHitResult = this.world.clip(new ClipContext(startPos, endPos, ClipContext.Block.OUTLINE, CraftFluidCollisionMode.toNMS(fluidCollisionMode), CollisionContext.empty()), this.position); +- return CraftRayTraceResult.fromNMS(this.getWorld(), nmsHitResult); ++ // Convert NMS result to Bukkit result ++ return CraftRayTraceResult.fromNMS(this.getWorld(), 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) { -+ // 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 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(); -@@ -719,6 +780,11 @@ public class CraftBlock implements Block { +- 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 +@@ -700,7 +1170,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 +@@ -713,26 +1186,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() { -+ // 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"); +- 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; + } -+ // SparklyPaper end - parallel world ticking - final ServerLevel level = this.world.getMinecraftWorld(); ++ 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 768d3f93da2522d467183654260a8bd8653588b1..5cef786fa2e5dfd3e7b79918bc73af02891b0bea 100644 --- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/world/ReadOperationType.java b/leaf-server/src/main/java/org/dreeam/leaf/async/world/ReadOperationType.java new file mode 100644 index 00000000..dffe7ba0 --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/async/world/ReadOperationType.java @@ -0,0 +1,11 @@ +package org.dreeam.leaf.async.world; + +public enum ReadOperationType { + BLOCK_GET_BIOME, + BLOCK_GET_COMPUTED_BIOME, + BLOCK_IS_INDIRECTLY_POWERED, + BLOCK_GET_BLOCK_POWER, + BLOCK_RAY_TRACE, + BLOCK_CAN_PLACE, + BLOCK_GET_NMS_STATE +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/world/WorldReadRequest.java b/leaf-server/src/main/java/org/dreeam/leaf/async/world/WorldReadRequest.java new file mode 100644 index 00000000..8fb45b7c --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/async/world/WorldReadRequest.java @@ -0,0 +1,10 @@ +package org.dreeam.leaf.async.world; + +import java.util.concurrent.CompletableFuture; + +public record WorldReadRequest( + ReadOperationType type, + Object[] params, // Parameters for the read operation + CompletableFuture future // Future to complete with the result +) { +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/SparklyPaperParallelWorldTicking.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/SparklyPaperParallelWorldTicking.java index 1c1c4efc..f4b516f3 100644 --- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/SparklyPaperParallelWorldTicking.java +++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/SparklyPaperParallelWorldTicking.java @@ -8,15 +8,19 @@ import org.dreeam.leaf.config.annotations.Experimental; public class SparklyPaperParallelWorldTicking extends ConfigModules { public String getBasePath() { - return EnumConfigCategory.ASYNC.getBaseKeyName() + ".parallel-world-tracking"; - } // TODO: Correct config key when stable + // Corrected path based on your comment + return EnumConfigCategory.ASYNC.getBaseKeyName() + ".parallel-world-ticking"; + } @Experimental public static boolean enabled = false; public static int threads = 8; public static boolean logContainerCreationStacktraces = false; public static boolean disableHardThrow = false; + @Deprecated public static boolean runAsyncTasksSync = false; + // STRICT, BUFFERED, DISABLED + public static String asyncUnsafeReadHandling = "BUFFERED"; @Override public void onLoaded() { @@ -34,15 +38,30 @@ public class SparklyPaperParallelWorldTicking extends ConfigModules { } else { threads = 0; } + logContainerCreationStacktraces = config.getBoolean(getBasePath() + ".log-container-creation-stacktraces", logContainerCreationStacktraces); logContainerCreationStacktraces = enabled && logContainerCreationStacktraces; disableHardThrow = config.getBoolean(getBasePath() + ".disable-hard-throw", disableHardThrow); disableHardThrow = enabled && disableHardThrow; - runAsyncTasksSync = config.getBoolean(getBasePath() + ".run-async-tasks-sync", runAsyncTasksSync); - runAsyncTasksSync = enabled && runAsyncTasksSync; + asyncUnsafeReadHandling = config.getString(getBasePath() + ".async-unsafe-read-handling", asyncUnsafeReadHandling).toUpperCase(); + + if (!asyncUnsafeReadHandling.equals("STRICT") && !asyncUnsafeReadHandling.equals("BUFFERED") && !asyncUnsafeReadHandling.equals("DISABLED")) { + System.err.println("[Leaf] Invalid value for " + getBasePath() + ".async-unsafe-read-handling: " + asyncUnsafeReadHandling + ". Defaulting to STRICT."); + asyncUnsafeReadHandling = "STRICT"; + } + if (!enabled) { + asyncUnsafeReadHandling = "DISABLED"; + } + + runAsyncTasksSync = config.getBoolean(getBasePath() + ".run-async-tasks-sync", false); // Default to false now + if (runAsyncTasksSync) { + System.err.println("[Leaf] WARNING: The setting '" + getBasePath() + ".run-async-tasks-sync' is deprecated. Use 'async-unsafe-read-handling: STRICT' for similar safety checks or 'BUFFERED' for buffered reads."); + } if (enabled) { LeafConfig.LOGGER.info("Using {} threads for Parallel World Ticking", threads); } + + runAsyncTasksSync = enabled && runAsyncTasksSync; // Auto-disable if main feature is off } }